cakephp3

cakePHP3のブックチュートリアルのリレーションを多対多にするレシピ

概要

cakePHP3のブックマークチュートリアルを終えた段階で、BookmarksテーブルとTagsテーブルは多対1のリレーションとなっています。
これを多対多のリレーションに変更するまでの手順を紹介します。
ブラウザアプリのEdit画面ではチェックボックスを用いて、ブックマークに関係するタグを選択できるようにします。
最後に、ブラウザアプリのView画面でブックマークに関係するタグがすべて表示されるようになれば完成!

導入

※注意事項

自分はプログラムのほぼ初心者です。
この記事に書いてあることは、根底から考えが間違えてる可能性があります。
むしろ誤解があるなら指摘してほしい。

実際のコードを載せていますが、可読性を上げるためにコメントとかコードの一部を予告なく省略することがあります。

この記事は?

cakePHP3のブックマークチュートリアルをパート1まで終えたとき、Bookmarksテーブルとtagsテーブルは多対1のリレーションになってると思います。
これを多対多のリレーションにするために自分が書いたコードや考え方の紹介です。

ちなみに、自分はブックマークチュートリアルのパート1までしかやってないです。
ブログチュートリアルの方では多対多のリレーションを扱うのだろうか?

実際に行った変更

データベースのスキーマを変更

まずは多対多のアソシエーションを作るために中間テーブルを作りましょう。
多対多のリレーションって、BTM(BelongsToMany)って名前があるらしいですね。
それすら知らなかったおかげでドキュメントを探すのに手間取りました。

データベースのスキーマを変更するために、マイグレーションを作成し、それを実行しました。
マイグレーションファイルの作り方や実行方法などはドキュメントを見てください。
up()だけでなく、down()も正常に動くかテストすることを忘れずに!

class CreateNewTable extends AbstractMigration
{
    public function up()
    {
        $table = $this->table('bookmarks_tags', ['id' => false]);
        $table->addColumn('bookmark_id', 'integer', [
            'default' => null,
            'limit' => 11,
            'null' => false,
        ]);
        $table->addColumn('tag_id', 'integer', [
            'default' => null,
            'limit' => 11,
            'null' => false,
        ]);
        $table->create();
    }

    public function down()
    {
        $table = $this->table('bookmarks_tags');
        $table->drop();
    }
}

ちなみに、integerの部分をいつもの癖で間違えてintって書いたせいでエラーが出て結構悩みました。

ブラウザアプリのEdit画面で、タグとの関係を更新できるようにする

タグの情報をViewに渡す

BookmarksContollor.phpを編集します。
自分は最初、BookmarksTable.php内に「中間テーブルをジョインしてTableRegistry::get()を使って、、、」というようなfinderメソッドを作成し、それを用いてControllerでデータベースから情報を検索して、、、というようなことをやってました。
しかし、そういったややこしいことは必要ないみたいです。

若干余談ですが、cakePHP3からはfind()は結果の配列ではなく「クエリーオブジェクト」を返すようになったようですね。
これによって「エンティティ」という概念を理解する必要が出てきましたが、ずいぶんと便利になった……らしいですよ。
自分は以前のバージョンとか使ったことないのでわからないですけど。

ドキュメントのこのページのちょっと下の方にさり気なく書いてあるのですが、以下のようにクエリーオブジェクトを直接set()で渡してしまえば、Table.phpでわざわざデータ持ってくるメソッドを作成しなくてもViewに情報を渡せるようです。
というわけで、以下のコードをBookmarksContollor.phpのedit()に加筆。
これで、タグのクエリーオブジェクトを、tagsListという名前でViewに渡すことができました。

$this->set('tagsList', $this->Bookmarks->Tags->find('list'));

チェックボックスを用いて、ブックマークとタグの関係を指定できるようにする

Edit.ctpを編集します。
ContorollerからtagsListという名前でクエリーオブジェクトが渡されているので、これをチェックボックスで表示するようにします。

ざっと調べた限り、フォームを表示する方法は以下のようなものがあるようです。
1. control()
2. checkbox()
3. multicheckbox()
どれ使えばいいのか迷ってのですが、ここでもドキュメント大先生が道を示してくれました。
上記ページを参考に、edit.ctpのタグを出力する部分のコードを以下のように修正。

echo $this->Form->control('tags._ids', [
                'type' => 'multiCheckbox',
                'label' => false,
                'options' => $tagsList,
            ]);

チェックボックスの入力値をデータベースに反映する。

cakePHP3において、「データベースに保存する」というのは「エンティティをsaveする」という形になる用です
もう少し詳しく説明すると、以下の手順を踏むようです。
1. エンティティのプロパティに、更新したい値を代入
2. エンティティをsave()に渡す
ここでもドキュメントに従い、BookmarksContollor.phpのedit()を修正、してないです。
便利なことに、修正の必要がなかったです。
最終的に、edit()は以下のようになりました。

