1. Home
  2. PHP
  3. CakePHP
  4. CakePHP3のモデルとFormクラスの混合バリデーションでフォーム&登録機能を構築する

CakePHP3のモデルとFormクラスの混合バリデーションでフォーム&登録機能を構築する

  • 公開日
  • 更新日
  • カテゴリ:CakePHP
  • タグ:PHP,validation,Form,CakePHP,Model,Table,Entity,FormClass,CustomValidation
CakePHP3のモデルとFormクラスの混合バリデーションでフォーム&登録機能を構築する

フォーム機能を構築する際に、CakePHP では一般的にモデルが絡むバリデーションは Table クラスで定義したバリデーションルールに則って行いますが、複合的なフォームを構築する際は、そこに該当しないバリデーションが必要になる時があります。

具体的には、「データベースに格納するテキスト情報など」と「アップロードするファイルやなど」の場合です。

例えば、課題を提出する機能を構築するとして、それを提出するフォームから以下の情報を入力して登録するとします。

  • 所属クラス
  • 生徒の名前
  • 提出する課題の名前
  • 課題ファイル

上記3点と課題ファイル名についてはテーブルにカラムを持っておりそれらの情報を格納するとして、課題ファイルのアップロードの際の、ファイル自体のバリデーションはどこで行ったらよいでしょうか?モデルに絡む情報ではないので Table クラスで定義するのはロジック的に違和感を覚えます。

もちろんファイル自体のバリデーションであったり、モデルに絡まないバリデーションには通常、Form クラスを作成し行いますが、Table クラス(Model)と Form クラスのバリデーションがそれぞれ存在する1つのリクエスト(フォームから POST →各バリデーション→登録 or エラー→画面遷移)の中で、どのようなロジックで実装していったら効率が良いでしょうか。

という事で今回は、Model 側(Table クラス)でのバリデーションと Form クラスのバリデーションを混合させたフォーム機能(入力+検証+登録)を構築します。

Contents

  1. 開発環境
  2. 前段準備
    1. テーブル作成
    2. モデル作成
    3. コントローラ作成
    4. テンプレート作成
  3. 機能実装
    1. テンプレート実装
    2. カスタムバリデーション作成
    3. Form クラス作成
    4. アップロード先ディレクトリ作成
    5. コントローラ実装
  4. 動作確認

開発環境

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

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

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

レコード登録も行うので、適当なデータベースを1つ用意してください。(テーブルはこれから作成します)

前段準備

まずはベースとなるデータや MVC 周りを作成していきます。

テーブル作成

マイグレーションにて、データベースにテーブルを追加していきます。

テーブル定義

今回は課題を提出する機能を例にするので、以下のようなテーブルを作成します。

submission テーブル- id(主キー)

  • student_name(生徒名)
  • affiliation(所属)
  • issue_name(課題名)
  • file_name(課題ファイル名)
  • created(作成日)
  • modified(更新日)

一点だけ補足しておくと「課題名」というのは「PHP プログラミング課題」「二次関数課題」とかの、いわゆる課題の名目です。

マイグレーションファイル生成

テーブル作成の為にマイグレーションファイルを生成します。 cakephp ルートディレクトリへ移動し、以下の bake コマンドを叩きます。

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

# bakek コマンドでマイグレーションファイルを生成する
bin/cake bake migration CreateSubmission student_name:string affiliation:string issue_name:string file_name:string created modified

# 実行結果
[demo@localhost cakephp]# bin/cake bake migration CreateSubmission student_name:string affiliation:string issue_name:string file_name:string created modified

Creating file /var/www/html/cakephp/config/Migrations/XXXX_CreateSubmission.php
Wrote `/var/www/html/cakephp/config/Migrations/XXXX_CreateSubmission.php`

cakephp/config/Migrations 配下に XXXX_CreateSubmission.php が生成されます。

cakephp
├─ config
│   ├─ Migrations
│   │   ├─ XXXX_CreateSubmission.php

マイグレーション実行

マイグレーションを実行し、テーブルを作成します。

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

# マイグレーションを実行する
bin/cake migrations migrate

# 実行結果
[demo@localhost cakephp]# bin/cake migrations migrate
using migration paths 
 - /var/www/html/cakephp/config/Migrations
using seed paths 
 - /var/www/html/cakephp/config/Seeds
