RitoLabo

CakePHP3のViewCellを使ってFormHelperでのForm生成をテンプレートから切り離す

  • 公開:
  • カテゴリ: PHP CakePHP
  • タグ: PHP,CakePHP,3.5,FormHelper,ViewCell

前回の記事でFormHelperにてフォームを作成した時に、以下のようなテンプレート(.ctp)が出来上がりました。

<style>
h1 {
margin-left: 10px;
font-size: 18px;
}
.form_wrap {
margin: 10px;
padding: 5px 30px;
width: 500px;
border: 1px solid #dcdcdc;
box-shadow: 2px 2px 3px #f5f5f5;
}
.error-message {
font-size: 12px;
color: #ac2925;
}
</style>
<h1>
members add
</h1>
<div class="form_wrap">
<?php
$form_template = [
'formstart' => '<form{{attrs}}>',
'formend' => '</form>',
'formGroup' => '{{label}}{{input}}',
'groupContainer' => '<div class="input {{type}}{{required}}">{{content}}</div>',
'groupContainerError' => '<div class="input {{type}}{{required}} error">{{content}}{{error}}</div>',
'legend' => '<legend>{{text}}</legend>',
'fieldset' => '<fieldset>{{content}}</fieldset>',
'error' => '<div class="error-message">{{content}}</div>',
'errorList' => '<ul>{{content}}</ul>',
'errorItem' => '<li>{{text}}</li>',
'label' => '<label{{attrs}}>{{text}}</label>',
'hiddenblock' => '<div style="display:none;">{{content}}</div>',
'input' => '<input type="{{type}}" name="{{name}}"{{attrs}}>',
'inputsubmit' => '<input type="{{type}}"{{attrs}}>',
'file' => '<input type="file" name="{{name}}"{{attrs}}>',
'select' => '<select name="{{name}}"{{attrs}}>{{content}}</select>',
'selectMultiple' => '<select name="{{name}}[]" multiple="multiple"{{attrs}}>{{content}}</select>',
'option' => '<option value="{{value}}"{{attrs}}>{{text}}</option>',
'optgroup' => '<optgroup label="{{label}}"{{attrs}}>{{content}}</optgroup>',
'radio' => '<input type="radio" name="{{name}}" value="{{value}}"{{attrs}}>',
'radioWrapper' => '{{label}}',
'checkboxWrapper' => '<div class="checkbox">{{input}}{{label}}</div>',
'checkboxFormGroup' => '{{input}}{{label}}',
'checkbox' => '<input type="checkbox" name="{{name}}" value="{{value}}"{{attrs}}>',
'dateWidget' => '{{year}}{{month}}{{day}}{{hour}}{{minute}}{{second}}{{meridian}}',
'textarea' => '<textarea name="{{name}}"{{attrs}}>{{value}}</textarea>',
'button' => '<button{{attrs}}>{{text}}</button>',
'submitContainer' => '<div class="submit">{{content}}</div>',
];

// フォーム開始
echo $this->Form->create($member, [
'type' => 'post',
'url' => ['controller' => 'Members', 'action' => 'add'],
'templates' => $form_template
]);

// hidden の生成
echo $this->Form->control('test', ['type' => 'hidden', 'value' => 12345]);

// 一般的なテキスト入力
echo $this->Form->control('name', ['label' => '一般的なテキスト']);

// メールアドレス
echo $this->Form->control('email', ['type' => 'email', 'label' => 'メールアドレス']);

// パスワード
echo $this->Form->control('password', ['label' => 'パスワード']);

// 数値のみの入力
echo $this->Form->control('age', ['type' => 'number', 'label' => '数値のみ', 'required' => true, 'min' => 20, 'max' => 100]);

// ラジオボタン
echo $this->Form->control('gender', [
'type' => 'radio',
'label' => 'ラジオボタン',
'required' => true,
'options' => [
1 => '女性',
2 => '男性',
3 => 'その他'
],
]);

// セレクトボックスで使うoptionの定義
$list = [
[ 'text' => '北海道', 'value' => 1 ],
[ 'text' => '東京都', 'value' => 2 ],
[ 'text' => '大阪府', 'value' => 3 ],
[ 'text' => '福岡県', 'value' => 4 ],
[ 'text' => '沖縄県', 'value' => 5 ],
];

// セレクトボックス
echo $this->Form->control('birthplace', [
'type' => 'select',
'label' => 'セレクトボックス',
'required' => true,
'options' => $list,
'multiple' => false,
'empty' => '選択してください'
]);

