Fuelで取り回しやすいValidatorを作る

# 本稿の内容を三行で

- FuelPHPには入力値を検証できる **Validation クラス**がある
- 直感的に使えるが呼び出しの呪文が長いし、ルールべた書きではミスを誘発しそう
- なので、**フィールド名を指定したらルールが自動的に補完される**ような Validator を作ってみた

以下本編です。

# 生値は生肉
ユーザー入力機能のあるWebサイトの作成・運営はたいへんです。

悪意があるものならXSSやらHTMLi/SQLiやら、悪意がなくても想定外の値でDbエラー（受取先でタイプヒンティングしてるはずなのでだいたいTypeErrorがthrowされる）が起きたりとか、まぁとにかくナマの入力値は**ナマ**だけあって、そのままプログラムに食べさせるのはとっても危険です。

入力フォームにjsで制御を入れて、意に沿わない値が送信されるのを防ぐことはもちろん大切ですが、受け取ったプログラム側でもチェックを行うことは必須です。特にPHPでは、良きにつけ悪しきにつけ、型の扱いが厳密でないので、想定した通りの値が渡されているか、いっそう注意する必要があると言えるでしょう。


# Validator を使おう

というわけで、プログラム側に渡された値が、その後の処理に回してよいものなのか、を確認・検証する（validate）手順が必要になります。

そして、FuelPHP フレームワークには、

 - 簡単に呼び出せて
 - 直感的に扱えて
 - 備え付けの定義で割と何とかなる

**Validation クラス**が用意されています。

## Validation クラスの使い方

例えば、
「会員の『生まれ年』は数字4桁（西暦）で受け付けたい、保存先は SMALLINT だし」
という要件に対しては、以下のように Validation クラスを利用することができます。

```php
// Validation インスタンス生成
$validator = Validation::forge();

// フィールドを追加(短縮構文を使用)
// 第1引数はフィールド名。検証にかける値を、$_POST の中からこの名称(添字)で拾う。値はフィールド名に対して個別の指定も可能(後述)
// 第2引数はラベル。デフォルトのエラーメッセージに使われたりする
// 第3引数はルール。プリセットで用意される他、自作もできる。短縮構文でなければ、関数を指定することも可能
$validator->add_field(
    'age',
    'age',
    'required|match_pattern[/\A\d+\z/]|exact_length[4]'
);

// 検証を実行
// 検証に失敗した場合、run() の戻り値は false になる
if ( !$validator->run() ) {
    throw new \Exception('invalid value.');
}

// ここを通るということは検証が完了しているので、値を使ってよい
$age = $_POST['age'];
```

検証対象の値を POST から拾うのではなく、個別に指定する場合は、`run()` にキーと値をセットした配列を渡します。

```php

// ユーザーの入力値を取得
$age = $_POST['age'];

$validator = Validation::forge();
$validator->add_field('age', 'age', 'required|match_pattern[/\A\d+\z/]|exact_length[4]');

// 検証対象の値を配列で指定
$validatee = array(
    'age' => $age,
);

// 値をセットした配列を run() に渡す
if ( !$validator->run( $validatee ) ) {
    throw new \Exception('invalid value.');
}

// 検証完了。以後は $age を検証された値として利用可能
// あるいは、以下のようにインスタンスから検証済みの値を取得することも可能
// フィールド名を指定しない場合は、フィールド名を添字とする連想配列で全件取得
// 検証ルールで trim を行っている場合などは、validated() から取得したほうがよい
$age = $validator->validated('age');
```

また、検証に失敗した場合、`error()` でその失敗理由を取得することができます。

```php
if ( !$validator->run() ) {
    // 戻り値は、フィールド名を添字、検証失敗のオブジェクトを値とする連想配列
    // validated() と同様に、フィールド名を指定して、特定のフィールドの結果を取得することも可能
    $valError = $validator->error();

    // 'age'フィールドの失敗については以下のように取得できる
    $value   = $valError['age']->value;  // 対象のフィールドに設定した検証値
    $rule    = $valError['age']->rule;   // 失敗と判断された際の検証ルール名
    $params  = $valError['age']->params; // rule の追加パラメータ(match_patternの[/\A\d+\z/]、exact_lengthの[4]が相当)
    $message = $valError['age']->get_message(); // エラーメッセージ
}
```

## 実際に組み込んでみる

上掲のように、使い方が明瞭で、Validationに失敗した際もその後の挙動を実装者が設定できることは大変便利です。

しかし一方で、呼び出しやすいが故に、

- `forge()` から `run()` までを検証の度に唱えなければならない
- 実装者・実装箇所によって、**同じフィールドに異なるルールを設定**できてしまう

というのが厄介で、例えば「電話番号」をユーザーに入力させる箇所が2箇所あった場合、その2箇所で表記（特に検証ルール）を統一しなければなりません。ここがまずヒューマンエラーが起きやすい。
これが守られない場合、**片方はハイフン入りの表記を許容しているが、もう片方はしていない**ということも起こり得る（そして、電話番号は殆どがゼロ始まり故に string で扱うはずなので、混在したままになりやすい）わけです。

なので、

- 同名のフィールドは必ず同じルールが適用されて
- `execute()` の1行で検証できるような

検証装置を作ってみました。


# Validator を自作する

## 検証装置

まずは実際に検証を行うツール部分からご紹介します。
渡すパラメータによって検証する値を切り替えることができます。

### ソース

```php:fuel\app\classes\lib\util\validator.php

use Fuel\Core\Validation;

class Util_Validator
{

    /**
     * validationの実行
     * @param string $type ルールセットの一意識別子
     * @param array $params 検証する値のセット
     * @param string $fieldset フィールドセット名(optional)
     * @return array 検証済みの値のセット
     */
    public static function execute( string $type, array $params, string $fieldset = 'default' )
    {
        // 指定の $type がルールリスト内に見当たらない場合はエラー
        if ( !array_key_exists( $type, \Common_Constants_Validation::TYPE_RULE_LIST ) ) {
            throw new \Exception( 'not found in TYPE_RULE_LIST' );
        }

        // 指定の $type に設定されているルールセットを参照
        $valList = \Common_Constants_Validation::TYPE_RULE_LIST[$type];

        // インスタンスを生成
        $val = Validation::forge( $fieldset );

        // 新たに定義した拡張ルールを読込
        $val->add_callable(new \Common_Logic_Validationrule());

        foreach( $valList as $field => $rule ) {
            $val->add_field( $field, $field, $rule );
        }

        if ( !$val->run( $params ) ) {
            throw new \InvalidValueException( $val->error() );
        }

        // 検証済みの値を返却
        return $val->validated();
    }
}
```

### 解説

- 第一引数を添字とするルールセットを一覧から取得
- フィールド名と対応するルールをセットしたフィールドを追加
- 第二引数を対象として検証実行
    - 失敗したら専用の例外を投げる（例外の拡張、エラーハンドリングについてはまた長くなるので割愛）
    - 成功したら検証済みの値を返却

という流れです。

ルールセットを事前に設定しておくことで、呼び出し元に対して必要十分なフィールドが生成されるようになっています。
また、ルールセット内で各フィールドとルールの一対一対応を定義しているので、同じフィールドなのに異なるルールで検証される可能性を減らしています。

第二引数に、ルールセットに定義されていないフィールドの値、例えば **"submit"** などが存在していた場合でも、`$val->run()` および `$val->validated()` では同名のフィールドが定義されていないため、検証・返却されなくなっています。

逆に、ルールセットに定義されているのに、第二引数に対応する値が存在しない場合、**required ルールを持つフィールドでは検証失敗**となります。
第二引数に `Input::post()` をまるごと渡す（＝第二引数に値が揃っている保証がない、添字がフィールド名と異なる可能性がある）ような実装―それはプリセットの `run()` と同じ振る舞いなのですが―の場合、ここがために検証に失敗することがあり得ます。`<form>` から送る **"name"** が正しいか、不足している値はないか等ご確認ください。


## フィールド名とルールセットの一覧

例として、住所情報をユーザーに入力していただく際の検証を想定し、

- 氏名
- 氏名読みカナ
- 郵便番号
- 住所1
- 住所2（ビル・マンション名等）
- 性別（1=男性、2=女性、3=その他・回答辞退）

を確認できるようにします。

※あくまでサンプルとしてわかりやすいものにしただけですので、性別を問うことの是非とかその辺の Political Correctness の検証は一旦脇に置いてください。

### ソース

```php:...\common\constants\validation.php

class Common_Constants_Validation
{
    // ルールセットの一意識別子
    const TYPE_CUSTOMER_INFO = 'customer_info'; // ユーザー情報入力

    // フィールド名
    const FIELD_NAME         = 'name';       // 氏名
    const FIELD_NAME_KANA    = 'nameKana';   // 氏名読みカナ
    const FIELD_POSTAL_CODE  = 'postalCode'; // 郵便番号
    const FIELD_ADDRESS      = 'address';    // 住所1
    const FIELD_BUILDING     = 'building';   // 住所2
    const FIELD_GENDER       = 'gender';     // 性別

    // ルール
    const RULE_REQUIRED      = 'required'; // 必須
    const RULE_PATTERN              = 'match_pattern';
    const RULE_LENGTH_MIN           = 'min_length';
    const RULE_LENGTH_MAX           = 'max_length';
    const RULE_LENGTH_EXACT         = 'exact_length';
    const RULE_NUMERIC_BETWEEN      = 'numeric_between';
    const RULE_PATTERN_WORD_DBKATAKANA = 'match_pattern_word_dbkatakana';
    const RULE_PATTERN_WORD_EXSYMBOL = 'match_pattern_word_exsymbol';

    const RULE_NAME          = 'max_length[32]|match_pattern_word_exsymbol[/\-\s/]'; // 32文字以内、ハイフン・空白文字を含む記号を不許可(拡張ルール、詳細は後述)
    const RULE_NAME_KANA     = 'max_length[32]|match_pattern_word_dbkatakana'; // 32文字以内、全角カタカナのみ許可(拡張ルール、詳細は後述)
    const RULE_POSTAL_CODE   = 'match_pattern[/\A\d+\z/]|exact_length[7]'; // 数値、7文字
    const RULE_ADDRESS       = 'max_length[64]|match_pattern_word_exsymbol'; // 64文字以内、記号を不許可
    const RULE_BUILDING      = 'max_length[128]|match_pattern_word_exsymbol'; // 128文字以内、記号を不許可
    const RULE_GENDER        = 'match_pattern[/\A\d+\z/]|exact_length[1]|numeric_between[1,3]'; // 数値、1文字、1～3の間

    // ルールセット
    const TYPE_RULE_LIST = array(

        self::TYPE_CUSTOMER_INFO => array(
            self::FIELD_NAME        => self::RULE_REQUIRED.'|'.self::RULE_NAME,
            self::FIELD_NAME_KANA   => self::RULE_REQUIRED.'|'.self::RULE_NAME_KANA,
            self::FIELD_POSTAL_CODE => self::RULE_REQUIRED.'|'.self::RULE_POSTAL_CODE,
            self::FIELD_ADDRESS     => self::RULE_REQUIRED.'|'.self::RULE_ADDRESS,
            self::FIELD_BUILDING    => self::RULE_BUILDING,
            self::FIELD_GENDER      => self::RULE_REQUIRED.'|'.self::RULE_GENDER,
        ),

    );
}
```

### 解説

- ルールセットの一意識別子（→ `Util_Validator::execute()` の第一引数）
- フィールド名（→検証完了後、呼び出し元の変数名となる）
- ルールの組み合わせ（同じフィールドでも場所によって必須の是非が分かれる可能性を考慮し、requiredは別建て）
- フィールド名とルールの対応をまとめたルールセット

を定数で管理し、誤記や検証内容の不一致などの人的ミスを防ぐようにします。

**building** フィールドのルールには **required** を付与していないので、「ビル・マンション名」は入力されない場合でも検証に成功します。


## 拡張ルール

上記のルールのうち、「拡張ルール」と記載した **match_pattern_word_exsymbol**, **match_pattern_word_dbkatakana** については、次に紹介するような方法で、新しく自作したものになります。

### ソース

```php:...\common\logic\validationrule.php

class Common_Logic_Validationrule
{

    /**
     * Validate match pattern (exclude symbol)
     *
     * @param string $val
     * @param string $addPattern(optional)
     * @return bool
     */
    public function _validation_match_pattern_word_exsymbol($val, $addPattern = null)
    {
        $isEmpty = ($val === false or $val === null or $val === '' or $val === array());
        $isValid = false;

        if ( !$isEmpty ) {
            // 「/」「!」「"」「#」などの記号が含まれていないことを確認
            $pattern = '/\A[^\\!"#$%&\'()=~\|`{\[;+:*}\]<,>?\/@_.\^\\\\]+\z/';

            // 第二引数が渡されている場合は、それも判定対象に追加する
            if ( !empty( $addPattern ) ) {
                $pattern = '/\A[^\\!"#$%&\'()=~\|`{\[;+:*}\]<,>?\/@_.\^\\\\'.trim( $addPattern, '/' ).']+\z/';
            }

            $isValid = preg_match( $pattern, $val );
        }

        return ( $isEmpty || $isValid );
    }

    /**
     * Validate match pattern (doublebyte katakana)
     *
     * @param string $val
     * @param string $addPattern(optional)
     * @return bool
     */
    public function _validation_match_pattern_word_dbkatakana($val, $addPattern = null)
    {
        $isEmpty = ($val === false or $val === null or $val === '' or $val === array());
        $isValid = false;

        if ( !$isEmpty ) {
            // 全角カタカナのみで構成されていることを確認
            $pattern = '/\A([ァ-ヶー])+\z/u';
            if ( !empty( $addPattern ) ) {
                $pattern = '/\A([ァ-ヶー])|'.trim( $addPattern, '/' ).')+\z/u';
            }

            $isValid = preg_match( $pattern, $val );
        }

        return ( $isEmpty || $isValid );
    }

```

### 解説

渡された値が空相当（`$isEmpty`）か、設定した条件に適う（`$isValid`）際、 **true** が返却されます。
逆に言えば、空でない値で、設定した条件を満たさない場合、**false** が返却されて検証失敗となります。

こうした自作のルールは、「検証装置」の項で
`$val->add_callable(new \Common_Logic_Validationrule());`
としているように、Validationインスタンスに対して設定することで利用可能になります。

この設定の仕方であれば、静的・非静的を問わず、定義したメソッドをルールとして使用可能です。

拡張ルールを作成する際に意識する点をまとめておきます。

- 作成するメソッド名には **\_validation_** の接頭辞が必要で、メソッド名からこの接頭辞を除いた名称が、ルール名となる
- 拡張ルールの中では、正規表現等による検査以外に、DBに問い合わせての検証も可能である（例えばユーザーIDのような一意の値の重複確認や、定期的に更新される使用禁止語句をDB管理にしておき、ルールの中から参照して判定するなど）
- 空の入力に対しては **true** が返却されるようにしておく（空の入力を弾くのは **required** の役目である）

詳しくは[公式ドキュメント](http://fuelphp.jp/docs/1.9/classes/validation/validation.html#extending_validation)をご参照ください。


## 呼び出し方

### ソース

```php:...\controller\customerinfo.php

use Fuel\Core\Input;

class Controller_Customerinfo
{

// ~~ 中略 ~~

    /**
     * ユーザー入力の確認
     */
    public function action_confirm()
    {
        try {

            // 入力された値の検証
            // 何れかの値で検証に失敗した場合、InvalidValueException が投げられる
            $valList = \Util_Validator::execute(
                \Common_Constants_Validation::TYPE_CUSTOMER_INFO,
                array(
                    \Common_Constants_Validation::FIELD_NAME        => Input::post('name'),
                    \Common_Constants_Validation::FIELD_NAME_KANA   => Input::post('nameKana'),
                    \Common_Constants_Validation::FIELD_POSTAL_CODE => (string)str_replace('-', '', Input::post('postalCode')),
                    \Common_Constants_Validation::FIELD_ADDRESS     => Input::post('address'),
                    \Common_Constants_Validation::FIELD_BUILDING    => Input::post('building'),
                    \Common_Constants_Validation::FIELD_GENDER      => (int)Input::post('gender'),
                )
            );

            // フィールド名を変数名にして値を格納
            foreach( $valList as $key => $value ) {
                ${$key} = $value;
            }

        } catch( \Exception $e ) {
            // エラー時の処理。ここでは記載を省略            
        }

        // 検証完了後の処理。省略
    }
```

### 解説

- 第一引数には、参照するルールセットの一意識別子
- 第二引数には、ルールセットに設定したフィールド名を添字、そのフィールドに当てはめて検証したい入力を値とする連想配列
- （一度の処理の中で複数回呼び出す場合のみ）第三引数には、連番など、任意の重複しない値

を渡して、`Util_Validation::execute()` を実行します。

第二引数の値には今回は全て POST 由来のものを使用していますが、GET や、その他処理で出来た値を置くこともできます。

戻り値には検証済みの値が配列で格納されているので、フィールド名を変数名として値を定義します。今回の例では、`$name`, `$nameKana`, `$postalCode`, `$address`, `$building`, `$gender` が検証済みの値で定義されます。

※統合開発環境で開発を行っていると、その後使用しようとする時に「未定義の変数」警告が出ます。可読性などを考慮する場合、`$valList` から取り出して明示的に定義してください。

一度の処理の中で複数回呼び出す場合に、第三引数の設定が必要なのは、同じフィールドセット名の Validation クラスを `forge()` することはできないからです。
例えば複数件の住所情報の配列が渡ってくることがあるとして、それを1件ずつ検証にかける場合、`foreach()` で回しながら `execute()` することになるかと思います。その際、第三引数を指定しないと、フィールドセット名 **'default'** の Validation インスタンスが複数回生成されてしまうため、エラーになります。
重複しない値（この例であればループ数など）を第三引数として渡せば、1件ごとに検証が行えるようになります。

ちなみに、`\Util_Validation::execute()` と呼び出す場合には、**bootstrap.php** への追記が必要です。

```php:fuel\app\bootstrap.php

// Bootstrap the framework DO NOT edit this
require COREPATH.'bootstrap.php';

\Autoloader::add_classes(array(
	// Add classes you want to override here
	// Example: 'View' => APPPATH.'classes/view.php',
        'Util_Validation' => APPPATH.'classes/lib/util/validation.php',
));
```

# まとめ

ナマの値はどんどん焼べましょう。


# 参考

[Validation クラス - FuelPHP ドキュメント](http://fuelphp.jp/docs/1.9/classes/validation/validation.html)
[Core の拡張 - FuelPHP ドキュメント](http://fuelphp.jp/docs/1.9/general/extending_core.html#extend_and_replace)