データベース
itemsというテーブルを作ります。
listカラムにjsonで保存するようにします。
config/Migrations/xxxxxx_CreateItems.php
<?php declare(strict_types=1); use Migrations\AbstractMigration; class CreateItems extends AbstractMigration { public function change() { $table = $this->table('items'); $table ->addColumn('title', 'string') ->addColumn('list', 'text') ->addColumn('created', 'datetime') ->addColumn('modified', 'datetime') ->create(); } }
モデル
モデルはbakeしたものをそのまま使用します。
バリデーションだけ複数入力用にカスタマイズしましょう。
src/Model/Table/ItemsTable.php
class ItemsTable extends Table { //... public function validationDefault(Validator $validator): Validator { $validator ->integer('id') ->allowEmptyString('id', null, 'create'); $validator ->scalar('title') ->requirePresence('title', 'create') ->minLength('title', 2, '2文字以上で入力してください。') ->maxLength('title', 150, '150文字以下で入力してください。') ->notEmptyString('title', '必ず入力してください。'); $listValid = new Validator(); $listValid ->scalar('name') ->minLength('name', 2, '2文字以上で入力してください。') ->maxLength('name', 20, '20文字以下で入力してください。') ->notEmptyString('name', '必ず入力してください。') ->scalar('age') ->maxLength('age', 2, '2桁以下で入力してください。') ->numeric('age', '整数で入力してください') ->notEmptyString('age', '必ず入力してください。'); $validator->addNestedMany('list', $listValid); return $validator; } }
list
カラムにname
とage
という項目を複数バリデーションします。
この場合はaddNestedMany
を使用して追加します。
コントローラー
コントローラーです。
jsonで保存する為にlistにはjson_encodeで変換するというのが重要なところでありますが、その他にもいろいろと処理が必要になります。
もっとスマートな方法があると思いますが、とりあえずこんな感じにおちつきました。
addもeditもちょっと違うだけでほぼ同じです。
src/Controller/ItemsController.php
<?php declare(strict_types=1); namespace App\Controller; use App\Model\Entity\Item; class ItemsController extends AppController { //... public function add() { $item = $this->Items->newEmptyEntity(); if ($this->request->is('post')) { $requestData = $this->request->getData(); $item = $this->Items->patchEntity($item, $requestData); if ($item->hasErrors()) { // setするとエラー消えるので先に保存しておく $errors = $item->getErrors(); $item->set('list', $item->getInvalidField('list')) ->setErrors($errors); $this->Flash->error('データの保存に失敗しました。'); } else { // 配列のindexを連番後、jsonに変換 $item->set('list', json_encode(array_values($requestData['list']))); if ($this->Items->save($item)) { $this->Flash->success('データの保存に成功しました。'); return $this->redirect(['action' => 'index']); } $this->Flash->error('データの保存に失敗しました。'); } } $this->set(compact('item')); } public function edit($id = null) { $item = $this->Items->get($id); if ($this->request->is(['patch', 'post', 'put'])) { $requestData = $this->request->getData(); $item = $this->Items->patchEntity($item, $requestData); if ($item->hasErrors()) { $errors = $item->getErrors(); $item->set('list', $item->getInvalidField('list')) ->setErrors($errors); $this->Flash->error('データの更新に失敗しました。'); } else { $item->set('list', json_encode(array_values($requestData['list']))); if ($this->Items->save($item)) { $this->Flash->success('データの更新に成功しました。'); return $this->redirect(['action' => 'edit', $id]); } $this->Flash->error('データの更新に失敗しました。'); } } else { $item->set('list', json_decode($item->list, true)); } $this->set(compact('item')); } // ... }
ビュー
ビューもaddとeditはほとんど同じです。
jsonごとJavaScriptに渡した方がスマートですが、今回はJavaScriptの作りは最小限にしたったので、PHPで展開してます。
templates/Items/add.php
<div class="row"> <div class="column"> <div class="content"> <?= $this->Form->create($item) ?> <fieldset> <legend>登録</legend> <?= $this->Form->control('title', [ 'label' => 'タイトル' ]) ?> <div class="multifield"> <table class="item-wrap"> <tr data-index="0"> <th>お名前</th> <td><?= $this->Form->control('list.0.name', [ 'class' => 'input-name', 'label' => false ]); ?></td> <th>年齢</th> <td><?= $this->Form->control('list.0.age', [ 'class' => 'input-name', 'label' => false ]); ?></td> <td><div class="button remove-btn">削除</div></td> </tr> </table> <div class="button add-btn">追加</div> </div> <?= $this->Form->button('保存') ?> </fieldset> <?= $this->Form->end() ?> </div> </div> </div>
templates/Items/edit.php
<div class="row"> <div class="column"> <div class="content"> <?= $this->Form->create($item) ?> <fieldset> <legend>編集</legend> <?= $this->Form->control('title', [ 'label' => 'タイトル' ]) ?> <div class="multifield"> <table class="item-wrap"> <?php foreach($item->list as $key => $value): ?> <tr data-index="<?= $key ?>"> <th>お名前</th> <td><?= $this->Form->control('list.'.$key.'.name', [ 'class' => 'input-name', 'label' => false ]); ?></td> <th>年齢</th> <td><?= $this->Form->control('list.'.$key.'.age', [ 'class' => 'input-name', 'label' => false ]); ?></td> <td><div class="button remove-btn">削除</div></td> </tr> <?php endforeach; ?> </table> <div class="button add-btn">追加</div> </div> <?= $this->Form->button('保存') ?> </fieldset> <?= $this->Form->end() ?> </div> </div> </div>
JavaScript
レイアウトでjQueryと新しく作るJSを読み込みます。
templates/layout/default.php
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <?= $this->Html->script('script.js') ?>
最低数とか最大数とかできてないですが、とりあえず動くレベルです。
webroot/js/script.js
(function($) { $(function () { const $wrap = $('.multifield'); const $itemWrap = $('.item-wrap', $wrap); let index = $('tr', $wrap).last().data('index'); const templete = (index) => { return `<tr data-index="${index}"> <th>お名前</th> <td><div class="input text"><input type="text" name="list[${index}][name]" class="input-name" id="list-${index}-name" value=""></div></td> <th>年齢</th> <td><div class="input text"><input type="text" name="list[${index}][age]" class="input-name" id="list-${index}-age" value=""></div></td> <td><div class="button remove-btn">削除</div></td> </tr>`; } $wrap.on('click', '.add-btn', function() { index++; $itemWrap.append(templete(index)); }); $wrap.on('click', '.remove-btn', function() { $(this).parent().parent().remove(); }); }); })(jQuery);