using environment default
using adapter mysql
using database cakephp

 == XXXX CreateSubmission: migrating
 == XXXX CreateSubmission: migrated 0.6392s

All Done. Took 0.7666s
using migration paths 
 - /var/www/html/cakephp/config/Migrations
using seed paths 
 - /var/www/html/cakephp/config/Seeds
Writing dump file `/var/www/html/cakephp/config/Migrations/schema-dump-default.lock`...
Dump file `/var/www/html/cakephp/config/Migrations/schema-dump-default.lock` was successfully written

MySQL へログインし、テーブルを確認します。

mysql> show tables;
+-------------------+
| Tables_in_cakephp |
+-------------------+
| phinxlog          |
| submission        |
+-------------------+

mysql> show columns from submission;
+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| id           | int(11)      | NO   | PRI | NULL    | auto_increment |
| student_name | varchar(255) | NO   |     | NULL    |                |
| affiliation  | varchar(255) | NO   |     | NULL    |                |
| issue_name   | varchar(255) | NO   |     | NULL    |                |
| file_name    | varchar(255) | NO   |     | NULL    |                |
| created      | datetime     | NO   |     | NULL    |                |
| modified     | datetime     | NO   |     | NULL    |                |
+--------------+--------------+------+-----+---------+----------------+

テーブルが作成されている事を確認できました。

モデル作成

次にモデルを作成します。 cakephp ルートディレクトリへ移動し、以下の bake コマンドを叩きます。

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

# bake コマンドでモデルを生成する
bin/cake bake model Submission

# 実行結果
[demo@localhost cakephp]# bin/cake bake model Submission
One moment while associations are detected.

Baking table class for Submission...

Creating file /var/www/html/cakephp/src/Model/Table/SubmissionTable.php
Wrote `/var/www/html/cakephp/src/Model/Table/SubmissionTable.php`

Baking entity class for Submission...

Creating file /var/www/html/cakephp/src/Model/Entity/Submission.php
Wrote `/var/www/html/cakephp/src/Model/Entity/Submission.php`
  • cakephp/src/Model/Table 配下に SubmissionTable.php
  • cakephp/src/Model/Entity 配下に Submission.php

が生成されます。

cakephp
├─ src
│   ├─ Model
│   │   ├─ Entity
│   │   │   └─ Submission.php
│   │   ├─ Table
│   │   │   └─ SubmissionTable.php

コントローラ作成

次はコントローラです。これも bake コマンドで生成するので、cakephp ルートディレクトリへ移動し以下のコマンドを叩きます。

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

# bake コマンドでコントローラを生成する
bin/cake bake controller Submission

# 実行結果
[demo@localhost cakephp]# bin/cake bake controller Submission

Baking controller class for Submission...

Creating file /var/www/html/cakephp/src/Controller/SubmissionController.php
Wrote `/var/www/html/cakephp/src/Controller/SubmissionController.php`
Bake is detecting possible fixtures...

Baking test case for App\Controller\SubmissionController ...

Creating file /var/www/html/cakephp/tests/TestCase/Controller/SubmissionControllerTest.php
Wrote `/var/www/html/cakephp/tests/TestCase/Controller/SubmissionControllerTest.php`

cakephp/src/Controller 配下に SubmissionController.php が生成されます。

cakephp
├─ src
│   ├─ Controller
│   │   ├─ SubmissionController.php

テンプレート作成

最後にテンプレートを作成します。 cakephp ルートディレクトリへ移動し、以下の bake コマンドを叩きます。

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

# bake コマンドでテンプレートを生成する
bin/cake bake template Submission

# 実行結果
[demo@localhost cakephp]# bin/cake bake template Submission

Baking `index` view template file...

Creating file /var/www/html/cakephp/src/Template/Submission/index.ctp
Wrote `/var/www/html/cakephp/src/Template/Submission/index.ctp`

Baking `view` view template file...

Creating file /var/www/html/cakephp/src/Template/Submission/view.ctp
Wrote `/var/www/html/cakephp/src/Template/Submission/view.ctp`

Baking `add` view template file...

Creating file /var/www/html/cakephp/src/Template/Submission/add.ctp
Wrote `/var/www/html/cakephp/src/Template/Submission/add.ctp`

Baking `edit` view template file...

Creating file /var/www/html/cakephp/src/Template/Submission/edit.ctp
Wrote `/var/www/html/cakephp/src/Template/Submission/edit.ctp`

