1. Home
  2. PHP
  3. CakePHP
  4. CakePHP3のViewCellを使ってFormHelperでのForm生成をテンプレートから切り離す

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

  • 公開日
  • 更新日
  • カテゴリ:CakePHP
  • タグ:PHP,CakePHP,FormHelper,ViewCell
CakePHP3のViewCellを使ってFormHelperでのForm生成をテンプレートから切り離す

前回の記事 で 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(ビューセル)を使ってテンプレートから切り離したいと思います。

Contents

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

開発環境

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

  • 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 ゴリゴリが生理的に無理な方でしたら是非、試してみてください。

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級