CakePHP3で複数選択チェックボックスを実装する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94360/45d85f5f-f8ed-cd77-c5c6-7177d3718943.png)

### 1. 公式ドキュメントを読んだけどよく分からなかった！

https://book.cakephp.org/3/ja/orm/saving-data.html#hasmany

CakePHPで複数選択可能なチェックボックスを実装しようとして苦戦したので、記事としてノウハウをまとめておきます。

> **HasMany データの変換**<br>hasMany データを変換するときは、 onlyIds オプションを使って、 新しいエンティティーの作成を行わなくすることができます。有効にすると、このオプションは belongsToMany の変換を _ids キーの使用のみに制限して、他のすべてのデータを無視します。

CakePHPの公式ドキュメントには上記のように書いてありますが、もう少し具体的な說明がほしいところ。onlyIdsオプションとは？どこにどう書く？チェックボックスの実装自体についても `'type' => 'select'` など、違和感を感じる記述があります。

[先人のブログ記事](https://www.google.com/search?q=cakephp+onlyIds&tbs=lr:lang_1ja&lr=lang_ja)なども探ってみたけどほとんど情報なし。仕方がないので本体ソースを見ながら公式ドキュメントと読み合わせて答えを出しました。

以下に說明を続けますが、基本的には公式ドキュメントに書いてあるとおりで、分かりにくいところを分かりやすくまとめたという感じです。

### 2. 前提条件

**やってみること: ユーザープロフィール画面に趣味の項目を作る**

複数選択可能な「趣味」項目をユーザープロフィール編集画面に追加し、データを正しく保存・表示する方法を說明します。

- **必要な知識**: CakePHPの基本的な知識
- **システム環境**: CakePHP 3.x (3.4で確認しました)

### 3. データベースの設定
#### テーブルの構成
ユーザープロフィールの趣味を管理するために、以下の3つのテーブルを作成します。
- `users` テーブル
- `hobbies` テーブル
- `user_hobbies` テーブル（中間テーブル）

#### `users` テーブル
```sql
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL
);
```

#### `hobbies` テーブル
```sql
CREATE TABLE hobbies (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL
);
```

#### `user_hobbies` テーブル
```sql
CREATE TABLE user_hobbies (
    user_id INT,
    hobby_id INT,
    PRIMARY KEY (user_id, hobby_id),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (hobby_id) REFERENCES hobbies(id) ON DELETE CASCADE
);
```

#### サンプルデータ
```sql
INSERT INTO hobbies (name) VALUES ('読書'), ('映画鑑賞'), ('旅行'), ('料理'), ('スポーツ');
```

### 4. モデルの設定
次に、モデルを設定します。

#### UsersTable.php
```php
namespace App\Model\Table;

use Cake\ORM\Table;

class UsersTable extends Table
{
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('users');
        $this->setPrimaryKey('id');
        $this->setDisplayField('name');

        $this->hasMany('UserHobbies', [
            'foreignKey' => 'user_id'
        ]);
        
        $this->belongsToMany('Hobbies', [
            'through' => 'UserHobbies',
            'foreignKey' => 'user_id',
            'targetForeignKey' => 'hobby_id'
        ]);
    }
}
```

#### HobbiesTable.php
```php
namespace App\Model\Table;

use Cake\ORM\Table;

class HobbiesTable extends Table
{
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('hobbies');
        $this->setPrimaryKey('id');
        $this->setDisplayField('name');

        $this->belongsToMany('Users', [
            'through' => 'UserHobbies',
            'foreignKey' => 'hobby_id',
            'targetForeignKey' => 'user_id'
        ]);
    }
}
```

#### UserHobbiesTable.php
```php
namespace App\Model\Table;

use Cake\ORM\Table;

class UserHobbiesTable extends Table
{
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('user_hobbies');
        $this->setPrimaryKey(['user_id', 'hobby_id']);

        $this->belongsTo('Users', [
            'foreignKey' => 'user_id'
        ]);
        $this->belongsTo('Hobbies', [
            'foreignKey' => 'hobby_id'
        ]);
    }
}
```

### 5. コントローラの設定
次に、`UsersController`の`edit`メソッドに画面表示と保存処理をまとめます。

#### UsersController.php
```php
namespace App\Controller;

use App\Controller\AppController;

class UsersController extends AppController
{
    public function edit($id = null)
    {
        $user = $this->Users->get($id, [
            'contain' => ['Hobbies']
        ]);
        $hobbies = $this->Users->Hobbies->find('list');
        
        if ($this->request->is(['patch', 'post', 'put'])) {
            $data = $this->request->getData();

            $user = $this->Users->patchEntity($user, $data, [
                'associated' => ['Hobbies' => ['onlyIds' => true]]
            ]);

            if ($this->Users->save($user)) {
                $this->Flash->success('保存しました！');
                return $this->redirect(['action' => 'view', $id]);
            }

            $this->Flash->error('保存できませんでした。もう一度お試しください。');
        }

        $this->set(compact('user', 'hobbies'));
    }
}
```
:::note info
**ポイント**
**onlyIdsオプションの使用**
コントローラの `patchEntity` メソッド内で `'associated' => ['Hobbies' => ['onlyIds' => true]]` を指定することで、関連付けられたIDのリストのみを使用し、他のデータを無視します。これにより、処理の過程においてエンティティの構築を回避できます。
:::

**onlyIdsオプションを指定しなかった時に渡される「他のデータ」とは？**

ここで言う「他のデータ」とは、関連付けられたエンティティの詳細な情報（例：`name` や `created` などのフィールド値）を指します。例えば、以下のようなデータが渡された場合：

```php
$data = [
    'name' => 'John Doe',
    'email' => 'john@example.com',
    'hobbies' => [
        ['id' => 1, 'name' => '読書'],
        ['id' => 2, 'name' => '映画鑑賞']
    ]
];
```

このデータには、`id` に加えて `name` などの詳細な情報が含まれています。`onlyIds` オプションがない場合、CakePHPはこれらの詳細な情報を元にエンティティを処理します。すなわち、既存のエンティティを更新したり、新しいエンティティを作成したりします。具体的には、CakePHPはこのような形式（`id`キーを含む配列）を受け取るとエンティティを新規作成する可能性があります。

**onlyIds オプションを使用した場合：**

```php
$data = [
    'name' => 'John Doe',
    'email' => 'john@example.com',
    'hobbies' => [
        '_ids' => [1, 2]
    ]
];

$user = $this->Users->patchEntity($user, $data, [
    'associated' => ['Hobbies' => ['onlyIds' => true]]
]);
```

`onlyIds` オプションを使用すると、`hobbies` に渡されるデータはIDのリスト（例：`_ids`）のみとなり、他のフィールド（`name` など）は無視されます。これにより、エンティティ構築処理はスキップされ、既存の関連付けのみが更新されます。

**onlyIds オプションの要点まとめ：**

- **IDのリストのみを使用**: `onlyIds` オプションを使用すると、IDのリストのみが処理され、詳細なデータは無視されます。
- **新しいエンティティの作成防止**: 関連付けに必要なIDだけが処理されるため、新しいエンティティの作成が行われません。
- **処理の簡略化**: データがシンプルになり、処理が効率化されます。

これにより、複雑なデータを渡さずに関連付けを管理でき、システムのパフォーマンスとメンテナンス性が向上します。ただし、patchEntityメソッド実行後に特殊な処理を行うためにエンティティが必要な場合は、`onlyIds` オプションを指定しません。

**ちなみに**

- 挿入：新しいIDが存在する場合、そのデータを挿入
- 更新：既存のIDが存在し、そのデータに変更があった場合、そのデータを更新
- 削除：元のデータに存在し、新しいデータに存在しないIDがある場合、そのデータを削除

チェックボックスの数だけ上記のようなsave処理を行います。いわゆるupsertです。チェックボックス実装はこういう特性があるため、自前で実装しようとすると手間がかかります。

### 6. ビュー(ctp)の作成
最後に、ユーザープロフィール編集画面に複数選択可能なチェックボックスを表示します。

#### edit.ctp
```php
echo $this->Form->create($user, ['url' => ['action' => 'edit', $user->id]]);
echo $this->Form->control('name');
echo $this->Form->control('email');
echo $this->Form->control('hobbies._ids', [
    'type' => 'select',
    'multiple' => 'checkbox',
    'options' => $hobbies,
    'value' => collection($user->hobbies)->extract('id')->toList()
]);
echo $this->Form->button('送信');
echo $this->Form->end();
```
:::note info
**ポイント**

**._ids の使用**
hobbies`._ids` の形式でフィールドを指定することで、中間テーブルによる関連付けを行います。

**複数選択の設定**
`'type' => 'select'` と `'multiple' => 'checkbox'` を指定することで、複数選択可能なチェックボックスをレンダリングします。

**初期選択値の設定**
`'value' => collection($user->hobbies)->extract('id')->toList()` と記述し、該当ユーザーの趣味IDのリストを設定します。
:::


**実装完了です！**

### 感想

フレームワークを使わず生PHPで同様の機能を実装した人なら分かると思いますが、チェックを外した時にデータを削除する考慮とか、他のフォーム要素と比べると意外と難しくて。フレームワークを使えば難なく実装できます。あとはドキュメントが分かりやすいと助かります。チェックボックスに関しては記述が各所に分散していて読みづらかったです・・