Создаем поведение (behavior) для Yii2
Часто, а на самом деле практически всегда, при создании сайта необходимо, чтобы страницы сайта открывались не по id сущности в базе, а по текстовому идентификатору, назовем его slug.
(из url'а стоило бы убрать и view, но урок не о том)
Самым примитивным путем можно создать в таблице post поле slug, в модели Post соответственно появляется новый атрибут, в представление (view) добавляем новый input, в который ручками вбиваем slug.
Но ручками это делать не всегда интересно (да кого я обманываю, вообще неинтересно), поэтому мы дописываем в модель методы, которые при сохранении модели генерируют slug автоматически из name, проверяют его уникальность в таблице (ведь мы по slug'у будем извлекать post из базы, а, значит, slug не может быть не уникальным), ну и, возможно, транслитерируем его (тестовая-новость => testovaya-novost) — это тоже может быть полезно. Что ж, пишем, привязываемся к событию, тестируем — все работает. И тут при разработке сайта мы сталкиваемся с тем, что slug'и еще нужны в модели Page. А еще в каталоге для товара — пусть это будет модель Item. Можно пойти по пути наименьшего сопротивления — копипаста. Но…
В Yii существует такая вещь как поведения (behaviors) — функционал, позволяющий использовать одни и те же функции в различных моделях. Итак, напишем поведение для slug'ификации.
В нашей модели Post (она же \commoin\models\Post на всякий случай) подключаем еще не созданное поведение:
Создали функцию behaviors, необходимую для подключения, прописали класс, в котором будем находиться поведение и передали в этот класс три атрибута: 1. in_attribute — атрибут модели, из которого будет генерироваться slug (в разных моделях он может отличаться, например name или title) 2. out_attribute — это атрибут соответственно slug'а (slug или alias) 3. translit — тут все понятно
При создании поведения был еще четвертый атрибут — unique, но потом я исключил этот функционал, т.к. очень редко нужно, чтобы slug был не уникальным.
Упомяну, что я использую структуру приложения yii2-app-advanced, то есть у меня есть папки backend и frontend, в которых лежат контроллеры и вьюшки, и папка common, в которой общие модели и поведения.
Класс наследуем от yii\base\Behavior, прописываем три атрибута с начальными установками, создаем метод events, который привяжет поведение к какому-то событию при сохранении модели. Так как slug обычно необходим и может быть прописан в rules как required, то привяжем генерацию slug'а до валидации.
Теперь создадим метод getSlug:
Сам объект модели передается в поведение как $this->owner. Таким образом slug нам будет доступен через обращение к $this->owner->slug или в нашем случае $this->owner->out_attribute>, так как название атрибута slug'а передается в переменную $this->out_attribute. Делаем проверку пуст ли slug при сохранении и, если пуст, то генерируем его из name (заголовок записи). Если же не пуст, то обрабатываем поступивший slug.
В первой строке метода мы функцией slugify убираем ненужные символы и переводим в транслит, если нужно. Давайте сразу ее и рассмотрим:
Что такое транслит? Это передача национальных символов их аналогами в стандартной латинице. Большинство сниппетов, найденных в зарубежном интернете, очищают текст только от умляутов, крышечек и прочих символов ('À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A',), то есть из «грязной» латиницы делают «чистую». Это делает и стандартный хелпер yii2 yii\helpers\Inflector::slug (кстати, за время создания поведения этот метод был несовместимо изменен — разработка над yii2 пока продолжается). В рунете же соответственно добавляют еще замену кириллицы на латиницу. Но хотелось бы создать максимально гибкую транслитерацию. В последней версии yii\helpers\Inflector::slug используется php-расширение intl, в том числе транслитерирующее даже китайские иероглифы, но, как я понимаю, по умолчанию оно не включено (php 5.5.6). Но у замечательного разработчика 2amigos, знакомого всем интересующимся yii, было найдено дополнение transliterator-helper (оно в свою очередь использует идеи из drupal'а, насколько я помню). Представляет оно некоторое количество php-файлов, в которых описаны большинство символов и их замена в латинице. Добавляем в composer.json зависимость "2amigos/transliterator-helper": "2.0.*" , обновляемся и теперь нам доступен dosamigos\helpers\TransliteratorHelper:
Транслитерируем, очищаем от неалфавитных символов, пробелы заменяем на черточку "-".
Если же транслит нам не нужен, то:
Метод slug (урезанная версия yii\helpers\Inflector::slug без транлитерации):
Вернемся к generateSlug:
Второй строчкой мы проверяем slug на уникальность в базе. Напомню, что неуникальный slug нам не нужен, так как мы будем по нему извлекать данные из таблицы.
Первичный ключ у нас теоритически может быть и не id, поэтому находим его функцией primaryKey(). Дальше делаем запрос в таблицу на предмет существования такого slug'а. Если же запись не новая, а мы делаем update (!$this->owner->isNewRecord), то slug уже может существовать и делаем исключение данного id:
Функция возвращает true, если slug уникален, и false, если нет. Дальше:
Если slug уникален, мы его возвращаем, присваиваем атрибуту модели и сохраняем модель в базу. Если же не уникален, то добавим цифровой суффикс testovaya-novost-2
Методом перебора находим первый свободный суффикс и добавляем его к slug'у. Решение подсмотрено в WordPress, но мне не нравится, что для каждого суффикса мы делаем по запросу, соответственно при занятых testovaya-novost, testovaya-novost-2, testovaya-novost-3, testovaya-novost-4, testovaya-novost-5 нам нужно будет сделать 6 запросов для проверки уникальности. Если кто может предложить лучшее решение, буду благодарен.
Итак, slug сгенирован, передан в модель, сохранен в базу, а получившееся поведение мы используем в других моделях.