【CakePHP2.x】alphaNumeric バリデーションで日本語が弾かれない問題の対応

# 検証バージョン
CakePHP2.9.7
PHP7.1.2

# 参考
[CakePHP2.xでvalidationルールで日本語がチェックされない - Qiita](http://qiita.com/yshishido/items/9e71a6233058600ec2ea)
[(CakePHP 2.x) validationルールのalphaNumeric日本語対応する方法 - 日々の覚書…日常のことも少しだけ](http://www.tailtension.com/cakephp/1112/)
[日本語に絡むUnicodeブロックとスクリプト(正規表現) - module.jp](http://module.jp/blog/regex_unicode_prop.html)

# 問題となる現象
Modelのバリデーションルールとして`alphaNumeric`を適用している場合、日本語が弾かれません。

```php:HogeModel.php
public $validate = [
  'hoge' => [
    'hoge' => [
      'rule' => ['alphaNumeric'],
      'message' => '英数字のみ入力可能です。',
    ]
  ]
];
```

この状態で `'ほげ'` とか投げると普通に通ってしまいます。

# alphaNumeric は何をしているのか
- チェック対象の値が空と見なされる値で、かつ`'0'`以外なら弾く
- チェック対象の値がスカラ型でなければ弾く
- `preg_match()` に渡されるマッチパターンが`string`でなければ弾く
- パターンマッチングしなければ弾く(*1)

```php:lib/Cake/Utility/Validation.php
/**
 * $check can be passed as an array:
 * array('check' => 'valueToCheck');
 * 
 * @param string|array $check Value to check
 * @return bool Success
 */
public static function alphaNumeric( $check ) {
  if ( empty( $check ) && $check != '0' ) {
    return false;
  }
  return static::_check( $check, '/^[\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}]+$/Du' );
}

protected static function _check( $check, $regex ) {
  if ( is_string( $regex ) && is_scalar( $check ) && preg_match( $regex, $check ) ) {
    return true;
  }
  return false;
}
```

(*1) ~~`_check()`に渡されている正規表現の意味が理解できないので助けてください~~
正規表現の理解には [regex101.com](https://regex101.com/r/lG2nG9/1) が便利そうです。

メソッドのコメントを読む限りでは、パラメータとして`string`または`array`を受け取る想定のようです。
まだ納得できないので、もう少しソースを追ってみます。

```php:lib/Cake/Model/Validator/CakeValidationRule.php
public function process($field, &$data, &$methods) {
  // 省略

  if (isset($methods[$rule])) {
    $this->_ruleParams[] = array_merge($validator, $this->_passedOptions);
    $this->_ruleParams[0] = array($field => $this->_ruleParams[0]);
    $this->_valid = call_user_func_array($methods[$rule], $this->_ruleParams);

  } elseif (class_exists('Validation') && method_exists('Validation', $this->_rule)) {
    // alphaNumeric はここを通るみたい!
    $this->_valid = call_user_func_array(array('Validation', $this->_rule), $this->_ruleParams);

  } elseif (is_string($validator['rule'])) {
    $this->_valid = preg_match($this->_rule, $data[$field]);

  } else {
  // 省略
}
```

`$this->_rule` に `'alphaNumeric'`, `$this->_ruleParams` に `array( 'ほげ' )` が渡ってきました。
`call_user_func_array()` で `Validation::alplaNumeric()` を呼んでいるので、結局パラメータは配列ではなく、文字列
 `'ほげ'` として渡ることになります。

[PHP: call_user_func_array - Manual](http://php.net/manual/ja/function.call-user-func-array.php)

# 対応方法
`AppModel.php` で上書きするのが一般的な対処法のようですが、以下のように挙動が変わります。

- **メソッドを上書きする場合 `CakeValidationRule.php` では最初のif文を通るようになる**
- **最初のif文を通ると `$this->_ruleParams` の構造が変化する（以下参照）**

```php:配列の内容が増えました
array(
  (int) 0 => array(
    'hoge' => 'ほげ'  // フィールド名 => 値
  ),
  (int) 1 => array(
    'rule'       => array(),
    'required'   => null,
    'allowEmpty' => false,
    'on'         => null,
    'last'       => true,
    'message'    => '英数字のみ入力可能です。'
  )
)
```

~~rule が `array( 'alphaNumeric' )` じゃなくて空配列になっているのが気になる~~
上書き = ユーザ独自定義になるので、拡張性を高めるための処置と思われます。

これまでの検証を踏まえて、以下のような実装でどうでしょうか。

```php:AppModel.php
/**
 * 英数字バリデーション
 * 単純にバリデーションを行いたい場合（引数を array 固定）
 * 
 * @param  array $check
 * @return bool
 */
public static function alphaNumeric( array $check ) {
  $check = array_values( $check );
  $check = $check[0] ?? null;

  if ( empty( $check ) && $check != '0' ) {
    return false;
  }

  if ( !is_scalar( $check ) || !preg_match( '/\A[a-zA-Z0-9]+\z/u', $check ) ) {
    return false;
  }

  return true;
}

/**
 * 英数字バリデーション
 * 引数として string, array を想定する場合
 * 
 * @param  string|array $check
 * @return bool
 */
public static function alphaNumeric( $check ) {
  if ( is_array( $check ) ) {
    $check = array_values( $check );
    $check = $check[0] ?? null;
  }

  if ( empty( $check ) && $check != '0' ) {
    return false;
  }

  if ( !is_scalar( $check ) || !preg_match( '/\A[a-zA-Z0-9]+\z/u', $check ) ) {
    return false;
  }

  return true;
}
```

結局参考リンクの実装とあまり変わらないです。
対策方法として、ほかに良い方法があればアドバイスください。