public function edit($id = null)
    {
        // $idのブックマークエンティティと、それに関係するTagsを取得する。
        $bookmark = $this->Bookmarks->get($id, [
            'contain' => ['Tags']
        ]);

        if ($this->request->is(['patch', 'post', 'put'])) {
            // https://book.cakephp.org/3.0/ja/orm/saving-data.html#id18
            // patchEntity メソッドは、データがエンティティーにコピーされる前に 検証を行う。
            // getData()で得られた値を持つエンティティを作成
            $bookmark = $this->Bookmarks->patchEntity($bookmark, $this->request->getData());

            // $bookmarkエンティティをsave(更新)する。
            if ($this->Bookmarks->save($bookmark)) {
                $this->Flash->success(__('The bookmark has been saved. (^・ω・^)ニャー'));

                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The bookmark could not be saved. Please, try again.'));
        }

        $users = $this->Bookmarks->Users->find('list', ['limit' => 200]);
        $tags = $this->Bookmarks->Tags->find('list', ['limit' => 200]);
        $this->set(compact('bookmark', 'users', 'tags'));
        // https://book.cakephp.org/3.0/ja/views/helpers/form.html#automagic-form-elements
        // 上記ドキュメントを参考にした。
        $this->set('tagsList', $this->Bookmarks->Tags->find('list'));
        $this->set('_serialize', ['bookmark']);
    }

ブラウザアプリのview画面で、任意のブックマークに関係するタグの一覧が見れるようにする。

データベースから任意のブックマークに関連するタグの情報を取得する

BookmarksTable.php内に以下のメソッドを追加します。
今となってはこのメソッドは必要ないんじゃないかという予感がするのですが、finderメソッドの練習だと思っておきます。

public function findRelatedTags(Query $query, array $options)
    {
        // SELECT *
        //  FROM tags
        //  JOIN bookmarks_tags
        //  ON tags.id = bookmarks_tags.tag_id
        //  WHERE bookmard_id = $bookmarkId;
        $tagsList = TableRegistry::get('tags')
        ->find('all')
        ->join([
            'table' => 'bookmarks_tags',
            'conditions' => 'tags.id = bookmarks_tags.tag_id',
        ])
        ->where(['bookmark_id' => $options['bookmarkId']]);

        // debug($tagsList->toArray());
        return $tagsList->toArray();
    }

view.ctpにタグの情報を渡す

上のほうでちょいちょい出てくる"View"とここでいう"view.ctp"は違うものを指してるので混同しないように気を付けてください。
"View"とはMVCのVで、"view.ctp"はそういう名前のテンプレートファイルです。

BookmarksContollor.phpのview()に以下を追記。

$tagsList = $this->Bookmarks->find('relatedTags', [
            'bookmarkId' => $id
        ]);

        $this->set('tagsList', $tagsList);

view.ctpでタグを表示

view.ctpのタグを表示する部分を以下のように修正。

<?php foreach ($tagsList as $tags): ?>
            <tr>
                <td><?= h($tags['id']) ?></td>
                <td><?= h($tags['title']) ?></td>
                <td><?= h($tags['created']) ?></td>
                <td><?= h($tags['modified']) ?></td>
                <td class="actions">
                    <?= $this->Html->link(__('View'), ['controller' => 'Tags', 'action' => 'view', $tags['id']]) ?>
                    <?= $this->Html->link(__('Edit'), ['controller' => 'Tags', 'action' => 'edit', $tags['id']]) ?>
                    <?= $this->Form->postLink(__('Delete'),
                     ['controller' => 'Tags', 'action' => 'delete', $tags['id']],
                      ['confirm' => __('Are you sure you want to delete # {0}?', $tags['id'])]) ?>
                </td>
            </tr>
            <?php endforeach; ?>

以上、修正終わり!

ちゃんと動くか確認

ブラウザアプリを実際に動かして確認します。
- Edit画面ではチェックボックスを用いてタグが選択できるか?
- Edit画面で更新した後、中間テーブルBookmarks_Tagsテーブルにブックマークとタグの関係が正しく入っているか?
- View画面ではブックマークに関係するタグがすべて表示されているか?

まとめ

自分がこの修正を始めてから、実時間で大体3か月かかってます。
そのうち、不要なメソッドを必死でメンテしたりViewのヘルパーを手当たり次第に試したりという時間に2か月半かけてます。
しかし、その2か月半で行った修正のほとんどはこの記事に書いてません。
この記事に書いてあるほとんどの修正内容は、「BTMという概念」「データを保存する公式の方法」といったものがあることに気づいてから実時間で大体2週間で書いたものです。
この修正で一番の教訓は概念を理解する労力を省いてはいけないドキュメントをよく調べるでしょう。

ここまで実際に手を動かして記事を追ってくれた読者の方々は気づいたでしょう。
Edit画面で、ブックマークに関係するタグにはデフォルトでチェックがついてるということに!
いったいどういった原理で達成されているのでしょうか……
デバッグで$tagsListを見たら array('tag1', 'tag2', 'tag3')という簡素な配列でした。
いやホントにどうなってるんだろう……

あと、ここまで書いて時点でBookmarksContollor.phpのadd()とかAdd画面も修正する必要があることに気が付きました。
そのうちやろう。

ここまで読んでいただいてありがとうございます。