__callStatic(),call_user_func_array(),引用和PHP 5.3.1

9

我一直在Stack Overflow和其他地方阅读,但似乎找不到什么确定性的内容。

有没有办法有效地通过此调用堆栈传递引用,从而实现如下示例中所述的所需功能?虽然该示例并未尝试解决该问题,但它确实说明了问题:

class TestClass{
    // surely __call would result similarly
    public static function __callStatic($function, $arguments){
        return call_user_func_array($function, $arguments);
    }
}

// note argument by reference
function testFunction(&$arg){
    $arg .= 'bar';
}

$test = 'foo';
TestClass::testFunction($test);

// expecting: 'foobar'
// getting:   'foo' and a warning about the reference
echo $test;

为了激发潜在的解决方案,我在这里添加摘要细节:

仅关注call_user_func_array(),我们可以确定(至少在PHP 5.3.1中),您不能隐式地通过引用传递参数:

function testFunction(&$arg){
    $arg .= 'bar';
}

$test = 'foo';
call_user_func_array('testFunction', array($test));
var_dump($test);
// string(3) "foo" and a warning about the non-reference parameter

通过明确将数组元素$test作为引用传递,我们可以缓解这种情况:

call_user_func_array('testFunction', array(&$test));
var_dump($test);
// string(6) "foobar"

当我们使用__callStatic()引入类时,一个显式的按引用传递的调用时间参数似乎会像我预期的那样被传递,但是(在我的IDE中)会发出弃用警告:
class TestClass{
    public static function __callStatic($function, $arguments){
        return call_user_func_array($function, $arguments);
    }
}

function testFunction(&$arg){
    $arg .= 'bar';
}

$test = 'foo';
TestClass::testFunction(&$test);
var_dump($test);
// string(6) "foobar"

TestClass::testFunction()中省略引用运算符会导致$test以传值的方式传递到__callStatic(),并且当然会通过call_user_func_array()以数组元素的形式传递到testFunction()。由于testFunction()需要一个引用,因此会出现警告。
在处理问题时,一些额外的细节浮出水面。如果__callStatic()定义为返回引用(public static function &__callStatic()),则没有明显的影响。此外,将$arguments数组的元素重新转换为引用,我们可以看到call_user_func_array()有一定的预期效果:
class TestClass{
    public static function __callStatic($function, $arguments){
        foreach($arguments as &$arg){}
        call_user_func_array($function, $arguments);
        var_dump($arguments);
        // array(1) {
        //   [0]=>
        //   &string(6) "foobar"
        // }
    }
}

function testFunction(&$arg){
    $arg .= 'bar';
}

$test = 'foo';
TestClass::testFunction($test);
var_dump($test);
// string(3) "foo"

由于$test不再通过引用传递,因此这些结果是可以预料的,更改不会传回其范围。但是,这证实了call_user_func_array()确实像预期的那样工作,并且问题肯定局限于调用魔术方法。

进一步阅读后,似乎这可能是PHP处理用户函数和__call()/__callStatic()魔术方法的“错误”。我已经查看了现有或相关问题的漏洞数据库,并找到了一个问题,但无法再次定位它。我正在考虑发布另一个报告或请求重新打开现有报告。


看起来问题更多地与__call()魔术方法有关,而不是call_user_func_array()...此外,似乎没有特别干净的解决方法。不过,欢迎任何想法。 - Dan Lugg
4个回答

3

这是一个有趣的例子。

TestClass::testFunction(&$string);

这个方法可行,但是它也会抛出一个“引用传递”的警告。

主要问题在于__callStatic的第二个参数是按值传递的。除非该数据已经是一个引用,否则它会创建一个数据的副本。

我们可以通过以下方式复制调用错误:

call_user_func_array('testFunction', array( $string ));
// PHP Warning:  Parameter 1 to testFunction() expected to be a reference, value given

call_user_func_array('testFunction', array( &$string ));
echo $string;
// 'helloworld'

我试图修改__callStatic方法,通过引用深度复制数组,但这也没有起作用。

我相信__callStatic引起的直接复制会在这里产生问题,并且你将无法做到这一点,除非进行足够的跳跃以使其语法上更加粘性。


谢谢Charles;是的,我想到了调用魔术方法可能会是问题所在。我注意到它已经出现在PHP错误修复请求中,但尚未被修补(至少从我找到的资料来看)。显式地通过引用传递参数当然会抛出警告,而且不幸的是这也不是什么满意的解决方案。你有什么想法可以解决这个问题吗?无论多么疯狂都可以。 - Dan Lugg
很不幸,我能想到的唯一解决方案就是完全不使用__callStatic,这可能对你并没有太大帮助。你能告诉我们更多关于你想要实现的目标吗? - Charles
现在它已经成为了一个学术试验,但最初的目的来自我之前发布的这个问题;https://dev59.com/PVbTa4cB1Zd3GeqP7At7第一个处理加载未分类函数库的建议选项利用了`__callStatic`。虽然从优化的角度来看采用不同的方法可能更有益,但我仍然想解决这个问题,因为(至少对我来说)它似乎是一个非常干净的解决方案。 - Dan Lugg
1
嗯,有趣的想法,但我不确定PHP是实现那种魔术的正确语言。如果用Perl或Ruby,我相信可以做到... - Charles
是的,我也希望能利用这个功能来实现其他目的。自动加载无类函数库是一个开始,但我正在思考一些其他轻量级教育MVC框架的想法,这将是一个资产。我会继续寻找。再次感谢Charles,如果您有任何其他想法,我很乐意听取。 - Dan Lugg

2

我将通过逐步复现问题并演示其可能无法实现所需功能来回答这个问题。

我们的测试函数相当简单,如下所示:

function testFunction(&$argument)
{
    $argument++;
}

如果我们直接调用该函数,它当然会按照预期的方式运行:
$value = 1;
testFunction($value);
var_dump($value); // int(2)

如果我们使用call_user_func_array调用函数:
call_user_func_array('testFunction', [$value]);
var_dump($value); // int(1)

值仍然为1,因为$value没有通过引用传递。我们可以强制这样做:

call_user_func_array('testFunction', [&$value]);
var_dump($value); // int(2)

这里再次调用函数,与直接调用函数相同。

现在,我们介绍使用__callStatic的类:

class TestClass
{
    public static function __callStatic($name, $argumentArray)
    {
        return call_user_func_array($name, $argumentArray);
    }
}

如果我们通过__callStatic来转发调用函数:
TestClass::testFunction($value);
var_dump($value); // int(1)

该值仍然为1并且我们也收到了警告:

警告:testFunction()的第1个参数期望是一个引用,但实际传递的是一个值

这证实了传递给__callStatic$argumentArray不包含引用。这很有道理,因为PHP不知道这些参数是要在__callStatic中被引用接受,也不知道我们正在转发这些参数。

如果我们将__callStatic更改为通过引用接受数组,试图强制实现所需的行为:

public static function __callStatic($name, &$argumentArray)
{
    return call_user_func_array($name, $argumentArray);
}

哎呀!出问题了!

Fatal error: Method TestClass::__callstatic() cannot take arguments by reference

不能这样做;此外,文档中也明确说明了这一点:(链接)

注意:
这些魔术方法的参数都不能通过引用传递。

尽管无法声明 __callStatic 接受引用,但如果我们通过 call_user_func_array 来转发引用到 __callStatic(在实践中相当笨拙且自毁),则可以实现引用传递:

call_user_func_array(['TestClass', 'testFunction'], [&$value]);
var_dump($value); // int(2)

它再次工作了。


0

哇,经过一个多小时的思考,我仍然不确定,但是......

mixed call_user_func_array ( callback $function , array $param_arr )

如果我们使用简单的逻辑(在这种情况下),当类调用函数call_user_func_array()时,它将静态字符串用作参数(通过魔术方法__callStatic()传递),因此参数2在call_user_func_array()函数中是一个字符串,在函数testFunction()中也是参数1。它只是一个字符串,而不是指向变量的指针,这可能是出现类似以下消息的原因:
Warning: Parameter 1 to testFunction() expected to be a reference, value given in ... on line ...

换句话说,它不能将值分配回不存在的变量,但是testFunction()期望参数是引用,并且会因为不是引用而抛出警告。
你可以在类外测试它:
function testFunction(&$arg){
    $arg .= 'world';
  }

$string = 'hello';
call_user_func_array('testFunction', array($string));
echo $string;

它抛出相同的警告! 因此问题不在类上,而是在这个函数上。

这个函数显然将函数参数作为静态数据传递,而没有指向变量和函数的指针,因此调用的函数无法处理引用参数。


感谢Wh1T3h4Ck5;我们已经缩小问题范围,发现是调用魔术方法的问题。__call()/__callStatic()通过参数数组不会在其定义体中传递引用,至少没有任何常规方式可以实现。调用魔术方法的函数定义(或根据文档定义的任何魔术函数)也不允许通过引用传递参数。 - Dan Lugg

0

经过多年的时间,我终于有了意外惊喜的时刻:

class TestClass{
    public static function __callStatic($functionName, $arguments) {
        $arguments[0]->{'ioParameter'} = 'two';
    }
}
$objectWrapper = new StdClass();
$objectWrapper->{'ioParameter'} = 'one';
TestClass::testFunction($objectWrapper);
var_dump($objectWrapper); 
// outputs: object(stdClass)#3 (1) { ["ioParameter"]=> string(3) "two" }

(在php 5.5上测试过)

如果您控制接口,则此解决方法干净且可行。

它通过将参数包装在对象中来实现,根据语言定义,该对象始终通过“按引用传递”传递。

即使使用__callStatic(可能还有__call),它也允许传递可写(引用)参数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接