RitoLabo

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クラスのバリデーションを混合させたフォーム機能(入力+検証+登録)を構築します。

アジェンダ
  1. 開発環境
  2. 前段準備
    1. テーブル作成
      1. テーブル定義
      2. マイグレーションファイル生成
      3. マイグレーション実行
    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
変更前フォーム
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'));
}
}

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

  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

まとめ

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