PHP
Filter

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

More than 1 year has passed since last update.

まっぴー氏から「 FILTER_CALLBACK の微妙な罠なんですが,こいつは array_map_recursive 的な動きをします」というコメントを頂いたので、ソースコードを追ってみた。

どんな挙動をしているのか

元記事のコメントで書いたことですが、一応再掲します。

コールバックフィルタは、各ノードの値

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

コールバックフィルタに渡されるのは、配列の各ノードの値
<?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

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

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

該当コードの解説

filter/filter.cphp_filter_call関数 が、この挙動の原因っぽいです。

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

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 などの、どこでも使えそうなフラグも使えない、ということです。(どこでも使えそうなフラグとは、フィルタフラグにある、「使える場所」が空欄になっているやつのことを言っています。)

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

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

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