// 日時入力
echo $this->Form->control('birth_at', [
'type' => 'datetime',
'label' => '日時',
'required' => true,
'monthNames' => false,
'minYear' => date('Y'),
'maxYear' => date('Y')+1,
]);

// チェックボックス
echo $this->Form->control('checkbox_1', ['type' => 'checkbox', 'label' => '通知を受け取らない']);
echo $this->Form->control('checkbox_2', ['type' => 'checkbox', 'label' => '申し込む']);

// 送信ボタン
echo $this->Form->submit();

// フォーム終了
echo $this->Form->end();
?>
</div>

これはこれで良いのかもしれませんが、個人的にはテンプレートファイルにゴリゴリPHPが記述されているのが好きではなかったりします。

そこで今回は、これらFormHelperでのフォーム生成部分を、ViewCell(ビューセル)を使ってテンプレートから切り離したいと思います。

アジェンダ
  1. 開発環境
  2. ViewCell
  3. セルクラス生成
  4. セルクラス実装
  5. セルテンプレート実装
  6. ビューテンプレートの変更

開発環境

今回の環境は以下の通りです。

  • Linux CentOS 7
  • Apache 2.4
  • PHP 7.1
  • CakePHP 3.5

CakePHPのバージョンについては3系であれば同一手順で進めていけます。

CakePHPのルートディレクトリを「cakephp/」としています。

ビューテンプレートファイルなどは前回記事「CakePHP3のFormHelper入門編&フォームHTML出力を独自テンプレートで行う」の続きという事で行います。

ViewCell

ViewCell(ビューセル)とは、CakePHP3に内蔵されているビューの中の機能で、ビューというくくりの中でもさらに1つの小さなMVCを構築できるものです。ビューセルを使うと、処理を伴うある表示に関して、1つのセルクラスでの実装で使いまわせるという利点があります。

例えばWEBアプリケーションのナビゲーションを表示するのに何らかの処理(動的にリンクを取得したり)が必要だったとして、それを毎回全てのコントローラに実装し、ビューへset()するのはナンセンスです。

そんな時にビューセルを使う事で、取得処理も含め1つのセルクラスで実装でき、あとはテンプレート内で出力するだけ。を実現できる結構便利なクラスです。

[cookbook]ビューセル
https://book.cakephp.org/3.0/ja/views/cells.html

セルクラス生成

それでは作業に入っていきます。まずはセルクラスを生成します。CakepPHPルートディレクトリへ移動し、以下のbakeコマンドを叩きます。

# CakePHPのルートディレクトリへ移動する
cd /path/to/cakephp

# bakeコマンドでセルクラスを生成する
bin/cake bake cell Members

# 実行結果
[demo@localhost cakephp]# bin/cake bake cell Members

Creating file /var/www/html/cakephp/src/Template/Cell/Members/display.ctp
Wrote `/var/www/html/cakephp/src/Template/Cell/Members/display.ctp`

Creating file /var/www/html/cakephp/src/View/Cell/MembersCell.php
Wrote `/var/www/html/cakephp/src/View/Cell/MembersCell.php`

Baking test case for App\View\Cell\MembersCell ...

Creating file /var/www/html/cakephp/tests/TestCase/View/Cell/MembersCellTest.php
Wrote `/var/www/html/cakephp/tests/TestCase/View/Cell/MembersCellTest.php`

cakephp/src/View/Cell 配下に MembersCell.php が、cakephp/src/Template/Cell/Members 配下に display.ctp が生成されます。

cakephp
├─ src
│   ├─ Template
│   │   ├─
Cell
│   │   │   └─
Members
│   │   │   └─
display.ctp
│   ├─ View
│   │   ├─
Cell
│   │   │   └─
MembersCell.php

ディレクトリがない場合はそれも生成されます。上記の黄色で示したディレクトリはデフォルトでは存在していなく、コマンドを叩いた時に一緒に生成されたものになります。

cakephp/src/View/Cell/MembersCell.php
<?php
namespace App\View\Cell;

use Cake\View\Cell;

/**
* Members cell
*/
class MembersCell extends Cell
{

/**
* List of valid options that can be passed into this
* cell's constructor.
*
* @var array
*/
protected $_validCellOptions = [];

/**
* Default display method.
*
* @return void
*/
public function display()
{
}
}
cakephp/src/Template/Cell/Members/display.ctp
// 
作成時は空っぽです。

