CakePHP3を触ってみました 〜バリデーションのバリエーション〜

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
ドラクエ2はクリアしたことがないです

この記事を三行にまとめると

ドラクエっぽいですね
「もょもと」を渡す方法が分かりませんでした
ぐうの音くらいしか出ないですけどもね
入力されたデータのエラーチェックを行うにあたって、CakePHPではバリデーション用のクラスやら何やらがあらかじめ用意されています。それを使えば、(一から自作するよりは)簡単で柔軟なエラーチェックが行えるのですが、個人的には、CakePHP2と3で、このバリデーションの使い方が大きく変わった気がします。ってか、CakePHP3の方のバリデーションが、いまいち上手く使いこなせねえ。

そんなわけで今日は、誰か助けて〜というメッセージも込めつつ、バリデーションの使い方を見て行きたいと思います。

正直、もんのすごい長い記事になることが予想されますが、必要なとこだけ拾って読んでもらえたらと思います。

内訳はこんな感じです。

では長丁場の旅に、れっつらごー。



エラーチェックの方法

まずは、エラーチェックをどうやって行うかを、簡単にさらっと見てみましょー。

ここでは、usersというテーブルに、名前とメールアドレスを入力する画面を用意してみます。CakePHPのFormヘルパーを使って、二つのテキストボックスを生成します。モデル名はUser。

<?php echo $this->Form->create('User') ?>
  <?php echo $this->Form->text('name') ?>
  <?php echo $this->Form->text('email') ?>
<?php echo $this->Form->end() ?>

ざっくり書くと、こんな感じですかね。

CakePHP2でエラーチェックを行うには、validates()というメソッドを使います。

$this->User->create($this->request->data);
if($this->User->validates()) {
  //エラーなし
} else {
  //エラーあり
}

CakePHP3では、エンティティを作成する際に、エラーチェックを行うことができます。

$user = $this->Users->newEntity($this->request->data);
if(!$user->errors()) {
  //エラーなし
} else {
  //エラーあり
}

細かい説明については、今日は省略します。こうやればエラーチェックができるってことだけ、押さえときやしょう。

newEntity()とかの動きについては、以前にも記事を書いたことがあるので、もし参考になるようであれば、こちらもどーぞ。エラーチェックについても少し触れてます。
CakePHP3を触ってみました 〜createもsetもできないんですけど?〜



基本的なバリデーション

ほんじゃあ、バリデーションの一番基本的な書き方、あらかじめ用意されているメソッドを使ったやり方から。

今回は、名前もメールアドレスも必須入力であるという前提でいきます。つまりどちらも未入力だったらエラーになる。ついでにメールアドレスに関しては、形式のエラーチェックも行ってみましょう。

CakePHP2だと、こうなります。

//User.php
class User extends AppModel {
  public $validate = array(
    'name' => array(
      'notEmpty' => array(
        'rule' => 'notEmpty',
        'message' => '必須入力です',
      ),
    ),

    'email' => array(
      'notEmpty' => array(
        'rule' => 'notEmpty',
        'message' => '必須入力です',
      ),
      'email' => array(
        'rule' => 'email',
        'message' => '形式が不正です',
      ),
    ),
  );
}

「rule」がエラーチェックのメソッド、「message」がエラーメッセージになります。

「notEmpty」や「email」というのは、あらかじめ用意されているメソッドです。これで、validates()メソッドを呼び出したときに、必須入力やメールアドレスの形式に関するエラーチェックを行うことができます。

他にもどんなメソッドがあるかは、コアライブラリの「Utility」というフォルダの中に「Validation.php」というファイルがあるので、その中を見てくだせえ。

上記のように書いた場合、エラーがあると、ビューの「$this->validationErrors」という変数にエラーメッセージが入ってきます。

//$this->validationErrors
array(
  [name] => array(
    [0] => '必須入力です'
  )
  [email] => array(
    [0] => '形式が不正です'
  )
)



続いてCakePHP3。

namespace App\Model\Table;
use Cake\Validation\Validator;

class UsersTable extends Table {
  public function validationDefault(Validator $validator) {
    $validator->notEmpty('name', '必須入力です');
   $validator->notEmpty('email', 'メールアドレスが未入力です');
    $validator->add('email', 'email', [
      'rule' => 'email',
      'message' => '形式が不正です'
    ]);
    return $validator;
  }
}

CakePHP2では、$validateというメンバ変数に必要な内容を書きましたが、CakePHP3では「validation***」というメソッドをテーブルに作ります。

