Yii2 SluggableBehavior
  • 1463

SluggableBehavior поведение yii2 для создания url.

Автор: admin | 01 мая (Вт.) 2018г. в 22ч.52м.

Вопрос создания читаемых url адресов всегда был одним из важных как в сфере SEO так и для
удобства для пользователя веб сайта. Набор идентификаторов в url не всегда приемлем и понятен,
такой адрес сложно запомнить. Поэтому в адресах указывается какое-то слово или словосочетание как
правило на англисском или транслит, с тире в качестве разделителей.
Вот пример того, как выглядет slug (иди алиас в url): site.com/how-to-program-with-yii2-blameable-behaviors
Эти словосочетания, которые идентифицируют страницу в url называются slug или alias. Как правило, при создании страницы в админке сайта создается и данный slug, либо автоматически на основе, например
заголовка статьи или тайтла, либо вводятся вручную со всеми правилами написания валидного url.

В данной стаятье рассмотрим помощника для автоматического генерирования slug из оприделенного
поля при создании страницы. Данный помощник уже имеется в yii2 из коробки. Это поведение, которое
называется SluggableBehavior.

Класс yii\behaviors\SluggableBehavior автоматически заполняет указанный атрибут значением, которое можно использовать в URL-адресе.
Это поведение зависит от расширения php-intl для транслитерации.
Если он не установлен, он использует набор символов для замены,
который определен в yii\helpers\Inflector::$transliteration . К этому методу вернемся позже.

Итак, перейдем к практике. Допустим, у нас есть таблица записей для блога.

blog_articles
----------------
`id` int(10) UNSIGNED NOT NULL,
`alias` varchar(255) NOT NULL, UNIQUE,
`title` text NOT NULL,
`text` text NOT NULL,​

Я сделал простую структуру таблицы, чтобы больше сконцентрироваться на сути. Тут есть поле
`alias`, которое и будет содержать строку для вставки в урл

Итак, при создании нового поста блога нужно заполнить эти поля. Id формируется автоматически в базе
данных. Поэтому нужно заполнить только `alias`, `title` и `text`. Создадим поля формы для заполнения
этих данных в файле видов form.php

 use yii\widgets\ActiveForm;
 
 
    <?php $form = ActiveForm::begin(); ?>
 
    <?php echo $form->field($model, 'alias')->textInput() ?>
    <?php echo $form->field($model, 'title')->textInput() ?>
    <?php echo $form->field($model, 'text')->textInput() ?>

    <div class="form-group">
        <?= Html::submitButton(Yii::t('app', 'Save'), ['class' => 'btn btn-success']) ?>
    </div>

    <?php ActiveForm::end(); ?>

