Help us understand the problem. What is going on with this article?

【CakePHP3】バリデーション・アプリケーションルールについて

More than 1 year has passed since last update.

社内用にまとめた内容の転載。

ココらへんの話をします

  • CakePHP3の値チェックは2段階!
  • ValidationとValidation Provider
  • Application Rules

Entityに渡す値のチェック(検証)は2段階で行われる

CakePHP3では、従来とは大幅に「バリデーション」の機構が変更されました。
これまでModel::save()Model::delete()時に暗示的に、もしくは明示的にModel::validate()をした際に走っていたデータの検証が、次のような2ステップに分けて扱われます。

  1. Entity作成・更新時に行われる「validation」
  2. Databaseに作用する(insert, update, delete)際に行われる「(application | domain) Rule」

この変化はパッと見とっつきにくいし、 なんぞ…という感じなのですが Bookによれば二者間の違いを次のように説明しています。

バリデーションルールは、ステートレスな方法の操作を意図しています。
アプリケーションルールは、あなたのエンティティの ステートフルなプロパティのチェックに最もよく作用します。

ちょっと分かりにくいのですが、例えば前者は「形式や必須項目漏れ等のチェック」 / 後者は「(データベース上のデータを絡めた)存在チェックや重複チェック」が該当します。

ここを踏まえて加えると、同じくBook中には以下のような記述も見つかりました。

バリデーションは直接のユーザ入力を意図しており、アプリケーションルールは アプリケーション中で生成されたデータの変更に特化しています。

具体例を挙げると、
* 「emailは必須項目」はvalidationに
* 正規表現を用いた「email形式であること」は validationに
* Databaseの内容にまつわる「emailがまだ利用されていないこと(isUnique)」はapplication rulesに

といったイメージでしょうか。
もちろん「validationで行っている内容はapplication rulesでも設定可能」になるので、検証内容の性質に応じて明確に区分される!というものではないと思います。1
※ どちらの検証についても、App\Model\Table に設定されるものになります。最初は「実データや値を扱うのだから、Entityに含まれるべきでは?」という問いも浮かびましたが、これは「Entityを生成する」役割と「EntityとDatabaseを仲介する」役割をTableが担っている、というイメージを持つことで掴みやすくなるかと思います。

validationとapplication rulesが分かれている意味(もう一歩踏み込んで。)

そもそもCakePHP2(など)では1つのタイミングで行われていた = 敢えて分ける必要性のなかったデータの検証が、CakePHP3で別々に扱われた理由はどこにあるのでしょうか?

この肝は、「validationが エンティティ構築時(もっと言えば、構築「前」)」に行われていて「application ruleが エンティティをDatabaseにsaveする時(同じく、saveする「前」)」 にそれぞれ行われているという点にあるかと思います。

Book を見ると、validationとEntityの構築の流れについて次のような記述があります。

  1. バリデータオブジェクトが作成されます。
  2. table および default バリデーションプロバイダが追加されます。
  3. 命名に沿ったバリデーションメソッドが呼び出されます。たとえば validationDefault 。
  4. Model.buildValidator イベントが発動します。
  5. リクエストデータが検証されます。 6 .リクエストデータがそのカラム型に対応する型に変換されます。
  6. エラーがエンティティにセットされます。
  7. 正しいデータはエンティティに設定されますが、 検証を通らなかったフィールドは除外されます。

特に注視すべきは「5」と「6」の順番だと思っていて、つまり setterやmarshalを挟む前の、受け取ったデータを検証する のがvalidationの働きである・・という事になります。

一方で、application ruleは「Databaseに保存可能なデータであるか」のチェックにその役割をとどめているわけです。(以前の機構では、 強いて言うなら「application rulesのみが存在していた」のだという理解を自分はしています。ライフサイクル的な観点から言えば。)

lorenzoはこう思ったッス

たぶん、コレが発端となったIssue
https://github.com/cakephp/cakephp/issues/5167

This makes clear that the problem is trying to validate data once it has been marshaled into the entity. Validation seems to work better when dealing with the unmodified data as received by the user, so we can report back in a better way what errors were actually made.

超訳: データーをごにょってから検証するんぢゃなくて、受け取った生データーを一旦チェックした方がよくね?そーすれば、ユーザーにもっと親切なメッセージとか返せちゃうし。

強調: validationの発動条件

ここまで「validationはEntity作成・更新時に行われるもの」として説明してきましたが、これは具体的には $Table->newEntity() $Table->patchEntity() が呼ばれていることを意味します。