cakephp/src/Template/Submission 配下にそれぞれのテンプレートファイルが生成されます。

cakephp
├─ src
│   ├─ Template
│   │   ├─ Submission
│   │   │   ├─ add.ctp
│   │   │   ├─ edit.ctp
│   │   │   ├─ index.ctp
│   │   │   └─ view.ctp

これで前段の準備は完了です。この時点で、テーブルに定義したカラムについての CRUD は既に実装出来た事になります。

機能実装

ここからは、混合バリデーションについての機能を実装していきます。

テンプレート実装

まずは、テンプレートの登録画面に、ファイル選択のパーツを追加します。

add.ctp を開いて、フォームヘルパー部分に1行追加&1行コメントアウト(削除でも OK)します。

cakephp/src/Template/Submission/add.ctp
<nav class="large-3 medium-4 columns" id="actions-sidebar">
    <ul class="side-nav">
        <li class="heading"><?= __('Actions') ?></li>
        <li><?= $this->Html->link(__('List Submission'), ['action' => 'index']) ?></li>
    </ul>
</nav>
<div class="submission form large-9 medium-8 columns content">
    <?= $this->Form->create($submission, ['type' => 'file']) ?>
    <fieldset>
        <legend><?= __('Add Submission') ?></legend>
        <?php
            echo $this->Form->control('student_name');
            echo $this->Form->control('affiliation');
            echo $this->Form->control('issue_name');
            //echo $this->Form->control('file_name'); // ← コメントアウト
            echo $this->Form->control('upload_file', ['type' => 'file', 'label' => 'ファイル', 'required' => true,]); // ← 追加
        ?>
    </fieldset>
    <?= $this->Form->button(__('Submit')) ?>
    <?= $this->Form->end() ?>
</div>

まずコメントアウトした部分ですが、アップロードするファイル名は、入力させるのではなく、アップロードしたファイルからファイル名を取得してそのまま挿入するので不要。という事でコメントアウトしています。(削除してしまっても OK)

追加した部分はファイル選択のフォームパーツです。 bake コマンドを叩いて生成されるテンプレートのフォームパーツはモデルでの定義によって生成されるのでファイルアップロードは当然ながら無いのでここで手動で追加する必要があります。

百聞は一見にしかずなのでフォームを見てみるとわかりやすいです。

Beforeと、こんな感じになります。

カスタムバリデーション作成

次はファイルのバリデーションを定義する為に、カスタムバリデーションを作成します。

cakephp/src/Model 配下に Validation ディレクトリを作成し、その中に CustomValidation.php を作成します。

cakephp
├─ src
│   ├─ Model
│   │   ├─ Validation
│   │       └─ CustomValidation.php

ファイルが作成できたら、カスタムバリデーションを定義します。

cakephp/src/Model/Validation/CustomValidation.php
<?php
namespace App\Model\Validation;

use Cake\Validation\Validation;

class CustomValidation extends Validation
{
  /**
   * ファイル容量制限
   * @param $files
   * @return bool
   */
  public static function limitFileSize($files)
  {
    return ($files['size'] < 100000);
  }

  /**
   * 画像ファイル「PNG」「JPG」「GIF」チェック
   * @param $files
   * @return bool
   */
  public static function isImage($files)
  {
    $ret = true;
    $mimes = array("image/png", "image/jpeg", "image/gif");
    $exts = array("png", "jpeg", "jpg", "gif");
    $sep = explode('.', $files['name']);
    $ext = end($sep);
    if (!in_array($files['type'], $mimes) || !in_array($ext, $exts)) {
      $ret = false;
    }
    return $ret;
  }
}

limitFileSize() メソッドとして、ファイルサイズが 100KB より小さい場合のみを true とし、それ以上を false が返るようにしています。

isImage() メソッドは、画像ファイル「png 」「jpg 」「gif 」のみを許可する記述を行っています。

今回はデモなので、これくらいのバリデーションでいこうと思います。

Form クラス作成

cakephp/src 配下に Form ディレクトリを作成し、その中に SubmissionForm.php を作成します。

cakephp
├─ src
│   ├─ Form
│   │   └─ SubmissionForm.php

ファイル作成後、Form クラスを以下のように定義します。

cakephp/src/Form/SubmissionForm.php
<?php
namespace App\Form;

use Cake\Form\Form;
use Cake\Validation\Validator;

class SubmissionForm extends Form
{
  protected function _buildValidator(Validator $validator)
  {
    $validator->provider('customValidate', 'App\Model\Validation\CustomValidation');

    $validator
      ->add('upload_file', 'isImage', [
        'provider' => 'customValidate',
        'rule' => 'isImage',
        'message' => '「PNG」「JPG」「GIF」画像のみアップロード可能です',
      ])
      ->add('upload_file', 'limitFileSize', [
        'provider' => 'customValidate',
        'rule' => 'limitFileSize',
        'message' => '100キロバイト以内にしてください',
      ]);

    return $validator;
  }
}

補足すると、まずカスタムバリデーションを customValidate としてプロパイダへ登録し、それから name 属性「upload_file 」つまりファイル選択のフォームデータに対してバリデーションを付与しています。

アップロード先ディレクトリ作成

ファイルをアップロードする場所を作っておきます。 cakephp/ 配下に storage ディレクトリを作成します。

cakephp
├─ src
│   ├─ storage

コントローラ実装

最後にコントローラの実装です。 add() アクションを以下のように変更します。(わかりやすさ重視でコントローラーに全て記載しますが適宜クラス設計をするのがベストです)

cakephp/src/Controller/SubmissionController.php
<?php
namespace App\Controller;

use App\Controller\AppController;
use App\Form\SubmissionForm;
use Cake\Datasource\ConnectionManager;
use Cake\Core\Exception\Exception;

class SubmissionController extends AppController
{
    public function add()
    {
        $submission = $this->Submission->newEntity();
        // Formクラスのインスタンス化
        $fileform = new SubmissionForm();
        if ($this->request->is('post')) {
          $conn = ConnectionManager::get('default');
          $conn->begin();
          try {
            $request_data = $this->request->getData();
            // ファイル名を格納
            $request_data['file_name'] = $this->request->data['upload_file']['name'];

            $submission = $this->Submission->patchEntity($submission, $request_data);
            // FormクラスとModelによるバリデーション
            if ($fileform->validate($this->request->data) && empty($submission->errors())) {
              // データベースへ登録
              if ($this->Submission->save($submission)) {
                // ファイルアップロード
                $ret = move_uploaded_file($this->request->data['upload_file']['tmp_name'], sprintf('/var/www/html/cakephp/storage/%s', $this->request->data['upload_file']['name']));
                if($ret) {
                  $conn->commit();
                  $this->Flash->success(__('課題を受け付けました'));
                  return $this->redirect(['action' => 'index']);
                } else {
                  $this->Flash->error(__('ファイルの保存に失敗しました'));
                  throw new Exception('File Upload Error.');
                }
              }
            } else {
              // ファイルバリデーションエラーをEntityへ追加
              if(!empty($fileform->errors()['upload_file'])) $submission->errors('upload_file', $fileform->errors()['upload_file']);
            }
          } catch (Exception $e) {
            //ロールバック
            $conn->rollback();
          }
         }
        $this->set(compact('submission'));
    }
}

基本的な処理の流れとしては以下になります。

  1. POST データを変数へ格納
  2. アップロードファイルからファイル名を取得し POST データ変数へ格納
  3. Form クラスと Model(Table クラス)による混合バリデーション
  4. バリデーションを通った場合は、データベースへ登録し、ファイルをアップロードする
  5. バリデーションに通らなかった場合は、Form クラス側のエラーを Model 側で受け取る

なんだかんだ書いていますが、肝は Form クラス側のエラーを Model 側に受け渡す部分です。こうする事で、フォームヘルパー側の実装もこれまで通りの記述(下手にこねくり回す必要がないという意味)で行う事ができます。

動作確認

全ての実装が完了したので、実際にブラウザからアクセスし動作確認を行います。
http://YOUR-DOMAI/submission/add
にアクセスします。

適当に入力して、バリデーションエラーを起こしてみます。

モデル側・ファイル側どちらのエラーでも、両方のエラーの場合も正常にエラーメッセージが表示されます。

登録が完了すれば、一覧に表示されます。

もちろん、ファイルもアップロードされます。

[demo@localhost cakephp]# ll storage/
total 12
-rw-r--r--. 1 apache apache 4282 Feb 24 22:34 test.png

まとめ

以上で作業は完了です。バリデーション周りはとても地味な部分ですが、こういったパターンの実装って現場では何気に少なくないので頭に入れておくと役に立つと思います。

Author

rito

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