上記の例では「validationDefault()」となっていますが、メソッド名は任意に変えることが可能です。「validationOyado()」でも「validationOyaki()」でも「validationOyakata()」でも何でも良い。

ただしDefault以外にした場合は、newEntity()やpatchEntity()でそのメソッドを呼び出すための設定が必要になる。

//validationOyado()にした場合
$this->Users->newEntity($this->request->data, ['validate' => 'oyado']);

//validationOyaki()にした場合
$this->Users->newEntity($this->request->data, ['validate' => 'oyaki']);

//validationOyakata()にした場合
$this->Users->newEntity($this->request->data, ['validate' => 'oyakata']);

第二引数(patchEntity()の場合は第三引数)に、「validation***」の***の部分を記述します。先頭は小文字でおk。

この「validate => ‘***’」を省略した場合は、validationDefault()が呼ばれます。

CakePHP3では、基本的には$validatorのadd()メソッドを使って、ルールを追加していきます。でもnotEmptyは、そのままnotEmpty()というメソッドを使うみたい。何でかな?

CakePHP3であらかじめ用意されているメソッドは、コアライブラリの「Validation」というフォルダの中にある「Validation.php」で確認できます。

エラーがあると、冒頭でも書きましたが、「$user->errors()」でエラーメッセージを取得できます。

//$user->erros()
array(
  [name] => array(
    [_empty] => 必須入力です
  )
  [email] => array(
    [email] => 形式が不正です
  )
)

notEmpty()の場合は「_empty」というキー(たぶん固定)にメッセージが入ります。add()で追加した場合は、自分でキーを指定できます。

$validator->add('フィールド', 'キー', [
  'rule' => 'email',
  'message' => 'メッセージ'
]);

//$user->erros()
array(
  [フィールド] => array(
    [キー] => メッセージ
  )
)

設定次第で、お好みのキーにできます。okonomiyakiとかにもできます。



モデルやテーブルに直接書く

基本だけでもだいぶ長くなってしまいましたね。でもこっからさらに長いのが続きますぞー。

次は、エラー判定用のメソッドを自作する場合です。例えば、そうっすね……名前にひらがなしか使えない、みたいなルールを自作してみましょうか。ドラクエっぽいですね。じゃあルール名も「draque」で行きましょう。

やり方は大きく分けて二つあります。

一つは、モデルやテーブルに直接メソッドを書くやり方。

CakePHP2の場合は、モデルに書きます。

//User.php
class User extends AppModel{
  public $validate = array(
    'name' => array(
      'rule' => 'draque',
      'message' => 'ひらがなで入力してください',
    ),
  );

  public function draque($data) {
    $check = is_array($data) ? array_shift($data) : $data;
    return preg_match("/^[ぁ-ん]+$/u", $check);
  }
}

ざっくりこんな感じ。何でdraque()の中でarray_shift()してるかっていうと、上記のようにエラーチェックを行った場合、$dataの中身はこうなってるからです。

[name] => 入力された文字列

「$data[‘name’]」としても良いんだけど、それだとフィールド名がnameのときしか対応できないから、一応こんな風に書いてます。

あとは正規表現で、入力された文字列がひらがなだけだったら、trueが返るようにしてます。

ちなみに、何かしらの値を一緒に渡したい場合。

//User.php
class User extends AppModel{
  public $validate = array(
    'name' => array(
      'rule' => array('draque', 'ああああ', 'もょもと')
      'message' => 'ひらがなで入力してください',
    ),
  );

  public function draque($data, $value1, $value2) {
    $check = is_array($data) ? array_shift($data) : $data;
    return preg_match("/^[ぁ-ん]+$/u", $check);
  }
}

ruleのところを配列で書くことによって、引数に値を渡せます。この場合は「ああああ」と「もょもと」が、それぞれ$value1と$value2に入ります。



CakePHP3の場合は、テーブルに書きます。

//UsersTable.php
class UsersTable extends Table {
  public function validationDefault(Validator $validator) {
    $validator->add('name', 'draque', [
      'rule' => [$this, 'draque'],
      'message' => 'ひらがなで入力してください',
    ]);
    return $validator;
  }

  public function draque($value) {
    return (bool) preg_match('/^[ぁ-ん]+$/u', $value);
  }
}

基本の書き方とちょっと違うのは、ruleのところですね。[$this, メソッド名]という書き方になっています。

CakePHP3の場合は、$valueに入力された値が入ってきますので、array_shift()的な処理はいらないです。そのまま使えます。あと返り値は、booleanにしないと上手く動かないっぽい。