セルクラス実装

それではセルクラスを定義していきます。 MembersCell.php を開き、以下のように記述します。

cakephp/src/View/Cell/MembersCell.php
<?php
namespace App\View\Cell;

use Cake\View\Cell;

/**
* Members cell
*/
class MembersCell extends Cell
{

/**
* List of valid options that can be passed into this
* cell's constructor.
*
* @var array
*/
protected $_validCellOptions = [];

/**
* Default display method.
*
* @return void
*/
public function display()
{
// Membersモデルの読み込み
$this->loadModel('Members');

// エンティティインスタンスの生成
$member = $this->Members->newEntity();

// POSTされていた場合はバリデーションチェックの結果を取得する
if ($this->request->is('post')) {
$member = $this->Members->patchEntity($member, $this->request->getData());
}

// FormHelper にセットするオプションを取得する
$form_data = $this->getFormOptions();

// display.ctp にエンティティとオプションをセット
$this->set(compact('member', 'form_data'));
}


/**
* フォームオプション定義
* @return array
*/
private function getFormOptions()
{
$form_data = array();
$form_data['base'] = ['type' => 'post', 'url' => ['controller' => 'Members', 'action' => 'add'], 'templates' => $this->getFormTemplates()];
$form_data['hidden_id'] = ['type' => 'hidden', 'value' => 12345];
$form_data['name'] = ['label' => '一般的なテキスト'];
$form_data['email'] = ['type' => 'email', 'label' => 'メールアドレス'];
$form_data['password'] = ['label' => 'パスワード'];
$form_data['age'] = ['type' => 'number', 'label' => '数値のみ', 'required' => true, 'min' => 20, 'max' => 100];
$form_data['gender'] = [ 'type' => 'radio', 'label' => 'ラジオボタン', 'required' => true, 'options' => $this->getOptionsGender()];
$form_data['birthplace'] = [ 'type' => 'select', 'label' => 'セレクトボックス', 'required' => true, 'options' => $this->getOptionsBirthplace(), 'multiple' => false, 'empty' => '選択してください'];
$form_data['birth_at'] = [ 'type' => 'datetime', 'label' => '日時', 'required' => true, 'monthNames' => false, 'minYear' => date('Y'), 'maxYear' => date('Y')+1, ];
$form_data['checkbox_1'] = ['type' => 'checkbox', 'label' => '通知を受け取らない'];
$form_data['checkbox_2'] = ['type' => 'checkbox', 'label' => '申し込む'];

return $form_data;
}

/**
* gender の option 要素を返却する
* @return array
*/
private function getOptionsGender()
{
return [
1 => '女性',
2 => '男性',
3 => 'その他'
];
}

/**
* birthplace の option 要素を返却する
* @return array
*/
private function getOptionsBirthplace()
{
return [
[ 'text' => '北海道', 'value' => 1 ],
[ 'text' => '東京都', 'value' => 2 ],
[ 'text' => '大阪府', 'value' => 3 ],
[ 'text' => '福岡県', 'value' => 4 ],
[ 'text' => '沖縄県', 'value' => 5 ],
];
}

/**
* テンプレート定義
* @return array
*/
private function getFormTemplates()
{
return [
'formstart' => '<form{{attrs}}>',
'formend' => '</form>',
'formGroup' => '{{label}}{{input}}',
'groupContainer' => '<div class="input {{type}}{{required}}">{{content}}</div>',
'groupContainerError' => '<div class="input {{type}}{{required}} error">{{content}}{{error}}</div>',
'legend' => '<legend>{{text}}</legend>',
'fieldset' => '<fieldset>{{content}}</fieldset>',
'error' => '<div class="error-message">{{content}}</div>',
'errorList' => '<ul>{{content}}</ul>',
'errorItem' => '<li>{{text}}</li>',
'label' => '<label{{attrs}}>{{text}}</label>',
'hiddenblock' => '<div style="display:none;">{{content}}</div>',
'input' => '<input type="{{type}}" name="{{name}}"{{attrs}}>',
'inputsubmit' => '<input type="{{type}}"{{attrs}}>',
'file' => '<input type="file" name="{{name}}"{{attrs}}>',
'select' => '<select name="{{name}}"{{attrs}}>{{content}}</select>',
'selectMultiple' => '<select name="{{name}}[]" multiple="multiple"{{attrs}}>{{content}}</select>',
'option' => '<option value="{{value}}"{{attrs}}>{{text}}</option>',
'optgroup' => '<optgroup label="{{label}}"{{attrs}}>{{content}}</optgroup>',
'radio' => '<input type="radio" name="{{name}}" value="{{value}}"{{attrs}}>',
'radioWrapper' => '{{label}}',
'checkboxWrapper' => '<div class="checkbox">{{input}}{{label}}</div>',
'checkboxFormGroup' => '{{input}}{{label}}',
'checkbox' => '<input type="checkbox" name="{{name}}" value="{{value}}"{{attrs}}>',
'dateWidget' => '{{year}}{{month}}{{day}}{{hour}}{{minute}}{{second}}{{meridian}}',
'textarea' => '<textarea name="{{name}}"{{attrs}}>{{value}}</textarea>',
'button' => '<button{{attrs}}>{{text}}</button>',
'submitContainer' => '<div class="submit">{{content}}</div>',
];
}
}