すなわち、 Book中でも記述がある通り バリデーションはエンティティのプロパティを直接設定した時には起動 しないことを意味します。他方で、application ruleについては(直接更新されたpropertyの内容も含めて)save時に検証がなされるようになっています。

なので、「validationをかけたい時は newEntity() / patchEntity()を用いるべき」という事になります。

※ validationの内容をapplication ruleで(再度)呼び出す必要がある場合については、Bookに説明がありますので参照して下さい。

ValidationとValidation Provider

実際にValidationの使い方を見ていきましょう。

<?php
// cf https://github.com/Xety/Xeta/blob/master/src/Model/Table/GroupsTable.php#L37

namespace App\Model\Table;

use Cake\ORM\Table;
use Cake\Validation\Validator;

class GroupsTable extends Table
{
    /**
     * Default validation rules.
     *
     * @param \Cake\Validation\Validator $validator The Validator instance.
     *
     * @return \Cake\Validation\Validator
     */
    public function validationDefault(Validator $validator)
    {
        $validator
            ->notEmpty('name', __("You must set a name."))
            ->add('name', 'minLength', [
                'rule' => ['minLength', 3],
                'message' => __("The name can not be less than {0} characters.", 3)
            ]);
        return $validator;
    }

割と見たままで大丈夫そうなので、検証内容を詳細に紐解くのは割愛します。
「必須化」 +「最低3文字以上、違反したらThe name can not〜 のメッセージを返す」の2重のルールを設置している感じですね。

※もしvalidationnに引っかかった場合でも、newEntity() / patchEntity()Entityインスタンスを返します。そして、値が適正だったかどうかは $entity->errors()メソッドを介して知ることができます。

ここでは、validationDefault()となっているメソッド名に注目したいと思います。
CakePHP3では「バリデーションセット」という概念を導入しており、いくつかの検証内容を1つのまとまりとして括ることが可能です。上記例では「default」セットを定義している状態です。
Entity構築時に利用したいルールセットを明示的に指定することで、その都度異なる検証内容を扱えるようになります。
例えば「管理者権限のユーザー情報を更新する時には、emailアドレスに@mycompany.com ドメインであることを必須条件とする」場合にはどうでしょう。 defaultには含まれない(全ユーザーを適用対象としたいわけではない)内容を含むケースです。

namespace App\Model\Table;

use Cake\ORM\Table;
use Cake\Validation\Validator;

class UsersTable extends Table
{
    public function validationDefault(Validator $validator)
    {
        // code
     }

    public function validationAdmin(Validator $validator)
    {
        // defaultの検証内容を引き継ぐ
        $validator = $this->validationDefault($validator);
        // 検証内容を追加
        $validator->add('email', 'isCompanyDomain', [
            'rule' => function($data, $providor) {
                    $valid = strpos($data, '@mycompany.com'); 
                    return $valid ? true : '会社用のメールアドレスを入力してください';
                }
            ]);

        return $validator;
    }
}

// Controllerの処理
if ($isAdmin) {
   $entity = $this->Users->newEntity(
        $data,
        ['validate' => 'admin']
    );
} else {
   $entity = $this->Users->newEntity($data);
}

これで、「自社emailアドレスの持ち主であるか」のチェックがされるようになりました。
cf) 利用可能なバリデーションルールの一覧 (Validation API)

Validation Provider

前項で挙げた例のようにTableクラス内で「自由にValidatorを作成する」ことが可能ですが、複数箇所で何度も出てくる独自validation(rule)をどっかに定義しておきたい!という欲求があると思います。
これは、「Validation Provider」を利用することで解決が可能です。
cf) バリデーションプロバイダーを加える

たとえば、様々なTableにおいて「休日のリクエストは受け付けない」というvalidationを加えたいというケースがあったとします。
「お菓子屋のEC/店舗連携システム」を作っていて「休日は店舗受取不可、配送希望日指定不可、カフェ予約不可・・・」みたいな要求でしょうか。

まずproviderを設置してしまいます。providerには実装すべきinterfaceや継承すべきclassなども指定されておらず、単純にbool値を返すメソッド名を実装したコンテンツになります。 src/Validation/下に設置するのが習わしです。

namespace App\Validation;

class CalendarValidation
{
    /**
     * 営業日かどうかをバリデートする
     * 
     * @param string 検証対象となる日付
     * @return bool 検証結果
     */
    public function isOpeningDay($check)
    {
        // 土日祝日でないことをチェックするロジック
        return $isValid;
    } 
}

