Phalconでカスタムバリデーションを作る

# 前置き
Phalcon使い始めの頃、モデルのバリデーションのコードをみると、よく以下のようなコードを見かけた。

```
public function isValid()
{
        // account ID
        if($this->account_id === null || $this->account_id === '')
        {
            $this->appendMessage(new Message('account-id.required', 'account_id', 'accountIdRequired'));
        }
        else if(!is_numeric($this->account_id) || $this->account_id < 1)
        {
            $this->appendMessage(new Message('account-id.invalid', 'account_id', 'accountIdInvalid'));
        }
        else if()
        ....

        return $this->validationHasFailed() !== true;
}
```

せっかくPhalconがバリデーションを提供してくれているのに、なぜそれを使わないのか尋ねてみたところ、Phalconが提供しているバリデーションだけだとチェックできないことがあるうえ、Phalconのドキュメントがわかりにくくカスタムバリデーションの作り方もよくわからないし、、バリデーションをifで書いたほうが早いと言われた。

いいたいことはわかるのだが、こういったことを繰り返してコードのメンテナンス性が悪くなっていくのが目に見えているため、改善しようと思い立ち、その時に作成したカスタムバリデータークラスや、クラスを使わなくてもバリデーションをかけるようにするCustomクラスについて書こうと思う。

# 環境
PHP 5.6.26
Phalcon2.0.13

# やりたいこと
Phalconのバリデーションを使ってカスタムバリデーションを簡単にかけるようにする。
こんな感じでかけるようにしたい。

```
$this->validation = new Validation();
$this->validation->add('account_id', new PresenceOf(array('cancelOnFail' => true)))
                 ->add('account_id', new Length(0, 255, true))
                 ->add('account_id', new Custom(
                      function($validation, $attribute, $value){
                         if ($value > 0){
                            return true;
                         }
                         return false;
                      })
                   ); //0より大きい数字であること
```


# カスタムバリデーターのサンプル
カスタムバリデーターのサンプルクラス。このクラスはパラメータの長さをチェックする。
※実際、長さをチェックしたい場合は、PhalconがPhalcon\Validation\Validator\StringLengthを提供しているので、こちらを使うのが良い。

```
<?php

namespace Araiguma\Common\Validator;

use \Phalcon\Validation\Message;
use \Phalcon\Validation\Validator;
use \Phalcon\Validation\ValidatorInterface;

class Length extends Validator implements ValidatorInterface
{
    const MAX_LENGTH=10000;

    private $min;
    private $max;
    private $multiByte;

    public function __construct($min=0, $max=self::MAX_LENGTH, $multiByte=false)
    {
        $this->min = $min;
        $this->max = $max;
        $this->multiByte = $multiByte;  //multiByteがtrueの場合はmb_strlenを長さ計算に用いる

    }

    /**
     * Executes the validation
     *
     * @param Phalcon\Validation $validation
     * @param string $attribute
     * @return boolean
     */
    public function validate(\Phalcon\Validation $validation, $attribute)
    {
        $value = $validation->getValue($attribute);
        $length = $this->getLength($value);

        if ($value === true || $value === false) {
            //true, falseは文字列としてみる。
            $length = 4;
        }

        if ($this->min <= $length && $length <= $this->max)
        {
            return true;
        }

        $validation->appendMessage(new Message('invalidLength', $attribute, 'invalidLength'));
        return false;
    }

    private function getLength($value)
    {
        if ($this->multiByte)
        {
            return mb_strlen($value);
        }

        return strlen($value);
    }
}

```

# Customクラス
バリデーターのクラスをつくらなくても、コードをかけるように作ったCustomクラス。

```
<?php
/**
 * 任意のバリデーションを登録できるバリデータ。
 */

namespace Araiguma\Common\Validator;

use \Phalcon\Validation\Message;
use \Phalcon\Validation\Validator;
use \Phalcon\Validation\ValidatorInterface;

class Custom extends Validator implements ValidatorInterface
{
    private $func;

    /**
     * funcには以下の引き数を定義した関数を指定する。
     *
     * func($validation, $val, $attribute)
     * $validation: Phalcon\Validation
     * $val:        mix    'attribute'の値
     * $attribute:  string 'チェックするフィールド名'
     **/
    public function __construct($func)
    {
        $this->func = $func;
    }

    /**
     * Executes the validation
     *
     * @param Phalcon\Validation $validation
     * @param string $attribute
     * @return boolean
     */
    public function validate(\Phalcon\Validation $validation, $attribute)
    {
        $value = $validation->getValue($attribute);

        return call_user_func($this->func, $validation, $attribute, $value);
    }
}
```

# 最終的なコード例

上のクラスを利用して、以下のような感じでかけるようになった。

```
namespace Araiguma\Mvc\Model;

use Araiguma\Common\Validator\Custom;
use Araiguma\Common\Validator\Length;
use Phalcon\Mvc\Model\Message as Message;
use Phalcon\Validation\Message as ValidationMessage;
use Phalcon\Validation;
use Phalcon\Validation\Validator\PresenceOf;

class AraigumaModel extends BaseModel
{
    protected $account_id         = '';
    protected $file_name          = '';

   ...

   public function isValid()
   {
        $this->validation = new Validation();
        $params = array(
            'account_id'         => $this->account_id,
            'file_name'          => $this->file_name
        );

        $this->validation->add('account_id', new PresenceOf(array('cancelOnFail' => true)))
            ->add('account_id', new Custom(
                function($validation, $attribute, $value){
                    if ($value > 0){
                        return true;
                    }
                    return false;
                })
            ); //0より大きい数字であること

        $this->validation->add('file_name', new PresenceOf(array('cancelOnFail' => true)))
            ->add('file_name', new Length(0, 255, true));
            ->add('file_name', new Custom(
                function($validation, $attribute, $value){
                    if (self::findFirstByFileName($value)) {
                        $validation->appendMessage(new ValidationMessage('同じファイル名がすでに存在します。', $attribute, 'duplicateFileName'));
                        return false;
                    }

                    return true;
                })
            ); // 同じファイル名がDBに存在しているか

        $messages = $this->validation->validate($params);
        foreach ($messages as $message) {
            $this->appendMessage(new Message( $message->getMessage() , $message->getField(), $message->getType()));
        }

        return !$this->validationHasFailed();
    }
}
```

# 結果
プロジェクト的には、のちの資産になるため各バリデーションで独自クラスは毎回作るようにしたいが、どうしてもそれが大変というときはCustomクラスを今でも使ってます。

# 課題
既存の各バリデーションも全部書き換えたいが、そちらにはなかなか手をつけられない...

# テスト
以下に各クラスのphpunitのテストを載せておく。
[LengthTest.php](https://gist.github.com/araiguma/5f9934caebf93edb11f23f159dbf1228)
[CustomTest.php](https://gist.github.com/araiguma/3b2dccc924c99e055227b2c5d05a2398)

# 参考
[Phalcon Validation]
(http://phalcon-docs-ja.readthedocs.io/ja/stable/reference/validation.html)