肉とビールとパンケーキ by @sotarok

少し大人になった「肉とご飯と甘いもの」

PHP 5.3: 参照渡しの関数/メソッドを定義してた人は call_user_func_array に注意


*1

あーっと.はじめにいっておくと,すべてのマニュアルをちゃんと読んでいて関数の使い方を間違っていなかった人には関係ない話です.
が,意外とハマるんじゃないかと思うのでメモ.

どういう問題が発生したか

<?php

function hoge(&$hoge) {
    var_dump($hoge);
}

$a = 1;

hoge($a);
call_user_func_array('hoge', array($a));

このコード,どういう実行結果が期待されるかというと,

int(1)
int(1)

です.

call_user_func_array の第二引数は,関数に渡したい値を配列で指定するものなので,これでOK.PHP 5.2 では,上記の結果が得られます.
ところが,PHP 5.3 では,以下のエラーが出ます.

int(1)

Warning: Parameter 1 to hoge() expected to be a reference, value given in callfunc_ref.php on line 1


では,まず, call_user_func_array の使い方について.

call_user_func_array の第二引数の参照渡し,値渡しについて

マニュアルにちゃんと

注意: param_arr 内で参照される変数は、 関数に参照渡しされます。それ以外は値渡しとなります。 つまり、パラメータが値渡しとなるか参照渡しとなるのかは、 関数のシグネチャには依存しないということです。

PHP: call_user_func_array - Manual

と書いてあります.

つまり,先ほど出したコードは,PHP 5.3 だろうと 5.2 だろうと,意図した通りに動かすためには,以下のコードが正しいものとなります.

<?php

function hoge(&$hoge) {
    var_dump($hoge);
}

$a = 1;

hoge($a);
call_user_func_array('hoge', array(&$a));

call_user_func_array 経由で呼び出される関数の引数が参照渡しか値渡しかは,関数の定義によるものではなく,call_user_func_array に渡した配列の要素(ようするに引数となるもの)が参照か値か,によるのです.「5.2 では同じ結果が得られる」とは言いましたが,内部で起こってることは実は違っていて,本当は参照ではなく値で渡されていたんですね.


これと,以下の PHP 5.3 での変更点が組合わさると,今回の問題が発現するわけです.

PHP 5.3 での変更点について

今回ハマるポイントだったのは,以下の変更点です.

引数を参照渡しする関数に値を渡した場合の振る舞いが変更されました。 以前は値渡しとして引数を受け取っていましたが、5.3.x からは warning が生成され、 全ての参照渡しのパラメーターが NULL となります。

PHP: 下位互換性のない変更点 - Manual

引数を参照渡しする関数に値を渡した場合の振る舞いが変更されました。 以前は値渡しとして引数を受け取っていましたが、今は fatal error が発生するようになりました。 参照渡しを期待している関数に定数やリテラルを渡していたコードは、 いったんその値を変数に代入してから関数に渡すよう書き換える必要があります

PHP: 下位互換性のない変更点 - Manual

ということで,最初に示したコードでは, array($a) としたため,関数hogeに渡される $hoge が値渡しされたことになっていた→ warning fatal ということでした.


(あと,これは不思議ですが, null さえ出力されませんでした.よ.ドキュメントと実装の矛盾?はて?)

  • ドキュメントが更新されてましたので,修正&追記
    • ドキュメントが更新されたようです.warningではなく,fatalになります.ご連絡ありがとうございます. m(__)m

解決法と予防法

  • call_user_func_array の第二引数の指定の仕方によって,関数がコールされるときに引数が値渡しになるか参照渡しになるかがかわります.マニュアルにも書いてあります.
  • call_user_func_array によって呼び出される関数が参照で引数を受け取るなら,コールするほうで気をつけてください.
  • ちなみに, call_user_func のほうも同じです.

しかし,本当の問題は

  • 呼び出し先が色々な形で存在するのを吸収するための call_user_func_array なのに呼び出し先の引数の受け取り方をしっていないと呼び出せない関数となっているのがすごい.
    • だって第二引数に入ってくる引数の数や型などがわかってれば, call_user_func だけでじゅうぶんなのに,配列で引数を指定できるようにしているのは,呼び出す先が不定*2であり,わからない状態でも呼べる,という柔軟性を求めて作ったものじゃなかったのか,という.
  • 関数の定義によらず,コールする方の引数の指定によって値渡し,参照渡しが変わるのはどうなんだ?
  • ちなみに,OpenID のライブラリ (http://openidenabled.com/php-openid/) の内部のコードでこういう問題があってはまったので,利用してる人は注意してください.
    • 具体的には,Auth/Yadis/XRDS.php:431 の call_user_func_array が,参照渡しで受け取るメソッドに値渡しをしています. call_user_func_array($filter, array(&$service)) としてあげましょう.
    • 追記:調べてたら,以下の箇所でも使われてました.あとでパッチつくるかも.
    • Auth/OpenID/Consumer.php:669 > s/array($message, $endpoint, $return_to));/array($message, &$endpoint, $return_to));/
    • Auth/OpenID/Consumer.php:1184 > s/$this->fetcher);/&$this->fetcher);/


あと,これは,こういったPHPの仕様は,それはそれでOK,という前提で,その上で,それを使う方の人に言いたい話.
オブジェクトを渡した場合,&をつけなくても参照渡しになるので,&にする必要がありません.もう過ぎ去った時代*3に縛られてオブジェクトを引数にとる関数に & つけるのやめてください.

<?php
// for example
class A
{
    public $a = 2;
}

function hoge($hoge) {
    $hoge->a = 3;
}

$a = new A();
echo $a->a , "\n"; // 2
hoge($a);
echo $a->a , "\n"; // 3
$a->a = 2;
echo $a->a , "\n"; // 2
call_user_func('hoge', $a); // no problem
echo $a->a , "\n"; // 3

ところで

に,

call_user_func_array('increment', array(&$a)); // PHP 5.3 より前のバージョンでは、このようにしてもかまいません

という一文があるのですが, PHP 5.3 でもこのようにしてかまわないとおもうのですが*4,マニュアル書いた人の意図が気になります.(英語でも「You can use this instead before PHP 5.3」です)

*1:イケてるタイトルが思いつかなくてちょっとげんなりしてます.微妙にイケてないタイトルですよねこの記事.

*2:あるいは呼び出し元からはわからない

*3:PHP4

*4:むしろ PHP 5.3 ではこうするべきで