ruleの中に、直接メソッドを書くこともできます。

//UsersTable.php
class UsersTable extends Table {
  public function validationDefault(Validator $validator) {
    $validator->add('name', 'draque', [
      'rule' => function draque($value) {
        return (bool) preg_match('/^[ぁ-ん]+$/u', $value);
      },
      'message' => 'ひらがなで入力してください',
    ]);
    return $validator;
  }
}

結果は一緒です。



ヘルプミーその1

ではここで、第一のヘルプミーポイント。

CakePHP3の方でやった場合、「ああああ」や「もょもと」を渡す方法が分かりませんでした。

//UsersTable.php
class UsersTable extends Table {
  public function validationDefault(Validator $validator) {
    $validator->add('name', 'draque', [
      'rule' => [$this, 'draque', 'もょもと'],
      'message' => 'ひらがなで入力してください',
    ]);
    return $validator;
  }

  public function draque($value, $value2) {
    return (bool) preg_match('/^[ぁ-ん]+$/u', $value);
  }
}

試しにこんな風に書いてみたりもしたんだけど、エラーになった。バリデーションのエラーではなくて、システムエラーね。

こういう書き方はできないみたいです。もし何かしらダイレクトに値を渡したいってなった場合は、どうすれば良いのかな?



カスタムファイルを作る

もう一つは、モデルやテーブルとは別のファイルに書く場合。

CakePHP2の場合は、Behaviorを使って書くことなんかもできます。

例えば、ValidateBehavior.phpというファイルを作ってみましょう。ファイルは、Modelフォルダの中にBehaviorというフォルダがあるので、その中に作成します。なければBehaviorフォルダを作ってください。

//ValidateBehavior.php
class ValidateBehavior extends ModelBehavior {
  public function draque(Model $model, $data) {
    $check = is_array($data) ? array_shift($data) : $data;
    return preg_match('/^[ぁ-ん]+$/u', $check);
  }  
}

//User.php
class User extends AppModel {
  public $actAs = array('Validate');
  public $validate = array(
    'name' => array(
      'rule' => 'draque',
      'message' => 'ひらがなで入力してください',
    ),
  );  
}

Behaviorを読み込むには、$actAsというメンバ変数を使います。

モデルに直接書く場合とほとんど一緒ですが、Behaviorに書いた場合、第一引数にはモデルのインスタンス(で良いのかな?)を渡しています。$dataは第二引数。もょもとを渡したければ、その後ろに引数を追加すればオーケーです。



CakePHP3の場合は、カスタムバリデーションというものを作成することができます。

Modelフォルダの下に「Validation」というフォルダを作り、その中にバリデーションファイルを作成します。ここでは「CustomValidation.php」というファイルを作ってみます。絶対に「Model/Validation」の下に作らないといけないわけではないですが、それについては後で。

//CustomValidation.php
namespace App\Model\Validation;
use Cake\Validation\Validation;

class CustomValidation extends Validation {
  //staticを忘れないように
  public static function draque($value) {
    return (bool) preg_match('/^[ぁ-ん]+$/u', $value);
  }
}

//UsersTable.php
class UsersTable extends Table {
  public function validationDefault(Validator $validator) {
    $validator->provider('custom', 'App\Model\Validation\CustomValidation');
    $validator->add('name', 'draque', [
      'rule' => 'draque',
      'provider' => 'custom',
      'message' => 'ひらがなで入力してください',
    ]);
    return $validator;
  }
}

CustomValidationを使えるようにするには、provider()というメソッドが必要になります。第一引数の「custom」は何でも良いです。add()メソッドの中で「’provider’ => ‘custom’」というのがありますが、ここに使用する文字列を指定しているだけなので、customの代わりにgundamとかにしたら、「’provider’ => ‘gundam’」とすれば良いだけ。

第二引数で、CustomValidationがあるパスを指定。こことnamespaceさえ正しい場所を指していれば、CustomValidation.phpはどこにあっても良いっぽいです。srcフォルダの直下に置いたら、こう書けば問題なく動く。

//CustomValidation.php
namespace App;
class CustomValidation extends Validation {
}

//UsersTable.php
class UsersTable extends Table {
  public function validationDefault(Validator $validator) {
    $validator->provider('custom', 'App\CustomValidation');
  }
}

カスタムを使う場合は、providerの指定を忘れないでください。これを忘れちゃうとエラーになっちゃう。

あともう一つ気をつけなきゃいけないのが、CustomValidation.phpの中にメソッドを書く場合は、staticでなければいけないということです。staticを書き忘れてると、やっぱりエラーになっちゃいます。



