Yii2 でタグ機能を作る

投稿のカテゴリー分け (この記事だと Yii と PHP) などでよく使われる機能ですが、いちから作るとわりと面倒なので今回は以下の 2 つの拡張を使ってやってみます。

* [yii2-taggable](https://github.com/creocoder/yii2-taggable)
* [yii2-selectize-widget](https://github.com/2amigos/yii2-selectize-widget)

## テーブルの準備

post, tag, post_tag テーブルの作成:

```sql
CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `body` text COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE `tag` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `frequency` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE `post_tag` (
  `post_id` int(11) NOT NULL,
  `tag_id` int(11) NOT NULL,
  PRIMARY KEY (`post_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
```

## 拡張のインストール

```
composer require creocoder/yii2-taggable 2amigos/yii2-selectize-widget
```

## 設定

post, tag のモデルを Gii で作成。Table Name は * で、Generate Relations と Generate ActiveQuery をチェックで生成 (必要のないものはチェックを外しておく) 。あと CRUD Generator で PostController と TagController 用を作っておく。

モデル:

```php:Post.php
namespace app\models;

use Yii;
use creocoder\taggable\TaggableBehavior;

class Post extends \yii\db\ActiveRecord
{
    public function rules()
    {
        return [
            // ...
            [['tagValues'], 'safe'],
        ];
    }

    public function getTags()
    {
        return $this->hasMany(Tag::className(), ['id' => 'tag_id'])
            ->viaTable('post_tag', ['post_id' => 'id']);
    }

    public static function find()
    {
        return new PostQuery(get_called_class());
    }

    public function behaviors()
    {
        return [
            TaggableBehavior::className(),
        ];
    }

    public function transactions()
    {
        return [
            self::SCENARIO_DEFAULT => self::OP_ALL,
        ];
    }
}
```

```php:PostQuery.php
namespace app\models;

use creocoder\taggable\TaggableQueryBehavior;

class PostQuery extends \yii\db\ActiveQuery
{
    public function behaviors()
    {
        return [
            TaggableQueryBehavior::className(),
        ];
    }
}
```

中間テーブル、トランザクション、yii2-taggable ビヘイビアの定義と、フォームでタグの作成などをするので SafeValidator で tagValues の値を許可しておく。

ビュー:

```php:_form.php
<?php $form = ActiveForm::begin(); ?>
    <?= $form->field($model, 'body')->textarea(['rows' => 6]) ?>
    <?= $form->field($model, 'tagValues')->textarea(['rows' => 6]) ?>
    <div class="form-group">
        <?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', [
            'class' => 'btn btn-primary',
        ]) ?>
    </div>
<?php ActiveForm::end(); ?>
```

とりあえずテキストエリアでやってみます。body カラムに `body1`、tagValues に `tag1, tag2, tag3` を入力したとすると以下のようなデータが挿入されることになります:

```
mysql> select * from post; select * from tag; select * from post_tag;
+----+-------+
| id | body  |
+----+-------+
|  1 | body1 |
+----+-------+
1 row in set (0.00 sec)

+----+------+-----------+
| id | name | frequency |
+----+------+-----------+
|  1 | tag1 |         1 |
|  2 | tag2 |         1 |
|  3 | tag3 |         1 |
+----+------+-----------+
3 rows in set (0.00 sec)

+---------+--------+
| post_id | tag_id |
+---------+--------+
|       1 |      1 |
|       1 |      2 |
|       1 |      3 |
+---------+--------+
```

GridView でタグを表示させたい場合は以下のようにします:

```php:index.php
<?= GridView::widget([
    'dataProvider' => $dataProvider,
    'filterModel' => $searchModel,
    'columns' => [
        ['class' => 'yii\grid\SerialColumn'],
        'id',
        'body:ntext',
        'tagValues', // 追加
        ['class' => 'yii\grid\ActionColumn'],
    ],
]); ?>
```

おそらく `tag1, tag2, tag3` と表示されます。タグの値はデフォルトでは文字列で表現されます。これは表示させる場合には便利ですが、フォームで作成、更新する場合には手打ちでタグを書かないといけないのでかなり不便です。補完も出ないし。

ということでとりあえずテキストエリアはやめて、チェックボックスで表現してみます。

モデル:
TaggableBehavior を名前付きにしておく。

```php:Post.php
public function behaviors()
{
    return [
        'taggable' => [
            'class' => TaggableBehavior::className(),
        ],
    ];
}
```

コントローラ:
例えば、更新アクションの場合。

```php:PostController.php
public function actionUpdate($id)
{
    $model = $this->findModel($id);
    $model->getBehavior('taggable')->tagValuesAsArray = true; // タグの値を配列で表現

    if ($model->load(Yii::$app->request->post()) && $model->save()) {
        return $this->redirect(['view', 'id' => $model->id]);
    } else {
        return $this->render('update', [
            'model' => $model,
        ]);
    }
}
```

ビュー:

```php:_form.php
<?php
$list = \yii\helpers\ArrayHelper::map(
    \app\models\Tag::find()->orderBy(['name' => SORT_ASC])->all(),
    'name',
    'name'
);
?>
<?php $form = ActiveForm::begin(); ?>
    <?= $form->field($model, 'body')->textarea(['rows' => 6]) ?>
    <?= $form->field($model, 'tagValues')->checkBoxList($list) ?>
    <div class="form-group">
        <?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', [
            'class' => 'btn btn-primary',
        ]) ?>
    </div>
<?php ActiveForm::end(); ?>
```

これでチェックボックスで追加、更新などができるかと思います。ただこのままだと動的にタグを追加することはできません。そこでもう一つの拡張の yii2-selectize-widget を使います。[Select2](https://select2.github.io/) みたいなものを yii2 のウィジェットで扱えるようにしたものです。

とりあえず更新アクションの `$model->getBehavior('taggable')->tagValuesAsArray = true;` はいらないので削除。次に TagController にタグ名をすべて json で返すアクションを作成:

```php:TagController.php
public function actionList()
{
    $items = (new \yii\db\Query())
        ->from('{{%tag}}')
        ->select(['name'])
        ->orderBy(['name' => SORT_ASC])
        ->all();

    Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
    return $items;
}
```

ビュー:

```php:_form.php
<?php
// ...
use dosamigos\selectize\SelectizeTextInput;
?>
<?php $form = ActiveForm::begin(); ?>
    <?= $form->field($model, 'body')->textarea(['rows' => 6]) ?>
    <?= $form->field($model, 'tagValues')->widget(SelectizeTextInput::className(), [
        'loadUrl' => ['tag/list'],
        'options' => ['class' => 'form-control'],
        'clientOptions' => [
            'delimiter' => ', ',
            'persist' => false,
            'plugins' => ['remove_button'],
            'valueField' => 'name',
            'labelField' => 'name',
            'searchField' => ['name'],
            'create' => true,
        ],
    ]) ?>
    <div class="form-group">
    <?= Html::submitButton($model->isNewRecord ? 'Create' : 'Update', [
        'class' => 'btn btn-primary',
    ]) ?>
    </div>
<?php ActiveForm::end(); ?>
```

これでタグの選択部分で補完が効いて、かつ新しいタグを動的に追加できるようにもなるかと思います。yii2-taggable だけでも十分便利ですが yii2-selectize-widget と合わせることによってフォームでの操作も直感的で使いやすくなるんじゃないかなぁと。見た目は以下:

![example.png](https://qiita-image-store.s3.amazonaws.com/0/11524/1e9812ef-eda3-95fb-1504-fbbff1bc7b00.png)