データベース
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);



