---
title: 'Laravel и теги с автодополнением'
url: 'https://graker.ru/news/2018/04/25/laravel-tags-autocomplete'
language: ru
type: post
author: 'Роман Грачев'
published: '2018-04-25T18:00:00+03:00'
updated: '2018-04-25T18:09:03+03:00'
categories:
  - Laravel
  - Программирование
---

# Laravel и теги с автодополнением

Сделать для того или иного материала теги — задача довольно типичная и распространенная. В этой заметке мы рассмотрим, как сделать модель тегируемой, а заодно — как реализовать автодополнение для этих тегов.

Для тегов в Laravel есть несколько пакетов (не с нуля же их писать), по описаниям мне подошел больше всего [Spatie Laravel Tags](https://github.com/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](https://laravel.com/docs/5.6/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](https://sagalbot.github.io/vue-select/).
