Сделать для того или иного материала теги — задача довольно типичная и распространенная. В этой заметке мы рассмотрим, как сделать модель тегируемой, а заодно — как реализовать автодополнение для этих тегов.
Для тегов в Laravel есть несколько пакетов (не с нуля же их писать), по описаниям мне подошел больше всего Spatie Laravel Tags. В нём работа с тегами реализована достаточно прозрачно и просто, плюс из коробки есть локализация для наименований тегов и slug-и для генерации страниц тегов.
Установить пакет просто:
composer require spatie/laravel-tags
php artisan vendor:publish --provider="Spatie\Tags\TagsServiceProvider" --tag="migrations"
php artisan migrate
Чтобы можно было тегировать свою модель, достаточно использовать трейт HasTags
:
class Order extends Model
{
use HasTags;
// ...
}
Теперь если мы где-нибудь (скажем, в контроллере) напишем:
$order->syncTags(['Тег 1', 'Тег 2', 'Тег 3']);
— теги добавятся к модели. При этом библиотека сама создаст эти теги, проследит, чтобы не создавались дубли, сама приаттачит, сама удалит то, что нужно и т.д. Другие примеры использования есть в описании пакета.
Пока всё примитивно — достаточно передать массив тегов. Но этот массив еще нужно сгенерировать на стороне фронт-энда, для чего желательно реализовать автодополнение по уже существующим тегам, чтоб проще было вводить и выбирать.
Для этого нужно обустроить поиск по началу названия среди существующих тегов. В Laravel есть такой довольно удобный в применении пакет — Laravel Scout, неплохо годится как раз для индексируемого поиска. Поставить его тоже просто:
composer require laravel/scout
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
А чтобы применять (то есть индексировать модели и искать по ним) — достаточно использовать трейт Searchable
. Но модель Tag
, используемая для создания тегов — находится в вендорном пакете spatie-laravel-tags и изменять её не хочется. Не проблема — пакет позволяет использовать для тегов собственные модели. Документация, правда, описывает этот момент недостаточно подробно, но можно додумать самому.
Для начала нужно унаследовать модель Tag
. У меня теги — это направления (города и страны), поэтому я сделал модель DirectionTag
. В ней уже можно использовать трейт Searchable
:
class DirectionTag extends Tag
{
use Searchable;
protected $table = 'tags';
}
Обратите внимание, что для модели используется та же таблица tags
, что и для родительской модели.
Также нужно указать, что наша модель Order
должна использовать именно теги данного типа, переопределив соответствующий метод трейта HasTags
:
class Order extends Model
{
use HasTags;
public static function getTagClassName(): string
{
return DirectionTag::class;
}
}
В документации говорится, что этого достаточно. На деле, то ли что-то к версии 5.6 Ларавела поменялось, то ли просто не дописали, но надо еще переопределить метод tags()
того же трейта:
public function tags(): MorphToMany
{
return $this
->morphToMany(self::getTagClassName(), 'taggable', 'taggables', null, 'tag_id')
->orderBy('order_column');
}
Если не указать ключ tag_id
, отношение Morph to Many будет пытаться найти поле direction_tag_id
, опираясь на имя модели тега.
Теперь мы можем искать по тегам с помощью DirectionTag::search()
и возвращать данные фронт-энду.
Нужно учитывать, что чтобы сделать теги локализуемыми — разработчики хранят их имена в столбце name
типа JSON
. То есть в столбце хранится строка вида { "en": "Russia", "ru": "Россия" }
, а нужная версия автоматически определяется по установленной локали. Поэтому возвращать во фронт-энд результаты поиска лучше, отобрав заранее имена в текущей локали:
$directions = DirectionTag::search($query)->get();
return response()->json($directions->pluck('name'), 200);
Кроме того, если вы (как я в данный момент) используете драйвер MySQL для Scout — то имейте в виду, что MySQL сейчас не поддерживает FULLTEXT-поиск по столбцам типа JSON. Поэтому имеет смысл добавить в таблицу tags
отдельный строковый столбец, содержащий индексируемое имя тега. Если локализация не нужна, то можно в код модели DirectionTag
просто добавить обработчик события:
protected static function boot() {
parent::boot();
static::saving(function (DirectionTag $tag) {
$tag->search_text = $tag->name;
});
}
Здесь мы просто сохраняем в строковом поле значение имени тега в текущей локали всякий раз, когда модель создается или обновляется. Сохраненное значение — не забудьте добавить его в миграцию таблицы tags
— без проблем проиндексируется Скаутом.
Наконец, чтобы не строить полнотекстового индекса по всяким лишним полям (например, по типу тега) — можно переопределить метод toSearchableArray()
в модели тега и возвращать только значение, которое действительно нужно индексировать:
public function toSearchableArray() {
return [
'search_text' => $this->search_text,
];
}
И это всё, останется только реализовать ввод/загрузку тегов на стороне фронт-энда. Для чего можно использовать, например, годный компонент Vue Select2.