【CakePHP3】FormHelperを使用した一対多のチェックボックスで沼にハマったお話

## はじめに
30代事務職未経験からWebエンジニアに転職しました、[しゅとー](https://twitter.com/shutooorm02)と申します。
転職以前はRuby on Railsを中心に学習を進めていましたが、現在はおもにCakePHPを使った開発を行っています。

CakePHPは、考え方もRailsに似ていて、基本的な使い方に沿っていればルーティングをする必要がなかったり、ヘルパーも充実していたり、簡単なフォームであれば、ささっと作ってくれる、とても良いフレームワークです。

しかし一方で、基本から外れて、ちょっと難しいことをしようとすると、とたんに沼にハマってしまうフレームワークでもあります。

本記事では、そんなCakePHPで、FormHelperを使用した一対多のチェックボックスを実装しようとしたときにつまったことをお話します。

:::note info
FormHelper（フォームヘルパー）とは？
フォームのマークアップを生成するビューヘルパーのこと。これを使用することで簡単にフォームを作成することができる。
:::
https://book.cakephp.org/3/ja/views/helpers/form.html

## 環境
- PHP 7.4
- CakePHP 3.10
- MySQL 8

## 実装したかったこと
実装したかったのは、以下のようなチェックボックスのあるフォームです。

項目は複数選択することができ、送信ボタンを押すと、確認画面へ遷移後、タイトルと内容、選択したチェックボックスのデータがそれぞれのテーブルに保存される想定です。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988428/3c98d016-72fd-c5e8-77b3-b523019ae6ce.png">

テーブル構成については、以下のとおりです。
### テーブル構成
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988428/999c9824-6f89-765a-0319-8f5c0d374763.png">

### 実際のコード：失敗編
当初作成したフォームは、こんな感じです。

```php:PostsController.php
public function index()
{
    $animals = Configure::read('animals');

    $post = $this->Posts->newEntity();

    if ($this->request->is('post')) {
        $post = $this->Posts->patchEntity($post, $this->request->getData(), [
            'associated' => ['Animals']
        ]);

        if ($post->hasErrors()) {
            $this->Flash->error('入力内容に誤りがあります');
        } else {
            // エラーがない場合は、確認画面へ遷移
            $this->request->session()->write('postData', $this->request->getData());
            return $this->redirect(['action' => 'confirm']);
        }
    }

    // すでに入力データがある場合は、各フォームに値を反映
    if ($this->request->session()->check('postData')) {
        $postData = $this->request->session()->read('postData');
        $this->request = $this->request->withParsedBody($postData);
        $this->request->getSession()->delete('postData');
    }

    $this->set(compact('post', 'animals'));
}
```
```PHP:index.php
<body>
    <div class="contact">
        <h1 class="contact-title">フォーム</h1>
        <?= $this->Form->create($post); ?>

        <?= $this->Form->control('title', [
            'label' => 'タイトル',
            'required' => false,
        ]); ?>

        <?= $this->Form->control('description', [
            'label' => '内容',
            'required' => false,
        ]); ?>

        <label>好きな動物</label>
        <?= $this->Form->select('animals[]', $animals, [
            'multiple' => 'checkbox',
            'value' => $postData['animals'] ?? [],
        ]); ?>

        <?php if ($this->Form->isFieldError('animals')) : ?>
            <?= $this->Form->error('animals'); ?>
        <?php endif; ?>

        <?= $this->Form->submit('確認する', [
            'name' => 'confirm'
        ]); ?>

        <?= $this->Form->end() ?>
    </div>
</body>
```
この状態で保存を実行すると、親テーブルであるPostsTableには無事データが保存されました。

しかし、なぜか子テーブルであるAnimalsTableにはデータが保存されません。

ネットでいろいろ検索してみましたが、公式ドキュメントにもそれらしき記述もなく…。手詰まりになったわたしは、伝家の宝刀「先輩に聞く」を実行しました。

すると、

「**保存する際のデータの形式、正しいかたちになってる？**」

というご助言をいただきます。

## 一対多のデータを保存する際の正しい形式
先輩いわく、一対多のテーブルにデータを保存する際は、以下のような形式でデータを送る必要があるとのことでした。
```
'Posts' => [
    'Animals' => [
        0 => ['animal' => 1],
        1 => ['animal' => 2]
    ]
];
```
なぜ ↑ の形式でないといけないのかというと、

「**Animalsテーブルの一つのカラムには、ひとつのデータしか入らないから**」

だそうです。

確認したところ、私が実装したフォームのデータ形式は以下のようになっていました。
ご指摘のとおり、データの形式が異なっていたためにデータが正しく保存されなかったようです。
```PHP:index.php
<label>好きな動物</label>
<?= $this->Form->select('animals[]', $animals, [
    'multiple' => 'checkbox',
    'value' => $postData['animals'] ?? [],
]); ?>
```
<img width="60%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988428/7284784a-4c55-2a7b-a915-60813e5f8af3.png">


では、FormHelperを使用して、一対多のチェックボックスを作成し、かつ、そのデータをデータベースに正しく保存するにはどのようなコードを書けばよいのでしょう？

## 結論
FormHelerを使った書き方をいろいろ検証した結果、

「**CakePHP3のフォームヘルパー単体では、一対多のテーブルにデータの保存ができない**」

ということが分かりました。

CakePHPにも複数選択用のFormHelperがありますが、どれも期待通りのデータ形式にはなりませんでした。

<details><summary>実際に試した検証の軌跡がこちら</summary>

#### FormHelper単体で頑張る

【検証1回目】
```php
<label>好きな動物</label>
<?= $this->Form->input('animals._ids', [
    'label' => false,
    'type' => 'select',
    'multiple' => 'checkbox',
    'options' => $animals,
    'hiddenField' => false,
])?>
```
<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988428/f22e6cbc-70e7-57b7-6f45-a0bbe06ccf69.png">

【検証２回目】
```php
<label>好きな動物</label>
<?= $this->Form->input('animals.0.animal', [
    'label' => false,
    'type' => 'select',
    'multiple' => 'checkbox',
    'options' => $animals,
    'hiddenField' => false,
])?>
```
<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988428/868ce8c6-17ec-3fde-c508-6c646587dd08.png">

#### `foreach` で回してみる

【検証3回目】

一見うまくいっているように見えるが、複数選択できず、一番最後のチェックボックスのみがデータに残ってしまう。
（`multiple => 'checkbox'`も効果なし）
```php
<label>好きな動物</label>
<?php foreach($animals as $num => $animal) : ?>
    <?= $this->Form->checkbox('animals.0.animal', [
        'label' => false,
         'options' => $animal,
         'required' => false,
         'value' => $num,
         'hiddenField' => false,
    ])?>
    <?= $animal ?>
<?php endforeach; ?>
```
<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988428/e4bdb79c-a80a-9a0f-5c2e-8836128b42e3.png">

【検証4回目】
```php
<label>好きな動物</label>
<?php foreach($animals as $num => $animal) : ?>
    <?= $this->Form->checkbox('animals.0.animal[]', [
        'label' => false,
        'options' => $animal,
        'required' => false,
        'value' => $num,
        'hiddenField' => false,
    ])?>
    <?= $animal ?>
<?php endforeach; ?>
```
<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988428/a7acd675-6ede-a5e8-7d3b-6e08fa49ce31.png">
</details>

### 実際のコード：成功編
最終的に、以下のコードでデータの保存に成功しました。

```php:index.php
<label>好きな動物</label>
<?php foreach($animals as $num => $animal) : ?>
<label>
    <?= $this->Form->input("animals.{$num}.animal", [
        'type' => 'checkbox',
        'label' => $animal,
        'required' => false,
        'value' => $num,
        'hiddenField' => false,
    ]); ?>
    <?= $this->Form->unlockField("animals.{$num}.animal"); ?>
</label>
<?php endforeach; ?>
```
<img width="60%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988428/2d469d76-397d-d3f2-ec5f-49f0a00fb4c7.png">

### （余談）一対多のチェックボックスのデータを表示するには？
「入力画面から受け取ったデータを保存前に、確認画面で表示したい！」という場合は、さらにひと手間必要で、View側でかたちを整えてあげる必要があります…（手がかかる）

```php:confirm.php
<tr>
    <th><label>好きな動物</label></th>
    <td>
        <?php if (isset($postData['animals'])) : ?>
            <?php $selectAnimals = []; ?>
            <?php foreach ($postData['animals'] as $num => $animal) : ?>
                <?php $selectAnimals[] = $animals[h($num)]; ?>
            <?php endforeach; ?>
            <?= implode(', ', $selectAnimals) ; ?>
        <?php endif; ?>
    </td>
</tr>
```
※ `$postData` には、`input` アクションから受け取ったデータが入っています。

## 終わりに
簡単なフォームであればささっと作ってくれる、しかし、時にはこちらできれいにデータの形式を整えて食わせてやる必要がある――、それがCakePHPです。

しかし今思い返せば、一番問題だったのは、私自身が、

「**フレームワーク側からどんなデータが流れてきていて、どういう過程でデータベースに保存されているのか**」

を理解していなかったことだと思います。

フレームワークの動きをしっかり理解することの重要性を再確認するとともに、この記事を通じて、CakePHPの一対多チェックボックス沼にハマる方がひとりでも少なくなれば幸いです。

最後まで読んでいただき、ありがとうございました！