WEBOPIXEL

CakePHP4で確認画面付き登録フォームの作成方法

Posted: 2020.09.21 / Category: CakePHP 

ザ・日本の業務システムだと登録前に確認画面を入れることがあると思います。
ということで、シンプルなパターンと深くアソシエーションが設定された場合の方法をご紹介します。

Sponsored Link

DB(テーブル)の構造

今回使用するDBをマイグレーションファイルで作成します。

config/Migrations/00000000000000_CreateProjects.php

<?php
declare(strict_types=1);

use Migrations\AbstractMigration;

class CreateProjects extends AbstractMigration
{
	public function change()
	{
		$table = $this->table('projects');
		$table->addColumn('title', 'string', [
				'limit' => 150,
				'null' => false,
			])
			->addColumn('description', 'text', [
				'limit' => 255,
			])
			->addColumn('created', 'datetime')
			->addColumn('modified', 'datetime')
			->create();


		$table = $this->table('project_details');
		$table->addColumn('title', 'string', [
				'limit' => 150,
				'null' => false,
			])
			->addColumn('description', 'text', [
				'limit' => 255,
			])
			->addColumn('project_id', 'integer', [
				'null' => false
			])
			->addColumn('created', 'datetime')
			->addColumn('modified', 'datetime')
			->addForeignKey('project_id', 'projects', 'id')
			->create();


		$table = $this->table('schedules');
		$table
			->addColumn('start_date', 'date')
			->addColumn('end_date', 'date')
			->addColumn('project_detail_id', 'integer', [
				'null' => false
			])
			->addColumn('created', 'datetime')
			->addColumn('modified', 'datetime')
			->addForeignKey('project_detail_id', 'project_details', 'id')
			->create();
	}
}

メインとなるProjectテーブルがありhasManyでproject_detailsを持っている、project_detailsはhasOneでschedulesを持っているという構造です。

基本となるファイルをbakeで作成しておきましょう。

$ bin/cake bake all projects
$ bin/cake bake all project_details
$ bin/cake bake all schedules

単一テーブルの更新

最初にアソシエーションのない場合の例です。
Projectだけを追加作成してみましょう。

src/Controller/ProjectsController.php

<?php
declare(strict_types=1);

namespace App\Controller;

/**
 * Projects Controller
 *
 * @property \App\Model\Table\ProjectsTable $Projects
 * @method \App\Model\Entity\Project[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
 */
class ProjectsController extends AppController
{
	/** @var \Cake\Http\Session */
	private $session;

	public function initialize(): void
	{
		parent::initialize();
		$this->session = $this->getRequest()->getSession();
	}

	// ...(その他の処理)

	// 入力画面
	public function add()
    {
        $project = $this->Projects->newEmptyEntity();

		// セッションに入力情報があったら読み込む
        if ($this->session->check('inputs')) {
            $inputData = $this->session->consume('inputs');

            $project = $this->Projects->patchEntity(
                $project, $inputData
            );
        }

		// セッションにエラー情報があったらセット
        if ($this->session->check('errors')) {
            $project->setErrors($this->session->consume('errors'));
        }

        $this->set(compact('project'));
	}
	
	// 確認画面
    public function addConfirm()
    {
        $this->request->allowMethod(['post']);

        $inputData = $this->request->getData();

        $this->session->write('inputs', $inputData);

        $project = $this->Projects->newEmpty($inputData);

		// バリデーションエラーの場合はセッションに入れて戻る
        if ($project->hasErrors()) {
            $this->session->write('errors', $project->getErrors());
            $this->Flash->error('バリデーションエラーです。');
            return $this->redirect(['action' => 'add']);
        }

        $this->set(compact('project'));
    }

	// 完了画面
    public function addComplete()
    {
        $this->request->allowMethod(['post']);

        // 入力セッションがない場合戻る
        if (!$this->session->check('inputs')) {
            $this->Flash->error('セッションがありません。');
            return $this->redirect(['action' => 'add']);
        }

        $inputData = $this->session->consume('inputs');

        $project = $this->Projects->newEntity($inputData);

		// バリデーションエラーの場合はセッションに入れて戻る
        if ($project->hasErrors()) {
            $this->session->write('errors', $project->getErrors());
            $this->Flash->error('バリデーションエラーです。');
            return $this->redirect(['action' => 'add']);
		}
		
        if ($this->Projects->save($project)) {
            $this->Flash->success('保存しました。');
            return $this->redirect(['action' => 'index']);
        }

        $this->Flash->error('保存に失敗しました。');
        return $this->redirect(['action' => 'add']);
    }
}

入力画面と完了(登録)画面の間に確認画面が入ることによって入力情報をセッションで保持する必要があります。
またCakePHPではバリデーションエラーの情報はエンティティに入るので、確認画面ではエラー発生時getErrorsでセッションに保持し、入力画面ではエラーセッションがある場合はsetErrorsでエンティティに入れてます。

ビュー

入力画面のビューファイルから作っていきましょう。
複数のForm->controlを使用する場合はForm->controlsを使うとまとめて作成できます。
(タグ構造が固定されるので実際はあまり使うケースはないと思いますが)

templates/Projects/add.php

