LoginSignup
6
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-14

まっぴー氏から「 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 で引数に渡される値が"元の配列そのもの"でない場合に困るパターンがなさそうなので、まぁいいかなぁと思います。

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

6
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3