ヘルプミーその2

さて、ではここで、第二のヘルプミーポイント。

テーブルに直接書く場合もカスタムファイルを作る場合も、第二引数にコンテキストデータってのが入ってきます。省略して書くこともできるので、上記の例では書いてませんが、コンテキストデータを取得したければ、メソッドの第二引数を書いてください。

public function draque($value, $context) {
  return (bool) preg_match('/^[ぁ-ん]+$/u', $value);
}

$contextの中には、ここでは書ききれないくらいのいろんなデータが入ってます。テーブルオブジェクトも入ってる。

テーブルオブジェクトがあるなら、それ使っていろいろと処理を行えても良さそうなものなんですけど、変数がprotectedになってるせいか、カスタムの方では使えないものがあるんですよ。

んーと、例えばメールアドレスの重複登録を避けるような場合、入力されたアドレスがすでにデータベースの中にあるかどうか、チェックしたいじゃないですか。

//UsersTable.php
class UsersTable extends Table {
  public function validationDefault(Validator $validator) {
    $validator->add('email', 'exist', [
      'rule' => [$this, 'exist'],
      'message' => 'すでに登録されています',
    ]);
    return $validator;
  }

  public function exist($value, $context) {
    $table = $context['providers']['table'];
    $query = $table->find();
    $query->where([$context['field'] => $value]);
    $count = $query->count();
    return (bool) $count == 0;
  }
}

例えばこんなことができるんですよ。やってることは、入力されたメールアドレスがDBの中にあるかどうか判定して、1件以上あったら、重複エラーを出すというもの。いろいろと説明は省略しちゃいますが、「$context[‘providers’][‘table’]」にテーブルオブジェクトが入ってるので、それ使えば、データベースにアクセスしたりできます。CustomValidation.phpの方に書いても同じように動きます。

新規登録時はこれで良いと思います。でもデータ更新時は、自身のデータを参照してしまうと、必ず$countが1以上になっちゃいます。だから自分のIDだけは除くような処理が必要になると思うんですね。

//UsersTable.php
class UsersTable extends Table {
  public function validationDefault(Validator $validator) {
    $validator->add('email', 'exist', [
      'rule' => [$this, 'exist'],
      'message' => 'すでに登録されています',
    ]);
    return $validator;
  }

  public function exist($value, $context) {
    $table = $context['providers']['table'];
    $query = $table->find();
    $query->where([$context['field'] => $value]);

    //自身のIDを除外
    if(!empty($context['data'][$table->_primaryKey])) {
      $query->where([$table->_primaryKey.' !=' => $context['data'][$table->_primaryKey]]);
    }

    $count = $query->count();
    return (bool) $count == 0;
  }
}

例えばこんな風に書いたとします。「$table->_primaryKey」には、プライマリーキーが入ってます。プライマリーキーを絶対に「id」で固定とかなら良いんですけど、id以外にするかもしれない事態を想定して、ここではテーブルオブジェクトデータの中から参照できるように書いています。

この時、テーブルに直接書いた場合はエラーにならないんですけど、CustomValidation.phpの方に書くと、プライマリーキーが取れません。エラーになります。

だから書き方次第では、せっかくCustomValidation.phpを作って、独自メソッドをそこで一括管理しようと思っていたのに、テーブルの方にも書かなきゃいけなくなったりすることも、あったりなかったり?

そもそも設計がいけないって言われちゃえばそれまでなんですけど……こういう時はどうすりゃ良いんでしょうね?



コントローラーから呼び出す

CakePHP3のみの話になりますが、バリデーションは、コントローラーから直接使うこともできる。

さっきのdraqueメソッドを使ったエラーチェックを、コントローラーから使ってみましょう。

//UsersController.php
class UsersController extends AppController {
  public function add() {
    if($this->request->is('post')) {
      $user = $this->Users->newEntity($this->request->data);

      //エラーチェックメソッドの呼び出し
      $errors = $this->Users->validateDraque($user);
      
      if(!$errors) {
        //エラーなし
      } else {
        //エラーあり
      }
    }
  }
}

//UsersTable.php
use Cake\Validation\Validator;
class UsersTable extends Table {
  public function validateDraque($user) {
    $validator = new Validator();
    $validator->add('name', 'draque', [
      'rule' => function draque($value) {
        return (bool) preg_match('/^[ぁ-ん]+$/u', $value);
      },
      'message' => 'ひらがなで入力してください',
    ]);

    //エラーチェック
    $errors = $validator->errors($user->toArray());
    return $errors;
  }
}

