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

フォーム機能を構築する際に、CakePHP では一般的にモデルが絡むバリデーションは Table クラスで定義したバリデーションルールに則って行いますが、複合的なフォームを構築する際は、そこに該当しないバリデーションが必要になる時があります。
具体的には、「データベースに格納するテキスト情報など」と「アップロードするファイルやなど」の場合です。
例えば、課題を提出する機能を構築するとして、それを提出するフォームから以下の情報を入力して登録するとします。
- 所属クラス
- 生徒の名前
- 提出する課題の名前
- 課題ファイル
上記3点と課題ファイル名についてはテーブルにカラムを持っておりそれらの情報を格納するとして、課題ファイルのアップロードの際の、ファイル自体のバリデーションはどこで行ったらよいでしょうか?モデルに絡む情報ではないので Table クラスで定義するのはロジック的に違和感を覚えます。
もちろんファイル自体のバリデーションであったり、モデルに絡まないバリデーションには通常、Form クラスを作成し行いますが、Table クラス(Model)と Form クラスのバリデーションがそれぞれ存在する1つのリクエスト(フォームから POST →各バリデーション→登録 or エラー→画面遷移)の中で、どのようなロジックで実装していったら効率が良いでしょうか。
という事で今回は、Model 側(Table クラス)でのバリデーションと Form クラスのバリデーションを混合させたフォーム機能(入力+検証+登録)を構築します。
Contents
開発環境
今回の開発環境は以下の通りです。
- 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')); } }
基本的な処理の流れとしては以下になります。
- POST データを変数へ格納
- アップロードファイルからファイル名を取得し POST データ変数へ格納
- Form クラスと Model(Table クラス)による混合バリデーション
- バリデーションを通った場合は、データベースへ登録し、ファイルをアップロードする
- バリデーションに通らなかった場合は、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
まとめ
以上で作業は完了です。バリデーション周りはとても地味な部分ですが、こういったパターンの実装って現場では何気に少なくないので頭に入れておくと役に立つと思います。