<h3>入力画面</h3>
<?= $this->Form->create($project, ['url' => ['action' => 'addConfirm']]) ?>
<fieldset>
	<?= $this->Form->controls(['title', 'description']) ?>
</fieldset>
<?= $this->Form->button('確認画面') ?>
<?= $this->Form->end() ?>

確認画面は詳細画面とほぼ同じです。
違いはフッターに戻るボタンと登録ボタンを設置することです。

templates/Projects/add_confirm.php

<h3>確認画面</h3>
<table>
	<tr>
		<th>タイトル</th>
		<td><?= h($project->title) ?></td>
	</tr>
	<tr>
		<th>概要</th>
		<td><?= $this->Text->autoParagraph(h($project->description)) ?></td>
	</tr>
</table>
<?= $this->Html->link('戻る', ['action' => 'add']) ?>
<?= $this->Form->create($project, ['url' => ['action' => 'addComplete']]) ?>
<?= $this->Form->button('登録') ?>
<?= $this->Form->end() ?>

アソシエーションのある場合

次にアソシエーションがある場合の設定方法を見ていきましょう。
基本的には命名規則をしっかりと守っていればある程度CakePHPがやってくれます。

モデル

モデルのアソシエーションから確認してみましょう。
Projectsは複数のProjectDetailsを持つのでhasManyで繋ぎます。

src/Model/Table/ProjectsTable.php

$this->hasMany('ProjectDetails', [
	'foreignKey' => 'project_id',
]);

ProjectDetailsは一つのSchedulesを持つのでhasOneで繋ぎます。

src/Model/Table/ProjectDetailsTable.php

$this->hasOne('Schedules', [
	'foreignKey' => 'project_detail_id'
]);

上記の場合は命名規則の通りのカラム名になっているので、foreignKeyオプションは指定しなくても問題ありません。

エンティティの$_accessibleにアソシエーションのプロパティ(project_details,schedule)が設定されていることを確認してください。

src/Model/Entity/Project.php

$protected $_accessible = [
	'title' => true,
	'description' => true,
	'created' => true,
	'modified' => true,
	'project_details' => true,
];

src/Model/Entity/ProjectDetail.php

protected $_accessible = [
	'title' => true,
	'description' => true,
	'project_id' => true,
	'created' => true,
	'modified' => true,
	'project' => true,
	'schedule' => true,
];

これでProjectsからProjectDetailsとSchedulesまでを繋げることができました。

コントローラー

コントローラーはProjectsとProjectDetails(1階層)まではCakePHPが自動で保存してくれるので、特に設定は必要ないのですが、Schedulesまでの2階層のアソシエーションを保存したい場合はオプションを追加する必要があります。
次のようにnewEntityassociatedオプションを追加します。

$project = $this->Projects->newEntity(
	$inputData,
	['associated' => ['ProjectDetails.Schedules']]
);

patchEntityの場合も同じように設定します。

$project = $this->Projects->patchEntity(
	$project, $inputData,
	['associated' => ['ProjectDetails.Schedules']]
);

ビュー

add.phpcontrolsを次のように編集します。
パラメータのの記述はスネークケースで記述し、hasManyは複数形、hasOneは単数形で指定します。
なのでProjectDetailsproject_detailsになり、Schedulesscheduleになります。 またhasManyはインデックスを指定します。サンプルでは一つなので0だけですが、複数になる場合は0の部分が1,2,3と増えていくことになります。

templates/Projects/add.php

<?= $this->Form->controls([
	'title',
	'description',
	'project_details.0.title',
	'project_details.0.description',
	'project_details.0.schedule.start_date',
	'project_details.0.schedule.end_date'
]) ?>

確認画面は次のようになります。

templates/Projects/add.php

<h3>確認画面</h3>
<table>
	<tr>
		<th>タイトル</th>
		<td><?= h($project->title) ?></td>
	</tr>
	<tr>
		<th>概要</th>
		<td><?= $this->Text->autoParagraph(h($project->description)) ?></td>
	</tr>
</table>

<?php if (!empty($project->project_details)) : ?>
	<?php foreach ($project->project_details as $detail) : ?>
		<table>
			<tr>
				<th>詳細タイトル</th>
				<td><?= h($detail->title) ?></td>
			</tr>
			<tr>
				<th>詳細概要</th>
				<td><?= h($detail->description) ?></td>
			</tr>
			<tr>
				<th>開始日</th>
				<td><?= h($detail->schedule->start_date) ?></td>
			</tr>
			<tr>
				<th>終了日</th>
				<td><?= h($detail->schedule->end_date) ?></td>
			</tr>
		</table>
	<?php endforeach; ?>
<?php endif; ?>


<?= $this->Html->link('戻る', ['action' => 'add']) ?>
<?= $this->Form->create($project, ['url' => ['action' => 'addComplete']]) ?>
<?= $this->Form->button('登録') ?>
<?= $this->Form->end() ?>

今回はhasMenyの項目は固定でしたが、入力項目を自由に増減させたいことがあると思います。
その場合JavaScriptを使用することになります。下記の記事で項目の増減方法を解説してるのよろしかったら!
jQueryで入力フォームの可変項目を増減する度にname属性を採番する