PHP 5.6と7の新機能を使った画期的バリデータの実装

以前、[private/protectedなプロパティを外部から読み込み可能にする](http://qiita.com/tadsan/items/2049243e47495764b191)を書いたとき、記事のコメントにて「コレクション(配列)内の要素の型を検証したいよー」「でもそうすると全要素を検証しなくちゃいけないから実行時のパフォーマンスの問題があるよね」って話をしました。

## 気付いてしまった

[PHP: 関数の引数 - Manual](http://php.net/manual/ja/functions.arguments.php)を読んでた私は気付いてしまったのです。PHP 5.6で実装された機能を巧みに利用すればバリデータが実装可能なことに。

……そうです。<dfn>**可変長引数**</dfn>です。

## PHPの可変長引数とは

PHP 5.6で追加された可変長引数は、以前の文法に`...`を追加することで**0個以上**の値をまとめて受け取れるようにしたものです。

```php
function f($a, ...$b)
{
    var_dump($a, $b);
}
```

呼び出した結果は以下のようになる。

```php
f("a")
// =>
// string(1) "a"
// array(0) {
// }

f("a", "b")
// =>
// string(1) "a"
// array(1) {
//   [0]=>
//   string(1) "b"
// }

f("a", "b", "c", "d")
// =>
// string(1) "a"
// array(3) {
//   [0]=>
//   string(1) "b"
//   [1]=>
//   string(1) "c"
//   [2]=>
//   string(1) "d"
// }
```

ご覧の通り、「一個以上」ではなく「0個以上」です。

## 可変個引数の受け渡し

何かの事情により、引数に受け渡すべき値を配列で受け取ったとき、*愚直には*以下のように書きますね。

```php

$a = array_values($args);
$count = count($args);

if ($count === 1) {
    f($a[0]);
} elseif ($count === 2) {
    f($a[0], $a[1]);
} elseif ($count === 3) {
    f($a[0], $a[1], $a[2]);
} elseif ($count === 4) {
    f($a[0], $a[1], $a[2], $a[3]);
} elseif ($count === 5) {
    f($a[0], $a[1], $a[2], $a[3], $a[4]);
} elseif ($count === 6) {
    f($a[0], $a[1], $a[2], $a[3], $a[4], $a[5]);
}
```

いや、これはあんまりにも不便すぎる…

なのでふつうは[`call_user_func_array()`](http://php.net/manual/function.call-user-func-array.php)を使って、こう書きます。

```php
call_user_func_array('f', $args);
```

さて、PHP 5.6以降では、さらに短く書けます。

```php
f(...$args);
```

短い！ べんり！

> 関数を呼び出すときに `...` を使うと、 配列変数や[`Traversable`](http://php.net/manual/class.traversable.php)クラスを引数リストに含めることができます。

しれっと、`Traversable`の名前が出てます。これが何を意味するかといふと、`$args`の実装は[`ArrayObject`](http://php.net/manual/class.arrayobject.php)とかでも良いのです。すごいじゃんPHP 5.6。

## タイプヒント、改め型宣言

引数には、クラス名を指定して`UserModel $user`とか`array $users`とか書くことができます。では、可変長引数と組み合せると？

マニュアルには以下のようにあります。

> `...`トークンの前に、 タイプヒント を付加することもできます。 タイプヒントがある場合、`...`が取り込むすべての引数はそのヒントに従わなければいけません。

なるほど！ べんり！ 超べんり!

## つまり…？

これだけで「配列要素の型チェックができる」ってことです。

```php
class User
{
}

$v = function (User ...$users) {};

$users = [new User("taro"), new User("jiro"), new User("saburo")];
$v(...$users);
```

検証に失敗する、つまり不正な要素が含まれてたら`TypeError`が投げられます。

```php
$null_users = [new User("taro"), null, new User("saburo")];
$v(...$null_users);
// PHP Fatal error:  Uncaught TypeError: Argument 2 passed to {closure}() must be an instance of User, null given ...
```

## 任意の型を検証する

PHPで動的な函数生成といったら、上のように`function`式を利用する方法がポピュラーですね。しかし今回のようにライブラリとして**タイプヒントに任意の型を指定したい**と思ったとき、`function`式だけではタイプヒントを動的に変更することは不可能です。ただし、`eval`を除く。

……`eval`を除く? みなさん、ちょっとセキュアな[モダンPHP](http://qiita.com/tadsan/items/157969b338fd8b782b21)に毒されすぎなんじゃないですか？？？

つまり[`create_function()`](http://php.net/manual/ja/function.create-function.php)の存在を思ひ出すのです。

> **警告** この関数は、内部的に `eval()` を実行しているので、 `eval()` と同様にセキュリティ上のリスクがあります。 さらに、パフォーマンスやメモリ使用効率の面でも問題があります。
> PHP 5.3.0 以降を使っている場合は、この関数ではなく、ネイティブの 無名関数 を使うべきです。

コワイ！ 正常なセキュリティ意識があれば絶対に近付きたくないやつです。

しかし、これなら…?

```php
$f = create_function($type . ' ...$args', 'return true;');
```

これなら、ちょっとは大丈夫な気がする…? `$type`にユーザー入力が入らないように注意すれば、まあ大丈夫そう。 **(保障はできない！)**

## 実装

[BaguettePHP/collection-validator](https://github.com/BaguettePHP/collection-validator)を用意しました。

比較のために作ったクラスはみっつ。`Each`ってついてるクラスは配列の中身を一個一個チェックするやつ。

* [TypeHint.php](https://github.com/BaguettePHP/collection-validator/blob/master/src/TypeHint.php): 可変長引数とタイプヒントを使った実装。PHP7必須。
* [EachIf.php](https://github.com/BaguettePHP/collection-validator/blob/master/src/EachIf.php): ループ内で自明な分岐を何回か繰り返すやつ
* [EachCallable.php](https://github.com/BaguettePHP/collection-validator/blob/master/src/EachCallable.php): 自明な分岐を繰り返さないように`callable`オブジェクトをキャッシュするやつ

今回の記事の本題の`TypeHint`クラスの実装です。ほかのクラスはGitHubで読んでね。

```php:TypeHint.php
<?php
declare(strict_types=1);
namespace Teto\CollectionValidator;

/**
 * @license WTFPL
 */
class TypeHint
{
    private static $functions = [];
    private $type;

    /**
     * @param  string   $type
     * @return TypeHint
     */
    public function __construct($type)
    {
        $this->type = ltrim($type, '\\');
    }

    /**
     * @return bool
     */
    public function validate($collection): bool
    {
        try {
            $is_valid = self::getValidateFunction($this->type)(...$collection);
        } catch (\TypeError $e) {
            $is_valid = false;
        }
        return $is_valid;
    }

    /**
     * @return \Closure
     */
    public static function getValidateFunction($type)
    {
        if (!isset(self::$functions[$type])) {
            self::$functions[$type] = create_function($type . ' ...$args', 'return true;');
        }
        return self::$functions[$type];
    }
}
```

さて、三種類のクラスのベンチマークをとってみよう。あと、テスト対象は「10要素の配列」と「1000要素の配列」にしておく。

```
% php --version
PHP 7.0.3 (cli) (built: Feb  6 2016 03:16:24) ( NTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies

% php ./vendor/bin/athletic -p ./tests/

Teto\CollectionValidator\StringBench
    Method Name             Iterations    Average Time      Ops/second
    ---------------------  ------------  --------------    -------------
    foreach_callable_10  : [     1,000] [0.0000009810925] [1,019,271.93196]
    foreach_if_10        : [     1,000] [0.0000008912086] [1,122,071.69609]
    typehint_10          : [     1,000] [0.0000005047321] [1,981,248.93718]
    foreach_callable_1000: [       100] [0.0000671696663] [   14,887.67259]
    foreach_if_1000      : [       100] [0.0000663876534] [   15,063.04184]
    typehint_1000        : [       100] [0.0000161123276] [   62,064.27937]

```

いいね！ `typehint`速いよ！ これなら勝てる！ `Average Time`は低いほど、`Ops/second`は高いほど性能が良いことになる。

……しかし、これは型チェックの対象が全て妥当な場合だ。では、不正な要素が入った配列では？

```
% php --version
PHP 7.0.3 (cli) (built: Feb  6 2016 03:16:24) ( NTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies

% php ./vendor/bin/athletic -p ./tests/

Teto\CollectionValidator\StringFailBench
    Method Name             Iterations    Average Time      Ops/second
    ---------------------  ------------  --------------    -------------
    foreach_callable_10  : [     1,000] [0.0000004541874] [2,201,734.38320]
    foreach_if_10        : [     1,000] [0.0000001759529] [5,683,338.75339]
    typehint_10          : [     1,000] [0.0000481393337] [   20,773.03366]
    foreach_callable_1000: [       100] [0.0000003933907] [2,542,002.42424]
    foreach_if_1000      : [       100] [0.0000002026558] [4,934,475.29412]
    typehint_1000        : [       100] [0.0426092553139] [       23.46908]

```

なんだこの結果は… たまげたなぁ。

アルゴリズム上、早期に失敗した方が良いので`foreach`が超高速化するのはわかる。しかして`typehint_10`は100倍、`typehint_1000`に至っては3000倍近くにもなる差がついてしまってる。

## 宿題

* `typehint`が不正な値を検出したときの性能劣化は何に起因するか考へてみよう
* このライブラリを、あなたの業務プロジェクトで導入することは可能か、懸念事項を挙げてみよう

## 追記

<q>PHP 5.6と7の新機能</q>って言った割に、PHP 7の説明をしてないや。まあ、いっか。