まっぴー氏から「 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.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
などの、どこでも使えそうなフラグも使えない、ということです。(どこでも使えそうなフラグとは、フィルタフラグにある、「使える場所」が空欄になっているやつのことを言っています。)
多分、バグではなく、「意図した実装」だと思う
消去法ですが、 FILTER_CALLBACK
で引数に渡される値が"元の配列そのもの"でない場合に困るパターンがなさそうなので、まぁいいかなぁと思います。
需要があれば、フローチャート書きます。