Nov 16 2014
Yii2 создание блога (Часть 6 – тэги для постов)
Из этой статье вы узнаете как работать с сущностями отношение у которых Многие-ко-Многим (Many to Many) на примере добавления функционала тэгов к статьям.
Для начала создадим необходимые таблицы для хранения тэгов и связей тэг-пост. Правила генерации опишем в миграции.
<?php use yii\db\Schema; use yii\db\Migration; class m141116_104920_tags extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB'; } $this->createTable('{{%tags}}', [ 'id' => Schema::TYPE_PK, 'title' => Schema::TYPE_STRING . ' NOT NULL', ], $tableOptions); $this->createTable('{{%tag_post}}', [ 'tag_id' => Schema::TYPE_INTEGER, 'post_id' => Schema::TYPE_INTEGER ], $tableOptions); $this->createIndex('FK_tag', '{{%tag_post}}', 'tag_id'); $this->addForeignKey( 'FK_tag_post', '{{%tag_post}}', 'tag_id', '{{%tags}}', 'id', 'SET NULL', 'CASCADE' ); $this->createIndex('FK_post', '{{%tag_post}}', 'post_id'); $this->addForeignKey( 'FK_post_tag', '{{%tag_post}}', 'post_id', '{{%post}}', 'id', 'SET NULL', 'CASCADE' ); } public function down() { $this->dropTable('{{%tags}}'); $this->dropTable('{{%tag_post}}'); } }
После этого сгенерируем модели при помощи Gii-генератора “Тэг”. Параметры для генератора:
Table Name: “tags”
Model Class: “Tags”
Use Table Preffix
Generate Relations
И модель для связи “Тэг-Пост”. Параметры для генератора:
Table Name: “tag_post”
Model Class: “TagPost”
Use Table Preffix
Generate Relations
После этого вносим корректировки в модель постов, добавляем метод, который будет возвращать тэги, закреплённые за постом. Т.к. в Yii2 нет связи многик-ко-многим (many to many), она заменяется на две hasMany.
/** * @return ActiveQuery */ public function getTagPosts() { return $this->hasMany(TagPost::className(), ['post_id' => 'id']); }
Для хранения массива идентификаторов присвоенных посту тэгов добавим в класс защищённую переменную и пару публичных метода на установку и получения идентификаторов тэгов:
/** * Список тэгов, закреплённых за постом. * @var array */ protected $tags = []; /** * Устанавлиает тэги поста. * @param $tagsId */ public function setTags($tagsId) { $this->tags = (array) $tagsId; } /** * Возвращает массив идентификаторов тэгов. */ public function getTags() { return ArrayHelper::getColumn( $this->getTagPost()->all(), 'tag_id' ); }
А так же вносим корректировки в правила валидации rules():
[['publish_date', 'tags'], 'safe'],
Для сохранения выбранных в форме редактирования тэгов добавляем обработчик на событие
public function afterSave($insert, $changedAttributes);
Который предварительно удаляет все закреплённые за постом тэги из связывающей таблицы, формируют новый массив и записывает его в БД:
/** * @inheritdoc */ public function afterSave($insert, $changedAttributes) { TagPost::deleteAll(['post_id' => $this->id]); $values = []; foreach ($this->tags as $id) { $values[] = [$this->id, $id]; } self::getDb()->createCommand() ->batchInsert(TagPost::tableName(), ['post_id', 'tag_id'], $values)->execute(); parent::afterSave($insert, $changedAttributes); }
После того, как модели подготовленны, переходим к генерации GRUD со стороны панели управления. Для этого можно так же воспользоваться инструментом кодо-генерации gii.
Параметры для CRUD Generator
Model Class: common\models\Tags
Controller Class: backend\controllers\TagsController
Зададим права доступа для контроллера backend\controllers\TagsController, что бы доступ к нему для неавторизованных пользователей был закрыт. Для этого добавляем правила в метод behaviors():
'access' => [ 'class' => AccessControl::className(), 'rules' => [ [ 'actions' => ['index', 'view', 'create', 'update', 'delete'], 'allow' => true, 'roles' => ['@'], ], ], ],
В контроллере поста правим action на update и create, добавляем возвращаемый параметр:
'category' => Category::find()->all()
Он понадобится для вывода списка доступных тэгов в формах редактирования и создания поста.
В саму форму поста добавляем элемент отвечающий за вывод списка с мультивыбором:
<?= $form->field($model, 'tags')->listBox( ArrayHelper::map($tags, 'id', 'title'), [ 'multiple' => true ] ) ?>
Правим шаблоны update.php и create.php добавляя в них передачу списка тэгов на рендеринг:
<?= $this->render('_form', [ 'model' => $model, 'category' => $category, 'tags' => $tags, 'authors' => $authors ]) ?>
На этом закончен раздел по работе с тэгами в панели администрирования. Перейдём к выводу списка тэгов на лицевой части сайта.
Для вывода тэгов необходимо отредактировать два шаблона вывод краткого поста post/shortView.php и полного поста post/view.php, добавив в него следующий код:
<div class="tags"> Тэги: <?php foreach($model->getTagPost()->all() as $post) : ?> <?= $post->getTag()->one()->title ?> <?php endforeach; ?> </div>
Более подробный код можно посмотреть в репозитории.
Николай
Nov 24, 2014 @ 20:42:26
Опечатка? “В контроллере поста правим action на update и create, добавляем возвращаемый параметр: ‘category’ => Category::find()->all()”
Надо добавить: ‘tags’ => Tag::find()->all() а не ‘category’ …
Вопрос: Опишите, пожалуйста, подробней как выводить теги к посту на странице вывода всех постов?
Т.е. вывожу я посты:
$post->title
…
// Как вывести все теги к каждому посту?
Николай
Nov 24, 2014 @ 21:13:10
foreach ($posts as $post):
$post->title
endforeach;
Николай
Nov 25, 2014 @ 21:49:55
Разобрался, все было в коде примера. Извините за спам )
Дмитрий
Feb 18, 2015 @ 12:50:08
Добрый день.
Сейчас как раз разбираюсь со связью многие-ко-многим, а именно, проблема в сохранении. Если метод сохранения связи работает, то это уже хорошо для меня. Однако может вы знаете, как можно избавиться от этой строчки?
>TagPost::deleteAll([‘post_id’ => $this->id]);
Возможно, более “правильным” было бы вставлять и удалять связи адресно, а не все сразу, при каждом сохранении.
Есть ли у вас идеи по этому поводу?
И еще один вопрос по поводу вывода listbox. Есть ли способы вместо него использовать чекбоксы, все-таки для пользователя так будет немного удобней, чем зажимать клавишу control, чтобы сделать множественный выбор?
Заранее, спасибо.
Georgy Spack
Feb 19, 2015 @ 23:08:32
Добрый.
Ничего плохого нет в том, что сначала записи удалились и потом добавились. По поводу checkboxList, конечно можно при визуализации отобразить тэги как список CheckBox:
echo $form->field($model, ‘items[]’)->checkboxList([‘a’ => ‘Item A’, ‘b’ => ‘Item B’, ‘c’ => ‘Item C’]);
Дмитрий
Feb 20, 2015 @ 14:39:15
Спасибо
DevNull
Mar 11, 2015 @ 21:52:17
Добрый вечер.
Пытаюсь разобраться со связями, пока не очень, хочу вывести список всех статей для тега, через связи что то не получается.
Vasy
Apr 01, 2015 @ 11:57:03
C:\OpenServer\domains\localhost\Blog-Yii2>php yii migrate
Warning: require(C:\OpenServer\domains\localhost\Blog-Yii2/vendor/autoload.php):
failed to open stream: No such file or directory in C:\OpenServer\domains\local
host\Blog-Yii2\yii on line 18
Fatal error: require(): Failed opening required ‘C:\OpenServer\domains\localhost
\Blog-Yii2/vendor/autoload.php’ (include_path=’.;c:/openserver/modules/php/PHP-5
.6;c:/openserver/modules/php/PHP-5.6/PEAR/pear’) in C:\OpenServer\domains\localh
ost\Blog-Yii2\yii on line 18
Georgy Spack
Apr 01, 2015 @ 12:10:03
У вас отсутствует файл: “/vendor/autoload.php” вероятней всего вы не сделали “composer install” или “composer update”, что бы подтянулись зависимости и сгенерировался файл “autoload.php”.
Александр
Jul 05, 2015 @ 16:35:33
Хотел у вас уточнить а при редактировании где вы берете список ID тегов которые привязаны к этому посту на сколько я понимаю у вас тут это не реализовано.
Georgy Spack
Jul 05, 2015 @ 16:47:02
Тэги у поста получаются методом getTags() модели Post. Для шаблонизации на форму так же передаётся весь список доступных тегов и на выводе шаблонизируется как список чекбоксов.
Александр
Aug 06, 2015 @ 13:06:37
Огромное спасибо за статью. По ней разобрался как можно сохранять объекты при отношениях many-to-many.
Несколько замечаний:
1. Таблица связей тегов и постов:
1.а) Для обоих полей лучше прописать NOT NULL (т.к. в связи обязательны оба значения);
1.б) Во внешних ключах на ON DELETE лучше указать CASCADE (при удалении родительской записи из таблицы тегов или постов удалять также и связь, если устанавливать SET NULL, то в таблице будут оставаться обрывки связей после удаления родительских объектов);
2. При сохранении связей тегов с постом лучше добавить проверку на наличие тегов у поста (я добавил проверку is_array перед foreach, в функции afterSafe поста), тогда можно будет сохранять посты без тегов;
3. В связях моделей можно использовать via или viaTable, тогда в представлении будет проще получить доступ к объектам тегов.
И ещё раз спасибо за статьи – очень помогли.
Giz
Jun 07, 2016 @ 19:50:13
А как сделать пагинацию для списка тегов?
Georgy Spack
Jun 09, 2016 @ 11:01:48
Для создания пагинации можно воспользоваться пагинатором из Framework: http://www.yiiframework.com/doc-2.0/yii-data-pagination.html
Азиз
Nov 03, 2016 @ 18:00:55
$tagsId – откуда эта переменная взялась??? Можете подсказать??
Азиз
Nov 03, 2016 @ 19:20:50
Извини мой косяк! Это локальная переменная, которая играет роль параметра при установке свойства объекта.
Просто у меня долго не получалось повторить твой код… но вроде работает. Пришлось короче перед сохранением принудительно запустить сеттер
ML
Jan 05, 2017 @ 13:28:21
Замечательно! Спасибо огромное за статьи!
Александр
May 15, 2017 @ 19:15:47
Спасибо за статью.
У меня вопрос – как можно получить массив всех постов с определенным тегом?
Так же, как мы получали список тегов, только обращаться к “противоположным” id из связывающей таблицы tag_id на post_id и наоборот?
Или в какую сторону нужно копать?