В контроллере это будет так:

    public function actionCreate()
    {
        $model = new BlogArticles();

        if ($model->load(Yii::$app->request->post()) && $model->validate())
        {
            $model->save();
        }

        return $this->render('form', ['model' => $model]);

Модель-

class BlogArticles extends \yii\db\ActiveRecord
{

    public function rules()
    {
        return [
            [['alias', 'title', 'text'], 'required'],
            ['alias', 'unique'],
            ];
            
    }     
    
    
    ...

В данном варианте нужно alias вводить в ручную. Теперь изменим код и сделаем автоматическую
генерацию alias:
В form.php комментируем поле для ввода alias

<?php //echo $form->field($model, 'alias')->textInput() ?>

В модели убираем правила валидации для alias, т.к. в данном случае это поле
будет генерироваться автоматически и не к чему его валидировать. Добавляем поведение
для формирования aliasa из строки title:
namespace app\models;
 
use Yii;
use yii\behaviors\SluggableBehavior;

class BlogArticles extends \yii\db\ActiveRecord
{

    public function rules()
    {
        return [
            [['title', 'text'], 'required'],
            ['alias', 'safe'],
            ];
            
    }     
    
    public function behaviors()
    {
        return [
            [
                'class' => SluggableBehavior::className(),
                'attribute' => 'title',
                'slugAttribute' => 'alias',//default name slug
            ],
        ];
    }
    
    ...​

По умолчанию SluggableBehavior будет заполнять атрибут slug значением, которое
можно использовать в URL-адресе, когда ассоциированный объект AR проверяется. В
данном случае slugAttribute генерируется из 'attribute' => 'title' тайтла.

Если поле alias для хранения урл было бы названо как slug, то 'slugAttribute' => 'alias'
можно было бы не указывать, но я для примера назвал поле иначе.

Обратите внимание, что 'attribute' может содержать не только название атрибута для формирования
slugAttribute, но и может быть и список атрибутов в виде массива, а также null.
Если указать 'attribute' => null , то slug будет генерироваться из содержимого
свойства value.


Если в нашей таблице будет поле, например h1, то при желании можно сформировать slug из
двух полей h1 и title перечислив их в массиве вот так:

    public function behaviors()
    {
        return [
            [
                'class' => SluggableBehavior::className(),
                'attribute' => ['title', 'h1'],
                'slugAttribute' => 'alias',//default name slug
            ],
        ];
    }

Если в поля формы в title вбить "это тайтл", а в "h1" - "это заголовок", то SluggableBehavior
переведет нашу кириллицу в латиницу и объеденит строки в таком порядке, как название
полей расположены в массиве.

Вот что получится: 'eto-zagolovok-a-eto-deskripsn'

Так можно наш алиас сформировать из любого количества полей и разделителем между словами
будет тире (-) благодаря этому методу в классе yii\behaviors\SluggableBehavior :

    protected function generateSlug($slugParts)
    {
        return Inflector::slug(implode('-', $slugParts));
    }

Если такой способ генерации не устраивает, то пишем 'attribute' => null и реализуем
нужную реализацию для создания slug в свойстве value (callable|string|null) .
Это может быть анонимная функция или произвольное значение или null. Если первое,
возвращаемое значение функции будет использоваться как slug.
Если null, то свойство $attribute будет использоваться для создания slug.

    public function behaviors()
    {
        return [
            [
                'class' => SluggableBehavior::className(),
                'attribute' => null,
                'slugAttribute' => 'alias',
                'value' => function ($event){//return slug
                    $array = ['title', 'metaDesc'];
                    foreach ($array as $attribute) {
                        $part = ArrayHelper::getValue($this->owner, $attribute);
                        if ($this->skipOnEmpty && $this->isEmpty($part)) {
                            return $this->owner->{$this->slugAttribute};
                        }
                        $slugParts[] = $part;
                    }
                    
                    $slug = \yii\helpers\Inflector::slug(implode('-', $slugParts));
                    
                    return $slug;
                },
            ],
        ];
    }

В этом примере я сделал такую ще генерацию строки для slug, какая есть по умолчанию
в классе поведения и результат будет таким же 'eto-zagolovok-a-eto-deskripsn' ,
если в поля формы в title вбить "это тайтл", а в "h1" - "это заголовок".

Если нужно изменить разделитель, то тут $slug = \yii\helpers\Inflector::slug(implode('-', $slugParts));
меняем тире на любой допустимый для url символ, например нижнее подчеркивание:
 $slug = \yii\helpers\Inflector::slug(implode('_', $slugParts), '_');

Также нужно обратить внимание, что доступ к текущей модели, в которой объявлено поведение
реализован через свойство $this->owner . Именно тут хранится объект модели. А так как
 'value' => function ($event){//return slug это функция обратного вызова, и она отработает внутри
класса поведения, то внутри нее обращаемся к текущей модели как $this->owner
Зная это можно внутри анонимной функции обращаться и к другим моделям для
того, чтобы получить данные и сформировать url. Например, если для страницы блога
slug нужен в виде строки с алиасом категории и алиасом статьи categoru-alias/article-title

Допустим, у нас есть модель категорий постов блога и при создании статьи, указывается
родительская категория в виде списка с title из категории. В файле form.php добавляем поле
для выбора категории:

    <?php $form = ActiveForm::begin(); ?>

    <?= $form->field($model, 'selectedCategory')
            ->dropDownList(
                    ArrayHelper::map(BlogCategories::find()->all(), 'id', 'title'), 
                    ['prompt'=>'Выбрать категорию']); ?>

Делаем обработку SluggableBehavior в модели:

    public function behaviors()
    {
        return [
            [
                'class' => SluggableBehavior::className(),
                'attribute' => null,
                'slugAttribute' => 'alias',
                'value' => function ($event){//return slug
                    
                    $title = $this->owner->title;
                    $catId = $this->owner->selectedCategory;

                    $cat = BlogCategories::findOne($catId);
                    $slug = $cat->alias."/".\yii\helpers\Inflector::slug($title,'-');
//                    var_dump($slug);die;
                    return $slug;
                },
            ],
        ];

На выходе в alias приходит строка 'yii2/eto-zagolovok', где 'yii2' это алиас категории,а
'eto-zagolovok' строка сгенерированная из title

В общем с value и генерацией slug по своим правилам понятно. Что есть еще?

immutable (boolean) - создавать ли новый slug, если он уже был создан раньше. Может быть
true или false. По умолчанию $immutable = false .
Если true, поведение не будет генерировать новый slug, даже если [[attribute]] изменен.
Польза от этого свойтсва в том, что можно запретить менять тот же alias например, при обновлении
страницы. Ведь для сео будет не очень хорошо, если у одной и той же страницы будет часто меняться
url.

ensureUnique (boolean) - проверка на уникальность поля для alias. По умолчанию false.
Если установить в true, то значение будет проверяться на уникальность в таблице. Если уже такое
значение есть в таблице у выбранного поля, в моем примере это alias, то тогда будет сгенерировано
уникальное значение у поля.

Так, если в базе уже есть алиас типа "eto-tajtl", то при вставке такого же имени сгенерируется
"eto-tajtl-2". Я бы советовал ставить два этих поля в активное состаяние:

'immutable' => true,//неизменный
'ensureUnique'=>true,//генерировать уникальный 

skipOnEmpty (boolean) по умолчанию false - Следует ли пропускать генерации slug, если 'attribute' содержит null или пустую строку.

uniqueValidator (array) - конфигурация массива для проверки достоверности уникальности slug.
Параметр «класс» может быть опущен - по умолчанию [[UniqueValidator]] будет использоваться.

uniqueSlugGenerator (callable) - тут используется функция обратного вызова, которая возвращает уникальный
slug. Это свойство используется, когда активировано свойтво 'ensureUnique'=>true. В примере выше видно,
что уникальный алиас сформировался за счет прибавления к строке числа 2. Вот как раз uniqueSlugGenerator
и позволяет изменить реализацию создания уникального урл.
Вот метод класса yii\behaviors\SluggableBehavior , который обробатывает данные из uniqueSlugGenerator:

/**
     * Generates slug using configured callback or increment of iteration.
     * @param string $baseSlug base slug value
     * @param int $iteration iteration number
     * @return string new slug value
     * @throws \yii\base\InvalidConfigException
     */
    protected function generateUniqueSlug($baseSlug, $iteration)
    {
        if (is_callable($this->uniqueSlugGenerator)) {
            return call_user_func($this->uniqueSlugGenerator, $baseSlug, $iteration, $this->owner);
        }

        return $baseSlug . '-' . ($iteration + 1);
    }
  

Добавим этот код к нашемо поведению и изменим способ уникализации:

'uniqueSlugGenerator' => function ($baseSlug, $iteration, $model)
{
    return $baseSlug . '-' . uniqid(); 
}

uniqid() возвращает уникальный идентификатор с префиксом на основе текущего времени в микросекундах.

Пожалуй на этом все.

Приветствую!

Меня зовут Сергей. Я - автор этого блога.

Если Вам был полезен материал на моем сайте, поддержите пожалуйста мой проект, чтобы о нем узнали другие люди - кликните plizz :) на иконку в соц. сети, чтобы поделиться материалом с другими.