besnovatyj / yii2-cms-tags
Общие теги для Yii2 CMS: один словарь и одна таблица связей на все контентные модули, форма назначения тегов, админка, страница тега и облако на фронте. Модули подключаются через контракт TaggableProvider.
Package info
github.com/besnovatyj/yii2-cms-tags
Type:yii2-extension
pkg:composer/besnovatyj/yii2-cms-tags
Requires
- php: >=8.4
- ext-mbstring: *
- besnovatyj/yii2-cms-backend-widgets: ^1.0
- besnovatyj/yii2-cms-contracts: ^1.0
- besnovatyj/yii2-cms-forms: ^1.0
- besnovatyj/yii2-cms-helpers: ^1.0
- besnovatyj/yii2-cms-kernel: ^1.0
- besnovatyj/yii2-cms-validators: ^1.0
- yiisoft/yii2: ~2.0.0
- yiisoft/yii2-bootstrap5: ~2.0.0
Requires (Dev)
- roave/security-advisories: dev-latest
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-13 16:08:25 UTC
README
Один словарь тегов и одна таблица связей на все контентные модули. Теги — плоская сквозная тема («о чём»), категории и таксономии — дерево, своё у каждого модуля («где лежит»). Отдельных словарей на модуль нет намеренно: нужен раздельный словарь — это категория.
Что даёт
- таблицы
tags_tagsиtags_assignments(tag_id, entity_type, entity_id); ключ типаentity_type— тот же<модуль>.<сущность>(blog.post), что в контрактах поиска, карты сайта и меню; forms\backend\TagsForm— вложенная форма назначения тегов для композитной формы модуля;forms\backend\TagForm— форма одного тега (валидация, вывод slug);services\TagAssigner— единственная точка записи связей (sync,detachAll);readModels\TagReadRepository— теги записи / пачки записей, id записей по тегу;- админка
/Tags/backend/tag/*— список, правка, удаление, пустые теги, «ничьи» связи, Select2-эндпоинтsearch-endpoint(один на все модули); - фронт
/tag/<slug>(записи всех модулей с фасетами по типу,?type=&page=) и/tags(облако);widgets\TagCloudдля тем; - консоль
Tags/tag/import(перенос legacy-таблиц модуля) иTags/tag/prune.
Создания тега в админке нет: тег рождается при сохранении записи. В админке — только правка опечатки и удаление.
Подключение контентного модуля
1. composer.json
"require": { "besnovatyj/yii2-cms-tags": "^1.0" }
2. Сущность — связи через трейт
use Besnovatyj\Tags\entities\TaggableEntityTrait; class Post extends ActiveRecord { use TaggableEntityTrait; public static function tagType(): string { return 'blog.post'; } } // дальше обычный AR: Post::find()->with('tags'), $post->tags
3. Форма записи — вложенная TagsForm
use Besnovatyj\Tags\forms\backend\TagsForm; class PostForm extends CompositeForm { public function __construct(?Post $post = null, $config = []) { $this->tags = new TagsForm($post?->tags ?? []); // ... } protected function internalForms(): array { return ['meta', 'tags']; } }
Вью формы:
<?= $form->field($model->tags, 'newTagsNames')->widget(Select2Widget::class, [ 'endpoint' => Url::to(['/Tags/backend/tag/search-endpoint'], true), ]) ?>
4. Сервис записи — TagAssigner
public function __construct(private readonly TagAssigner $tags, ...) {} // create / edit — внутри своей транзакции, после сохранения записи: $this->tags->sync(Post::tagType(), $post->id, $form->tags->items); // remove — ДО или после удаления записи, обязательно: $this->tags->detachAll(Post::tagType(), $post->id);
$form->tags->items — это TagForm[], заполненные валидацией: для каждого имени либо форма
существующего тега, либо провалидированная новая. Slug модуль не выводит сам.
5. Класс модуля — TaggableProvider
use Besnovatyj\Contracts\tags\TaggableProvider; use Besnovatyj\Contracts\tags\TagSource; use Besnovatyj\Contracts\tags\TaggedItem; class Module extends CmsModule implements ..., TaggableProvider { public function tagSources(): array { return [new TagSource('blog.post', 'Статьи блога', 'bi bi-newspaper')]; } public function visibleTaggedIds(string $type, array $ids): array { return match ($type) { 'blog.post' => (new PostReadRepository())->visibleIds($ids), // Post::find()->visible()->andWhere(['id' => $ids])->column() default => [], }; } public function taggedItems(string $type, array $ids): iterable { return match ($type) { 'blog.post' => (new PostReadRepository())->taggedItems($ids), // yield new TaggedItem(...) default => [], }; } }
Оба метода обязаны отдавать только то, что анонимный посетитель может открыть: страница тега общая и публичная.
6. Страница тега модуля (по желанию)
Свою blog/tag/<slug> модуль может оставить: TagReadRepository::entityIdsByTag($tag, 'blog.post')
→ свой readModel → свои карточки в стиле раздела. Облако только по своим записям —
TagCloud::widget(['type' => 'blog.post']).
Перенос данных из legacy-таблиц
По разу на модуль, после установки модуля Tags и до удаления старых таблиц:
php yii Tags/tag/import --tags=blog_tags --assignments=blog_tag_asgmt --fk=post_id --type=blog.post --dryRun
php yii Tags/tag/import --tags=blog_tags --assignments=blog_tag_asgmt --fk=post_id --type=blog.post
Слияние по slug: одинаковые slug разных модулей становятся одним тегом. Slug, не проходящий конвенцию (первый символ — буква), перевыводится из имени. Повторный запуск безопасен.
Целостность
Внешнего ключа tags_assignments → запись нет и быть не может (таблицы записей — в разных
модулях). Целостность — конвенцией: удаление записи идёт через сервис модуля с detachAll().
Связи, оставшиеся после deleteAll() в обход сервиса, безвредны: провайдер их не найдёт, на
страницу они не попадут. Связи типов, которых не объявляет ни один установленный модуль,
показываются в админке и удаляются кнопкой или Tags/tag/prune.
Видимость и кэш
Модуль тегов не знает, какие записи видимы, — это отдаёт провайдер. Счётчики облака и /tags
считаются по видимым записям и кэшируются: сброс — при любом изменении связей (тег кэша
tags), по времени — countsTtl (по умолчанию 10 мин; столько облако может не замечать, что
запись скрыли или опубликовали). Страница тега счётчики не кэширует — они считаются на запрос.
Настройки (yii2-cms-config)
perPage — записей на странице тега; cloudLimit — тегов в облаке; countsTtl — кэш
счётчиков, секунд.