PHPのfilter関数に配列を渡しても、FILTER_CALLBACKの引数で渡されるのは配列の各要素を辿った値になる

まっぴー氏から「 `FILTER_CALLBACK` の微妙な罠なんですが，こいつは `array_map_recursive` 的な動きをします」という[コメントを頂いた](http://qiita.com/ndxbn/items/6c0cec139607420634f9#comments)ので、ソースコードを追ってみた。

# どんな挙動をしているのか
[元記事のコメントで書いた](http://qiita.com/ndxbn/items/6c0cec139607420634f9#comments)ことですが、一応再掲します。

## コールバックフィルタは、各ノードの値
コールバックフィルタの場合、 **第1引数の配列そのものではなく、配列の各ノードの値が渡されます** 。

```php:コールバックフィルタに渡されるのは、配列の各ノードの値
<?php

$string_matrix = [
    ['1', '2', '3'],
    ['11', '22', '33'],
];
$result = filter_var(
    $string_matrix,
    FILTER_CALLBACK,
    [
        'options' => function($value){
            var_dump($value);
            return 999;
        }
    ]
);
var_dump($result);
/* Output for 5.6.0 - 5.6.29, hhvm-3.12.0 - 3.13.2, 7.0.0 - 7.1.0
string(1) "1"
string(1) "2"
string(1) "3"
string(2) "11"
string(2) "22"
string(2) "33"
 
 // 以下、"$result" の値
array(2) {
  [0]=>
  array(3) {
    [0]=>
    int(999)
    [1]=>
    int(999)
    [2]=>
    int(999)
  }
  [1]=>
  array(3) {
    [0]=>
    int(999)
    [1]=>
    int(999)
    [2]=>
    int(999)
  }
}
*/
```

`return 999;` したものが、もとの配列の各値になっています。
まっぴー氏の言ったとおり、「 `array_map_recursive` のような動き」になっていることがわかります。

## コールバックフィルタ以外は、渡された値自体
コールバックフィルタ **以外** の場合は、ちゃんと第1引数で渡された値 *自体* の型で判定されてます。

```php:コールバックフィルタ以外の場合
<?php

$string_matrix = [
    ['1', '2', '3'],
    ['11', '22', '33'],
];

$result = filter_var($string_matrix, FILTER_SANITIZE_NUMBER_INT);
var_dump($result);
/*
bool(false)
*/
```


# 該当コードの解説

[`filter/filter.c` の `php_filter_call関数` ](https://github.com/php/php-src/blob/e8e41aa/ext/filter/filter.c#L591)が、この挙動の原因っぽいです。

いつものように、自己解釈コメントを入れたやつをおいておきます。

```c:php_filter_call()
/**
 * filtered は、「フィルタリングされる」変数そのもの。
 * filter は、適用するフィルタ（の種類）
 * filter_args は、フラグとかオプションとか。
 * copy は、 `filtered`変数自体 を破壊的に変更してはいけない場合のフラグ。
 *         現在だと、filter_inputのときにtrue（=1）にされている。
 * filter_flags は、filter_args で FILTER_REQUIRE_ARRAY,
 *          FILTER_FORCE_ARRAY, FILTER_REQUIRE_SCALAR
 *         の いずれも渡されなかったときに使用される、デフォルトの型。
 *         つまり、filter_var($data); みたいな呼び出し方をされたときに使われるということ。
 */
static void php_filter_call(zval *filtered, zend_long filter, zval *filter_args, const int copy, zend_long filter_flags) /* {{{ */
{
  zval *options = NULL;
  zval *option;
  char *charset = NULL;
  
  if (filter_args && Z_TYPE_P(filter_args) != IS_ARRAY) {
  /* filter_var($data, FILTER_DEFAULT, FILTER_VALIDATE_INT); みたいな、 第3引数が配列じゃない渡され方をした場合 */
     zend_long lval = zval_get_long(filter_args);

    if (filter != -1) { /* handler for array apply */
      /* filter_args is the filter_flags */
      filter_flags = lval;

      /* 配列で返すようなフラグを建てていない場合は、スカラで返すようにフォールバック */
      if (!(filter_flags & FILTER_REQUIRE_ARRAY ||  filter_flags & FILTER_FORCE_ARRAY)) {
        filter_flags |= FILTER_REQUIRE_SCALAR;
      }
    } else {
      filter = lval;
    }
  } else if (filter_args) {
  /* 第3引数が配列で渡された場合 */
  /* キー名でフィルタ、フラグ、オプションを展開 */
  /* "filter" が渡されるのは、`filter_var_array()` とか場合の話。*/
    if ((option = zend_hash_str_find(HASH_OF(filter_args), "filter", sizeof("filter") - 1)) != NULL) {
      filter = zval_get_long(option);
    }
    /* フラグの展開 */
    if ((option = zend_hash_str_find(HASH_OF(filter_args), "flags", sizeof("flags") - 1)) != NULL) {
      filter_flags = zval_get_long(option);
      /* 配列で返すようなフラグを建てていない場合は、スカラで返すようにフォールバック */
      if (!(filter_flags & FILTER_REQUIRE_ARRAY ||  filter_flags & FILTER_FORCE_ARRAY)) {
        filter_flags |= FILTER_REQUIRE_SCALAR;
      }
    }
    /* オプションの展開 */
    if ((option = zend_hash_str_find(HASH_OF(filter_args), "options", sizeof("options") - 1)) != NULL) {
      if (filter != FILTER_CALLBACK) {
        if (Z_TYPE_P(option) == IS_ARRAY) {
          options = option;
        }
      } else {
        /*
          コールバックフィルタの場合、フラグはなかったことにする。
          filter_flags の 0 は、"FILTER_FLAG_NONE"に等しい。
          https://github.com/php/php-src/blob/master/ext/filter/filter_private.h#L24
        */
        options = option;
        filter_flags = 0;
      }
    }
  }
  
  /* この時点で、filter_flags が PHPの `filter_var()` で渡されている場合はそれを使うようになっており、
     そうでない場合は、呼び出し元で指定されたデフォルトの値になっている。
     つまり、フラグが FILTER_NONE になっているパターンはない。たぶん。
   */

  /* フィルタ対象が配列の場合 */
  if (Z_TYPE_P(filtered) == IS_ARRAY) {
    /* 
      SCALARの場合、
      → FILTER_XXX_ARRAY もしくは、FILTER_NONE "以外"の場合
      FILTER_CALLBACK の場合は、常に ↑ こっちになっている。
    */
    if (filter_flags & FILTER_REQUIRE_SCALAR) {    
      if (copy) {
        SEPARATE_ZVAL(filtered);
      }
      zval_ptr_dtor(filtered);
      if (filter_flags & FILTER_NULL_ON_FAILURE) {
        ZVAL_NULL(filtered);
      } else {
        ZVAL_FALSE(filtered);
      }
      return;
    }
    /*
      FILTER_REQUIRE_ARRAY or FILTER_FORCE_ARRAY
    */
    php_zval_filter_recursive(filtered, filter, filter_flags, options, charset, copy);
    return;
  }
  
  /* フィルタ対象が配列じゃない場合、ここにたどり着く。 */
  
  /* フィルタ対象が配列じゃないけど、 REQUIRE_ARRAY の場合は、フィルタ失敗。 */
  if (filter_flags & FILTER_REQUIRE_ARRAY) {
    if (copy) {
      SEPARATE_ZVAL(filtered);
    }
    /* 例によって、参照カウンタがデクリメントされているけれど、理由わからない */
    zval_ptr_dtor(filtered);
    if (filter_flags & FILTER_NULL_ON_FAILURE) {
      ZVAL_NULL(filtered);
    } else {
      ZVAL_FALSE(filtered);
    }
    return;
  }
  
  /* フィルタ対象が配列じゃない場合で、REQUIRE_ARRAY ではないので、普通にフィルタリングする */
  php_zval_filter(filtered, filter, filter_flags, options, charset, copy);
  
  /* フィルタ対象を配列にキャストする場合ば、キャストする。 */
  if (filter_flags & FILTER_FORCE_ARRAY) {
    zval tmp;
    ZVAL_COPY_VALUE(&tmp, filtered);
    array_init(filtered);
    add_next_index_zval(filtered, &tmp);
  }
}
```

重要なのは、 **コールバックフィルタの場合、フラグはなかったことにする** という点です。  
コールバックフィルタの場合、 `FILTER_NULL_ON_FAILURE` とか `FILTER_REQUIRE_ARRAY` などの、どこでも使えそうなフラグも使えない、ということです。（どこでも使えそうなフラグとは、[フィルタフラグ](http://jp2.php.net/filter.filters.flags)にある、「使える場所」が空欄になっているやつのことを言っています。）

# 多分、バグではなく、「意図した実装」だと思う

消去法ですが、 `FILTER_CALLBACK` で引数に渡される値が"元の配列そのもの"でない場合に困るパターンがなさそうなので、まぁいいかなぁと思います。

需要があれば、フローチャート書きます。