エンティティを作成後、コントローラーで直接バリデーションメソッドを呼び出しています。やってることはほとんど一緒です。$validatorのインスタンスを新規に作成して、ルールの書き方は今までと一緒。最後にerros()というメソッドでエラーチェックを行っています。errors()には、エンティティのオブジェクトデータではなく、入力されたデータの配列を渡す必要があるので、toArray()メソッドを使って、データだけを渡すようにしています。

エラーがあると$errosにエラー内容が入ってくるので、それを使ってエラーメッセージを表示したり、登録処理を行ったりできます。



ヘルプミーその3

んじゃ、最後のヘルプミー。

入力されたデータを、エンティティを使って加工すること、あるじゃないですか。エンティティにメソッドを作って。

バリデーションのときにあのエンティティで作ったメソッドを使いたいなーって思ったことがあるんですけど、validationDefault()とかの中で、あれ使えないんですかね?

何を言ってるかってーと、例えば……データベースでは、名前はnameという一つのフィールドで持ってるんだけど、入力は姓と名を分けて入力するような場合を考えてみます。そんで入力する際に姓と名をくっつけて、フルネームにしてからエラーチェックを行う、みたいな。

//テンプレート
<?= $this->Form->text('sei') ?>
<?= $this->Form->text('mei') ?>

//コントローラー
$this->Users->newEntity($this->request->data);

//テーブル
public function validationDefault(Validator $validator) {
  $validator->add('name', 'draque', [
    'rule' => function draque($value) {
      return (bool) preg_match('/^[ぁ-ん]+$/u', $value);
    },
    'message' => 'ひらがなで入力してください',
  ]);

  return $validator;
}

//エンティティ
public function fullName() {
  $this->name = $this->sei.$this->mei;
}

いろいろとすっげー省略して書いちゃいましたが、おおよそこんな感じのことをやりたいとする。

ようは、エンティティのfullName()っていうメソッドを使って、エラーチェックを行う前に姓と名を合わせた値をnameに入れるような、そんなことができたら良いなと。その上で、nameフィールドに関するエラーチェックを行いたいなと。こんな程度のことだったら、エラーチェックを行った後に姓と名をくっつければ良いんですけど、もっと複雑なデータが飛んできた時などに、エラーチェック前にエンティティでごちゃごちゃやれたら便利かなーって。

そういうことって、やっぱり無理なんでしょうか。

僕の中では、今んとこ無理っていう結論が出てるので、もしこんな風にエンティティのメソッドを使うような時は、直接コントローラーからバリデーションメソッドを呼び出すやり方を使ってます。まずnewEntityでエンティティを作成して、そのデータを使ってエラーチェックする、みたいな。

newEntity()とかpatchEntity()って、エンティティを作成するメソッドだから、どっかでエンティティオブジェクトとかを受け渡すような処理が入ってて、こっちでこねくり回せば、それを使えるとかあっても良さそうなんですけどね。僕が調べた限りでは、見当たりませんでした。






ということで、バリデーションに関するお話でしたー。ここまで読んでくれた方、本当にお疲れさまです。でも、これくらい長くなるだろうという予想は最初からあったので、特に反省はしていません。すみません。てへぺろでござんす。

僕がいじった感じでは、ヘルプミーの項目で書いてるように、データベースへの接続を使ったエラーチェックとか、エンティティメソッドを使ったエラーチェックとか、いろんなパターンが必要になりました。そしたら、カスタムバリデーションを作ったり、テーブルに直接書いたり、コントローラーから直接呼び出すようなメソッド書いたりと、統一感のないコードになってしまいました。

CustomValidation.phpにすっきりまとめられれば、CakePHP3マジスゲーってなってたんですけどね。残念ながらそうはいかずでした。ぶっちゃけバリデーションに関しては、Behavior一つにまとめることができたCakePHP2の方が、僕個人としては使い勝手が良かったような……?

や、もちろんCakePHP3の仕様が悪いんじゃなくて、僕の考える仕様やら設計が悪い可能性も、おおいにあるんですけどね。こういう風にならないように設計を考えるべきなんだって言われてしまえば、ぐうの音くらいしか出ないですけどもね。ぱあの音も出るかな。ちょきの音(ry

でも実際のところ、エンティティを使ったエラーチェックとか行うパターンって、十分あり得ると思うんだけどなぁ……どうっすかね?



その他のCakePHP3を触ってみましたの記事はこちら
まとめという名の箸休め
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください