PHP
CodeIgniter3

不思議なvalidator(filter)順序

この記事はCodeIgniter Advent Calendar 2017の18日目の記事です。

CodeIgniter3のバリデーションライブラリはフィルタを兼ねています。入力に対して

  1. 加工する
  2. 検証する

という2つの役割を持ちます。
このとき、検証してから加工するのか、加工してから検証するのか、で微妙に挙動が変わってしまいます。この順序はほぼ直観どおりに動いてくれるのですが、がんばった使い方をするとハマる場合があります。この記事ではそのハマるパターンと対応方法を紹介します。

ノーマルな例

例として、電話番号のフォーム入力を考えてみます。
次のような仕様としましょう。

  1. 全角での入力は半角に変換します
  2. トリミングします(このとき全角空白は半角空白に変換されていてほしい:以下同じ)
  3. ハイフンは消しましょう
  4. 必須入力にしておきましょう
  5. 10桁または11桁を要求します(市外局番からの固定電話、もしくは携帯電話)
  6. 例示のため、IP電話(050)は拒否することにします

実装はこんな感じです。

<?php

class Welcome extends CI_Controller
{
    public function index()
    {
        $this->load->library('form_validation');

        $this->form_validation->set_rules('tel', '電話番号', 'mb_convert_kana[asKV]|trim|callback__remove_hyphen|required|min_length[10]|max_length[11]|callback__check_ipphone');

        $tel = '';
        if ($this->form_validation->run()) {
            $tel = $this->input->post('tel');
        }

        $this->load->view('form', array('tel' => $tel));
    }

    public function _remove_hyphen($str)
    {
        return str_replace('-', '', $str);
    }

    public function _check_ipphone($str)
    {
        if (mb_ereg('^050', $str)) {
            $this->form_validation->set_message('_check_ipphone', 'IP電話はダメです');
            return false;
        }
        return true;
    }
}

バリデーションルールは 'mb_convert_kana[asKV]|trim|callback__remove_hyphen|required|min_length[10]|max_length[11]|callback__check_ipphone' です。
この例はだいたい意図通り動きます(一部バグあり:後述)。パイプで指定しているので左側から実行される――ように見えます。

MY_Form_validationに共通化:うまく行かない例

ここでちょっと欲張ってみます。
ハイフンを削るような処理は他でも使いそうです。共通化しましょう。消す文字はハイフンに限らず、正規表現で指定できるようにしたいです。mb_ereg_replace()を使えば1行ですが、引数の順番上、そのままではバリデーションに使えません。そこで MY_Form_validation でルールを追加します。

<?php

class MY_Form_validation extends CI_Form_validation
{
    public function ereg_remove($str, $regex)
    {
        return mb_ereg_replace($regex, '', $str);
    }
}

バリデーションルールは次に差し替えます。

$this->form_validation->set_rules('tel', '電話番号', 'mb_convert_kana[asKV]|trim|ereg_remove[-]|required|min_length[10]|max_length[11]|callback__check_ipphone');

一見してうまく動くのですが、全角の「−050−99999999」がIP電話チェックをすり抜けてしまいます。
こうなると全角の「−−−−−−−−−−−」がどうなのよという不安になります。すり抜けます。実は最初の例でもダメです。

指定順ではなく特殊ルール順で処理される

この処理順はマニュアルに載っていないようなのですが、ソースコードにコメントがあります。

/**
 * Prepare rules
 *
 * Re-orders the provided rules in order of importance, so that
 * they can easily be executed later without weird checks ...
 *
 * "Callbacks" are given the highest priority (always called),
 * followed by 'required' (called if callbacks didn't fail),
 * and then every next rule depends on the previous one passing.
 *
 * @param   array   $rules
 * @return  array
 */
protected function _prepare_rules($rules)

気を利かせてくれている……らしいです。
このメソッドにより優先順位は入れ替えられ、このようになります。

  1. callback_で始まるルール、引数なしのcallableな関数は第1優先
  2. requiredは第2優先
  3. 上記2つ以外は第3優先(Form_validation組み込み、引数ありのcallableな関数

上記例remove_hyphen()は第2優先だったのがereg_remove()に置き換えたときに第3優先に下がってしまいました。callback__check_ipphoneのあとでハイフンを削るようになったので、先頭にハイフンが入ると正規表現^050に当たらなくなってしまいすり抜けてしまう、というのがバグの理由です。

また、最初の例では半角の「-----------」は必須チェックに引っかかるのですが、全角の「−−−−−−−−−−−」だと必須チェックが通らず半角になった「-----------」を受け取ってしまいます。これは本当はmb_convert_kana()が第1優先になるべきところと思いますが、引数の[asKV]のせいで第3優先になってしまいます。
これは本体側の不具合ではないかと思っています(直すと影響範囲でかいのでPullRequestはしていません……)。

対策例:順序入れ替えをやめる

パイプで書いているのに左から実行されないのが直観から外れる原因です。これをやめてしまえば動きます。

    protected function _prepare_rules($rules)
    {
        return $rules;
    }

こうすれば安直に左から実行されます。
デメリットは、CodeIgniter3に慣れている人に対して罠が仕掛けられてしまいます。どちらがいいかちょっと判別はしかねますが、慣れている人にコケてもらったほうがバグの深刻度は低くなるのではないかと思います。

そうはいってもだいたい動く

順序を入れ替えている理由は知りませんが、callbackが先に動くことでうまく動くことは経験的には多いように思います。Form_validation組み込みで使用するのはほとんどが検証ルールで、変換ルールは(MY_Form_validationを使わなければ)コールバック/callableな関数です。先に変換し、その後検証する、という順序はだいたいの場合合理的ですので、順序指定ミスしていてもうまく動いてくれます。

バリデーション順序はプログラマに制御させていいんじゃないかとは思いますが、不具合なく制御するとだいたい変換→検証の順になるので順序入れ替えは発生しないんですよね。

罠になるのは引数ありの関数がrequiredの後に来てしまうことですが、これも変換後が空文字列にならなければ問題になりにくいです。

特に対策しなくても「なんかrequiredが動かないぞ」というときに思い出せればよいかと思います。