CakePHP 4 でファイルアップロード時のバリデーションを実装する方法

はじめに

以前の投稿「CakePHP 4 でファイルアップロードを実装する方法」では、ファイルをアップロードする方法はご紹介しましたが、バリデーションの実装方法は説明しませんでした。

今日は CakePHP 4 でファイルアップロード時のバリデーションを実装する方法を紹介します。

今回使用したのは CakePHP 4.0.8 です。

目次
  1. 下準備
  2. モデルを使う場合
  3. モデル無しの場合
  4. おわりに

1. 下準備

今回は Samples コントローラの index アクションとして実装しました。

/src/Controller/SamplesController.php
<?php
declare(strict_types=1);

namespace App\Controller;

class SamplesController extends AppController
{
    public function index()
    {
    }
}
/templates/Samples/index.php
<?= $this->Form->create(null, ['type' => 'file']) ?>
    <!-- エラーメッセージ -->
    <?php if ($this->Form->isFieldError('upfile')) : ?>
        <?= $this->Form->error('upfile') ?>
    <?php endif; ?>
    <!-- ファイル選択フォーム -->
    <?= $this->Form->file('upfile') ?>
    <!-- 送信ボタン -->
    <?= $this->Form->submit('アップロード') ?>
<?= $this->Form->end() ?>

Form ヘルパーの使い方は下記が参考になると思います。
file() は control() でも差支えありません。

サンプルコードを試される場合は、アップロードしたファイルの保存用として /webroot/upload フォルダを作ってください。これが無いとエラーになります。

2. モデルを使う場合」のサンプルコードは、下記テーブルで確認しました。

CREATE TABLE my_files (
  id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  path varchar(255) NOT NULL,
  created datetime NOT NULL,
  modified datetime NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. モデルを使う場合

モデルを使う場合には、通常のバリデーションと同様に Table クラスに実装します。

今回のサンプルでは Entity/MyFile.php は無くて大丈夫です。

/src/Model/Table/MyFilesTable.php
<?php
declare(strict_types=1);

namespace App\Model\Table;

use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class MyFilesTable extends Table
{
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('my_files');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');

        $this->addBehavior('Timestamp');
    }

    public function validationDefault(Validator $validator): Validator
    {
        $validator
            // 未選択
            ->notEmptyFile('upfile', 'ファイルを選択してください')
            // アップロードエラー
            ->add('upfile', 'uploadError', [
                'rule' => ['uploadError'],
                'message' => 'ファイルのアップロードができませんでした',
                'last' => true,
            ])
            // ファイルサイズ
            ->add('upfile', 'fileSize', [
                'rule' => ['fileSize', '<', '102400'],
                'message' => '100 キロバイト未満のファイルを選択してください',
            ])
            // 拡張子
            ->add('upfile', 'extension', [
                'rule' => ['extension', ['jpg', 'png']],
                'message' => '拡張子が jpg か png のファイルを選択してください',
                'last' => true,
            ])
            // MIME タイプ
            ->add('upfile', 'mimeType', [
                'rule' => ['mimeType', ['image/jpeg', 'image/png']],
                'message' => 'JPEG か PNG 形式のファイルを選択してください',
            ]);

        return $validator;
    }

    // ファイルの保存処理はコントローラなど別な場所に書いても影響ありません
    public function beforeSave(
        EventInterface $event,
        EntityInterface $entity,
        ArrayObject $options
    ) {
        // ファイル保存
        // ※ webroot/upload フォルダを事前に作ってください
        $file = $entity->upfile;
        $name = $file->getClientFilename();
        $path = WWW_ROOT . 'upload' . DS . $name;
        $file->moveTo($path);

        $entity->set(compact('path'));
    }
}
/src/Controller/SamplesController.php
public function index()
{
    $this->loadModel('MyFiles');

    $myFile = $this->MyFiles->newEntity([]);

    $errors = [];
    if ($this->request->is('post')) {
        $myFile = $this->MyFiles->patchEntity($myFile, $this->request->getData());
        if ($this->MyFiles->save($myFile)) {
            $this->Flash->success('アップロード完了');
        } else {
            $errors = $myFile->getErrors();
            $this->Flash->error('アップロード失敗');
        }
    }

    $this->set(compact('errors', 'myFile'));
}
/templates/Samples/index.php
<!-- ▼ $myFile に変更 -->
<?= $this->Form->create($myFile, ['type' => 'file']) ?>
...

ファイルのバリデーションルールは公式ドキュメントでは見つけられず、下記ページで見つけた mimeType を手がかりに
/vendor/cakephp/cakephp/src/Validation/Validation.php を見て書きました。

下記ページの例では、特定条件を満たす場合にだけファイルのバリデーションを行う 'on' オプションの使い方が紹介されています。

僕は使ったことがないのですが uploadedFile ルールを使えば、uploadError、mimeType、fileSize が1つでチェックできるようです。uploadedFile の機能については下記ページに説明はあるのですが、記述が少ないので、前述の Validation.php を見るのが良いのかなと思います。

3. モデル無しの場合

データベースへの登録が不要な場合など、モデルを使用しない場合には、下記のように独自の Form クラスを作ってバリデーションを実装できます。

/src/Form/MyForm.php
<?php
namespace App\Form;

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

class MyForm extends Form
{
    protected function _buildSchema(Schema $schema): Schema
    {
        return $schema;
    }

    public function validationDefault(Validator $validator): Validator
    {
        $validator
            // 未選択
            ->notEmptyFile('upfile', 'ファイルを選択してください');
            // 他のルールは先に紹介した「MyFilesTable.php」の例などを参考にしてください

        return $validator;
    }

    // ファイルの保存処理はコントローラなど別な場所に書いても影響ありません
    protected function _execute(array $data): bool
    {
        // ファイル保存
        // ※ webroot/upload フォルダを事前に作ってください
        $file = $data['upfile'];
        $name = $file->getClientFilename();
        $path = WWW_ROOT . 'upload' . DS . $name;
        $file->moveTo($path);

        return true;
    }
}
/src/Controller/SamplesController.php
...
use App\Form\MyForm; // ← 追加

class SamplesController extends AppController
{
    // ▼ 処理を追加
    public function index()
    {
        $myForm = new MyForm();
        $this->set(compact('myForm'));

        $errors = [];
        if ($this->request->is('post')) {
            $postData = $this->request->getData();
            if ($myForm->execute($postData)) {
                $this->Flash->success('アップロード完了');
            } else {
                $errors = $myForm->getErrors();
                $this->Flash->error('アップロード失敗');
            }
        }
    }
}
/templates/Samples/index.php
<!-- ▼ $myForm に変更 -->
<?= $this->Form->create($myForm, ['type' => 'file']) ?>
...

モデル不使用でフォームをつくる方法は、公式ドキュメントの下記ページが参考になります。

4. おわりに

CakePHP 4 はファイルのバリデーションも実装されていて便利だなと思います。自前で書くと結構大変なんですよね。

また、モデル無しフォームも使い勝手がいいと思います。モデルと書き方が同様なので、後日 DB に保存することになった場合には、モデルへの移行が簡単にできますね。

CakePHP の公式ドキュメントはかなり充実していると思うのですが、残念ながらファイルのバリデーションについては詳細な説明がみつかりませんでした。

同じように困っている方に、この記事が参考になればいいなと思っています。