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