まず、display()メソッド以外のprivateメソッドは全て、フォームのオプションやテンプレートを定義しているものになります。ここでそれぞれの値を定義し、返却するメソッドです。

メインとなるdisplay()メソッドについて、上から説明します。

// Membersモデルの読み込み
$this->loadModel('Members');

// エンティティインスタンスの生成
$member = $this->Members->newEntity();

// POSTされていた場合はバリデーションチェックの結果を取得する
if ($this->request->is('post')) {
$member = $this->Members->patchEntity($member, $this->request->getData());
}

ここではモデルインスタンスの作成と、フォームから送信(POST)されていた場合にはバリデーションチェックの結果を取り込む処理を行っています。この処理を行う事で、フォームから送信された値に不備がありバリデーションエラーで表示が戻された際にエラーメッセージをキャッチできます。(厳密にはエラーメッセージだけを取りに行っているわけではありません)

// FormHelper にセットするオプションを取得する
$form_data = $this->getFormOptions();

// display.ctp にエンティティとオプションをセット
$this->set(compact('member', 'form_data'));

ここでは、privateメソッドからフォームオプションが定義された配列を取得し、ビューへ渡しています。この時に渡しているのは、memberエンティティと、フォームオプションの配列です。

セルテンプレート実装

セルクラスでset()されたものは、セルクラスと関連付けられたテンプレートへ送られるので、そこでFormHelerを使ってフォームを定義していきます。

セルクラスと一緒に生成し、空っぽだった display.ctp を開き、以下のように記述します。

cakephp/src/Template/Cell/Members/display.ctp
<div class="form_wrap">
<?php
// オプション配列をループさせてフォームを描画する
foreach ($form_data as $k => $op) {
if($k=='base') {
echo $this->Form->create($member, $op);
} else {
echo $this->Form->control($k, $op);
}
}
echo $this->Form->submit();
echo $this->Form->end();
?>
</div>

フォームのオプションデータをループで回して作成しているので、記述はこれだけです。

前回の記事でも書きましたが、基本的にフォームパーツについてはcontrol()メソッドを使うようにすると、こうしてループする際にいちいち条件分岐を行わずにスマートに記述できます。

ビューテンプレートの変更

最後に、ビューテンプレートを変更します。本記事の冒頭でお見した、PHPゴリゴリのあいつです。やつを開いて、以下のように変更します。

cakephp/src/Template/Members/add.ctp
<style>
h1 {
margin-left: 10px;
font-size: 18px;
}
.
form_wrap {
margin: 10px;
padding: 5px 30px;
width: 500px;
border: 1px solid #dcdcdc;
box-shadow: 2px 2px 3px #f5f5f5;
}
.
error-message {
font-size: 12px;
color: #ac2925;
}
</
style>
<
h1>
members add
</h1>
<?= $this->cell('Members'); ?> // ←ここだけになたよ!v>c<v

なんという事でしょう。あんなに長かったFormHelperの記述が、セルのコールたった1行で済んでしまいました。

これで一件落着。ですね。

まとめ

作業は以上です。

ちなみにViewCelは、基本的にコンポーネント化を行う為に使われるケースがほとんどなので、今回のようにForm生成を切り出す使われ方はあまりしないかもしれません。

ですがこうしてフォームを切り出しておくと、フォームの定義部分も明確に管理できるし、何よりテンプレートがごちゃごちゃしなくなるのですごく良い!と私は思います。

もしあなたがテンプレートファイルにPHPゴリゴリが生理的に無理な方でしたら是非、試してみてください。