これを、実際にTable内で利用できるようにします。

namespace App\Model\Table

use Cake\ORM\Table;
use Cake\Validation\Validator;

class Reservations extends Table
{
        public function validationDefault(Validator $validator)
        {
            // `calendar`というキーでproviderを登録
            $validator->provider('calendar', 'App\Validation\CalendarValidation');
            $validator->add('reserved', 'isReservableDate', [
                'rule' => 'isOpeningDay', // CalanderValidation::isOpeningDay()の適用
                'provider' => 'calendar',
                'message' => '指定された日は営業日外となります' 
            ]);

            return $validator;
        }
}

「providerを登録して」「その中のruleを呼び出させる」だけで、複数のTableからオリジナルのruleを手軽に呼び出せるようになりました。

Application Rule

今度はapplication ruleについて掘り下げてみていきましょう。
validationがValidatorに対して設定を重ねていくことで成立させられていたように、application ruleはRuleCheckerに対して設定を重ねていくことになります。

フレームワークにビルトインされているのは IsUniqueExistsInValidCount の3つで、これらはそれぞれクラスとして定義されています。
cf) Namespace Cake\ORM\Rule | CakePHP

これらについてはデフォルトのRuleCheckerから呼び出す事ができるので、なにも意識せずとも利用可能です。
もしくは、クロージャを渡してその内部でゴニョる方法もあります。

// http://book.cakephp.org/3.0/ja/orm/validation.html#id11
use Cake\ORM\RulesChecker;

// テーブルクラスの中で
public function buildRules(RulesChecker $rules)
{
    // 組み込みのルールを利用(classをinvokeさせる)
    $rules->add(new IsUnique(['email']), 'uniqueEmail');

    // クロージャの利用 && 更新時のルールを指定
    $rules->addUpdate(function ($entity, $options) {
        $conditions = [
            'user_id' => $entity->user_id,
            'created >' => CakeTime::create('3 month ago')
        ];
        return !$options['repository']->exists($conditions);
    }, 'notUsedRecently');

    // 削除のルールを追加 && Entityのメソッドを利用
    $rules->addDelete(function ($entity, $options) {
        return $entity->notInReserved($this->sheet_id);
    }, 'notInReserved');

    return $rules;
}

クロージャでのルール追加

$entity, $options という引数をそれぞれ引き受けます。
$entityに関しては名前のままなので説明を割愛しますが、$optionsについては この辺り をまずは参照して下さい。

先の例で$options['repository'] に対してのアクセスを行っていますが、これは「元となるTable」のインスタンスが入っています。そのため、Database内部の情報との整合性をとるといったロジックも簡単に実装が可能です。

これに加えて、 add***()の第3引数がマージされた状態で渡ってきます。
ルールの中身については、
* 成功した場合は TRUEを
* 失敗した場合は FALSE、もしくは$entity->errors()の内容として通知したいエラーメッセージを

返却するようにして下さい。
その際に、$options['message'](etc)の内容のクロージャ内に引き渡されているので、入力値と絡めて動的なエラーメッセージの出力を行うことも容易に実現できます。

    $rules->addDelete(
        function ($entity, $options) {
            // 何らかの判定  if ($valid) {return true;}
            return sprintf($options['message'], $entity->name);
        },
        'invalidName',
        ['errorField' => 'name', 'message' => '%sは使えない名前です']
    );

クラスとしてRuleを定義する(カスタムルールオブジェクト)

cf) カスタムルールオブジェクト作成

組み込みのExistsInIsUniqueの様に、再利用性の高いルールを作成してクラスとして設置・利用することが可能です。
コレらは/src/Model/Rule以下に配置して、__invoke()メソッドを実装します。
実装例としては、組み込みのruleである ValidCountクラスが完結なコードとなっていますので、参照してみて下さい。

感想など

最初は「validationとappli application ruleってなんだよ!なんだか面倒くさそうなことしやがって!」と思ったのですが、元となったIssueの発議内容を見て「なるほどな」と感じました。やはり実装意図だったりの why な部分を知ることは大事で、OSSであること = 議論がオープンになっていることは良きことだな〜とそのメリットを享受する形になったなぁと感じました。
あと、チェックしたいルールを仕込む時にクロージャをぶっ込めるのはアツい。待ってました感ありますね。。


参考にさせていただいた記事: http://qiita.com/kozo/items/be1b32d1670e64f723f7


  1. 「検証内容」ではなく「検証対象となるデータが何に由来するか」で使い分けるべき、というのが設計の思想だと理解しています 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした