__destruct()和__call()会创建无限循环。

3

我将代码进行了简化,但我所做的事情类似于以下内容:

class App{

    protected $apps = [];

    public function __construct($name, $dependencies){
        $this->name = $name;

        $apps = [];
        foreach($dependencies as $dependName){
            $apps[$name] = $dependName($this); // returns an instance of App
        }
        $this->apps = $apps;
    }

    public function __destruct(){
        foreach($this->apps as $dep){
            $result = $dep->cleanup($this);
        }
    }

    public function __call($name, $arguments){
        if(is_callable([$this, $name])){
            return call_user_func_array([$this, $name], $arguments);
        }
    }
}


function PredefinedApp(){
    $app = new App('PredefinedApp', []);

    $app->cleanup = function($parent){
        // Do some stuff
    };
    return $app;
}

I then create an app like this:

$app = new App('App1', ['PredefinedApp']);

它创建一个App实例,然后数组中的项创建定义的任何内容的新应用程序实例,并将它们放入内部应用程序数组中。
当我在主应用程序上执行我的析构函数时,它应该调用所有子应用程序上的cleanup()。但是发生了无限循环,我不确定为什么会这样。
我注意到如果我注释掉call_user_func_array,那么__call()只会被调用一次,但它不会执行实际的closure
我还注意到,如果我在__call()中使用var_dump(),它会无限制地转储信息。如果我在cleanup()中使用var_dump(),则会出现http 502错误。

@RyanVincent如果你把所有的代码都放到一个文件中,然后运行这个文件,你应该能看到问题。 - Get Off My Lawn
我非常确定这是由于您超载__call()然后使用call_user_func_array()引起的...它会再次调用__call()。永远。或者至少直到你的内存耗尽。 - doublesharp
我正在调试它,我认为__destruct()是导致所有问题的原因。因为如果我将其删除,一切都可以正常工作。 - Stanimir Dimitrov
2个回答

3

将您的__call()函数替换为以下内容可以避免出现递归:

public function __call( $method, $arguments ) {
    if ( $this->{$method} instanceof Closure ) {
        return call_user_func_array( $this->{$method}, $arguments );
    } else {
        throw new Exception( "Invalid Function" );
    }
}

请参考@Rizier123的答案以获得更多详细信息,但TLDR版本为:
问题在于您最初调用了cleanup(),并将其作为方法调用使用$dep->cleanup(),这会调用__call()。如果您随后使用call_user_func_array()并传递[$this,$method],它将作为方法调用,从而再次调用_call()并触发循环(实际上没有调用cleanup()方法),而使用$this->{$method}将作为一个Closure调用,并防止循环。

@Rizier123 仍在努力确认,但我相信这是因为没有检查它是否为闭包,导致将太多的调用推入堆栈并耗尽内存。 - doublesharp
不是回答你的问题,但我很好奇为什么cleanup()是一个闭包而不是App类的方法? - doublesharp
1
我希望能够在函数中覆盖它,但是我想不出如何实现。 - Get Off My Lawn
我仍在努力弄清楚为什么,但我被教导使用$this->{$method}格式来防止循环。我相信这与call_user_func_array()反射对象的方式有关...但不确定。我找到了这个链接,虽然没有太大帮助,但却证实了这一点:https://dev59.com/4W855IYBdhLWcg3wGQTY - doublesharp
我的回答已经更新,加入了@Rizier123提供的一些细节。 - doublesharp

3
所以让我们一起查看代码,了解这里发生了什么以及为什么发生这种情况:

01|    class App{ 
02|
03|        protected $apps = [];
04|
05|        public function __construct($name, $dependencies){
06|            $this->name = $name;
07|
08|            $apps = [];
09|            foreach($dependencies as $dependName){
10|                $apps[$name] = $dependName($this);
11|            }
12|            $this->apps = $apps;
13|        }
14|
15|        public function __destruct(){
16|            foreach($this->apps as $dep){
17|                $result = $dep->cleanup($this);
18|            }
19|        }
20|
21|        public function __call($name, $arguments){
22|            if(is_callable([$this, $name])){
23|                return call_user_func_array([$this, $name], $arguments);
24|            }
25|        }
26|    }
27| 
28|    function PredefinedApp(){
29|        $app = new App('PredefinedApp', []);
30|
31|        $app->cleanup = function($parent){
32|            //Do stuff like: echo "I'm saved";
33|        };
34|        return $app;
35|    }
36|  
37|    $app = new App('App1', ['PredefinedApp']);

注意:在代码每一行都添加了行号,这样我可以在下面的答案中引用这些行

问题

  1. You create an instance of: App with the following line:

    $app = new App('App1', ['PredefinedApp']);  //Line: 37
  2. The constructor gets called:

    public function __construct($name, $dependencies){ /* Code here */ }  //Line: 05

    2.1. Following parameters are passed:

    • $name = "App1"
    • $dependencies = ["PredefinedApp"]
  3. You assign $name to $this->name with this line:

    $this->name = $name;  //Line: 06
  4. You initialize $apps with an empty array:

    $apps = [];  //Line: 08
  5. Now you loop through each element of $dependencies, which has 1 element here (["PredefinedApp"])

  6. In the loop you do the following:

    6.1 Assign the return value of a function call to an array index:

    $apps[$name] = $dependName($this);  //Line: 10
    //$apps["App1"] = PredefinedApp($this);

  7. You call the function:

    PredefinedApp(){ /* Code here */}  //Line: 28
  8. Now you create again a new instance of: App in PredefinedApp() same as before (Point 2 - 6, expect in the constructor you have other variable values + you don't enter the loop, since the array is empty)

  9. You assign a closure to a class property:

    $app->cleanup = function($parent){  //Line: 31
        //Do stuff like: echo "I'm saved";
    };
  10. You return the new created object of App:

    return $app;  //Line: 34
  11. Here already the __destruct() gets called, because when the function ends the refcount goes to 0 of that zval and __destruct() is triggered. But since $this->apps is empty nothing happens here.

  12. The new created object in that function gets returned and assigned to the array index (Note: We are back from the function to point 6.1):

    $apps[$name] = $dependName($this);  //Line: 10
    //$apps["App1"] = PredefinedApp($this);

  13. The constructor ends with assigning the local array to the class property:

    $this->apps = $apps;  //Line: 12
  14. Now the entire script ends (We have done line: 37)! Which means for the object $app the __destruct() is triggered for the same reason as before for $app in the function PredefinedApp()

  15. Which means you now loop through each element from $this->apps, which only holds the returned object of the function:

    public function __destruct(){  //Line: 15
        foreach($this->apps as $dep){
            $result = $dep->cleanup($this);
        }
    }
    Array(
        "App1" => App Object
            (
                [apps:protected] => Array
                    (
                    )
    
                [name] => PredefinedApp
                [cleanup] => Closure Object
                    (
                        [parameter] => Array
                            (
                                [$parent] => <required>
                            )
    
                    )
    
            )
    )
    
  16. For each element (Here only 1) you execute:

    $result = $dep->cleanup($this);  //Line: 17

    But you don't call the closure! It tries to call a class method. So there is no cleanup class method, it's just a class property. Which then means __call() gets invoked:

    public function __call($name, $arguments){  //Line: 21
        if(is_callable([$this, $name])){
            return call_user_func_array([$this, $name], $arguments);
        }
    }
  17. The $arguments contains itself ($this). And is_callable([$this, $name]) is TRUE, because cleanup is callable as closure.

  18. So now we are getting in the endless stuff, because:

    return call_user_func_array([$this, $name], $arguments);  //Line: 23

    Is executed, which then looks something like this:

    return call_user_func_array([$this, "cleanup"], $this);

    Which then again tries to call cleanup as a method and again __call() is invoked and so on...

所以在整个脚本的结尾,整个灾难开始了。但是我有一些好消息,虽然听起来很复杂,但解决方案要简单得多!

解决方案

只需更改:

return call_user_func_array([$this, $name], $arguments);  //行:23

使用以下内容替换:

return call_user_func_array($this->$name, $arguments);
                           //^^^^^^^^^^^ See here

通过这样改变,您不会尝试调用方法,而是闭包。因此,如果您还添加:

echo "I'm saved";

在你赋值时使用闭包,你最终将得到以下输出结果:
I'm saved

1
这个回答比我的好多了,实际上描述了原因...然而“修复”是相同的。OP声称他无法调用$this->$name,因为$name是一个对象(顺便说一下,你的回答中有语法错误)。 - doublesharp
2
这也很好用!@doublesharp,在使用你的代码时,由于某种原因,当将其转移到实际代码时它没有起作用,但在示例中运行良好... - Get Off My Lawn
1
@GetOffMyLawn 我整理了一下我的答案,也许你想再看一遍,现在应该更加清晰了。 - Rizier123

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