如何在PHP中实现每次调用函数时自动调用另一个函数

70
Class test{
    function test1()
    {
        echo 'inside test1';
    }

    function test2()
    {
        echo 'test2';
    }

    function test3()
    {
        echo 'test3';
    }
}

$obj = new test;
$obj->test2();//prints test2
$obj->test3();//prints test3

现在我的问题是,

如何在任何已调用的函数执行之前调用另一个函数? 在上面的情况下,如何自动调用“test1”函数以便对每个其他函数的调用进行处理, 从而使得我可以得到以下输出结果:

test1
test2
test1
test3

目前我正在获取以下输出:

test2
test3

在每个函数定义中调用'test1'函数并不可行,因为可能存在许多函数。我需要一种在调用类的任何函数之前自动调用函数的方法。

也可以使用任何其他替代方法。

8个回答

77

你最好使用魔术方法__call,下面是示例:

<?php

class test {
    function __construct(){}

    private function test1(){
        echo "In test1", PHP_EOL;
    }
    private function test2(){
        echo "test2", PHP_EOL;
    }
    protected function test3(){
        return "test3" . PHP_EOL;
    }
    public function __call($method,$arguments) {
        if(method_exists($this, $method)) {
            $this->test1();
            return call_user_func_array(array($this,$method),$arguments);
        }
    }
}

$a = new test;
$a->test2();
echo $a->test3();
/*
* Output:
* In test1
* test2
* In test1
* test3
*/
请注意,由于使用了protectedprivatetest2test3在被调用的上下文中是不可见的。如果这些方法是公开的,则以上示例将失败。 test1不必声明为private
可以在ideone.com找到示例,其中包括返回值。已更新。

实际上,a) 你也可以将方法设置为 protected,只是不能设置为 public,b) 你不需要下划线。 - ktolis
你是对的,使用protected也可以,我已经在示例中删除了“_”并将test3设置为protected。 - Kristoffer Sall-Storgaard
__call中应该使用return吗? - Alex2php
1
使用这种解决方案,您仍然需要将公共方法修改为私有方法。您可以修改方法的访问修饰符,但不能在每个方法中调用所需的方法“test()”,这是没有意义的。 - RN Kushwaha
1
@Nebulosar,你可以使用__callStatic构建类似的东西。 - Kristoffer Sall-Storgaard
显示剩余5条评论

23
All previous attempts are basically flawed because of http://ocramius.github.io/presentations/proxy-pattern-in-php/#/71 这是一个简单的例子,摘自我的幻灯片:
class BankAccount { /* ... */ }

这是我们“简陋”的拦截器逻辑:

class PoorProxy {
    public function __construct($wrapped) {
        $this->wrapped = $wrapped;
    }

    public function __call($method, $args) {
        return call_user_func_array(
            $this->wrapped,
            $args
        );
    }
}

现在如果我们要调用以下方法:
function pay(BankAccount $account) { /* ... */ }

然后这个不起作用:
$account = new PoorProxy(new BankAccount());

pay($account); // KABOOM!

这适用于所有建议实现“代理”的解决方案。
那些建议显式使用其他方法来调用内部API的解决方案是有缺陷的,因为它们强制你更改公共API以更改内部行为,并且它们会降低类型安全性。
Kristoffer提供的解决方案没有考虑到public方法,这也是一个问题,因为你不能重写API使其全部为private或protected。
以下是部分解决此问题的解决方案:
class BankAccountProxy extends BankAccount {
    public function __construct($wrapped) {
        $this->wrapped = $wrapped;
    }

    public function doThings() { // inherited public method
        $this->doOtherThingsOnMethodCall();

        return $this->wrapped->doThings();
    }

    private function doOtherThingsOnMethodCall() { /**/ }
}

这是如何使用它的:

$account = new BankAccountProxy(new BankAccount());

pay($account); // WORKS!

这是一种类型安全、干净的解决方案,但涉及大量编码,请只将其作为一个示例。
编写这个样板代码并不有趣,所以您可能想使用不同的方法。
为了让您了解这类问题有多么复杂,我可以告诉您,我编写了 整个库 来解决它们,一些更聪明、更有智慧、更年长的人甚至发明了一个完全不同的范例,称为 "面向切面编程" (AOP)
因此,我建议您研究这三种解决方案,我认为它们可以以更加清晰的方式解决您的问题。
  • Use ProxyManager's "access interceptor", which is basically a proxy type that allows you to run a closure when other methods are called (example). Here is an example on how to proxy ALL calls to an $object's public API:

    use ProxyManager\Factory\AccessInterceptorValueHolderFactory;
    
    function build_wrapper($object, callable $callOnMethod) {
        return (new AccessInterceptorValueHolderFactory)
            ->createProxy(
                $object,
                array_map(
                    function () use ($callOnMethod) {
                        return $callOnMethod;
                    },
                    (new ReflectionClass($object))
                        ->getMethods(ReflectionMethod::IS_PUBLIC)
                )
            ); 
    }
    

    then just use build_wrapper as you like.

  • Use GO-AOP-PHP, which is an actual AOP library, completely written in PHP, but will apply this sort of logic to ALL instances of classes for which you define point cuts. This may or may not be what you want, and if your $callOnMethod should be applied only for particular instances, then AOP is not what you are looking for.

  • Use the PHP AOP Extension, which I don't believe to be a good solution, mainly because GO-AOP-PHP solves this problem in a more elegant/debuggable way, and because extensions in PHP are inherently a mess (that is to be attributed to PHP internals, not to the extension developers). Additionally, by using an extension, you are making your application as un-portable as possible (try convincing a sysadmin to install a compiled version of PHP, if you dare), and you can't use your app on cool new engines such as HHVM.


还有另一个独立的库 https://github.com/koriym/Ray.Aop,它被Bear.Sunday使用。 - Hari K T

8
也许这已经有点过时了,但是我想说几句...

我认为通过 __call() 提供私有方法的访问并不是一个好主意。如果你有一个真正不希望在对象外部被调用的方法,那么你就没有办法避免它发生。

我认为更优雅的解决方案是创建一种通用代理/装饰器,并在其中使用 __call()。让我来展示一下:

class Proxy
{
    private $proxifiedClass;

    function __construct($proxifiedClass)
    {
        $this->proxifiedClass = $proxifiedClass;
    }

    public function __call($methodName, $arguments)
    {

        if (is_callable(
                array($this->proxifiedClass, $methodName)))
        {
            doSomethingBeforeCall();

            call_user_func(array($this->proxifiedClass, $methodName), $arguments);

            doSomethingAfterCall();
        }
        else
        {
            $class = get_class($this->proxifiedClass);
            throw new \BadMethodCallException("No callable method $methodName at $class class");
        }
    }

    private function doSomethingBeforeCall()
    {
        echo 'Before call';
        //code here
    }

    private function doSomethingAfterCall()
    {
        echo 'After call';
        //code here
    }
}

现在是一个简单的测试类:
class Test
{
    public function methodOne()
    {
        echo 'Method one';
    }

    public function methodTwo()
    {
        echo 'Method two';
    }

    private function methodThree()
    {
        echo 'Method three';
    }

}

现在您需要做的是:
$obj = new Proxy(new Test());

$obj->methodOne();
$obj->methodTwo();
$obj->methodThree(); // This will fail, methodThree is private

优点:

1)您只需一个代理类即可与所有对象一起使用。 2)您不会违反可访问性规则。 3)您无需更改受代理对象。

缺点:在封装原始对象后,您将失去接口/契约。如果您频繁使用类型提示可能会成为问题。


4
也许目前最好的方法是创建自己的方法调用器,并在方法之前和之后包装您需要的任何内容:
class MyClass {

    public function callMethod()
    {
        $args = func_get_args();

        if (count($args) == 0) {
            echo __FUNCTION__ . ': No method specified!' . PHP_EOL . PHP_EOL;;
        } else {
            $method = array_shift($args); // first argument is the method name and we won't need to pass it further
            if (method_exists($this, $method)) {
                echo __FUNCTION__ . ': I will execute this line and then call ' . __CLASS__ . '->' . $method . '()' . PHP_EOL;
                call_user_func_array([$this, $method], $args);
                echo __FUNCTION__ . ": I'm done with " . __CLASS__ . '->' . $method . '() and now I execute this line ' . PHP_EOL . PHP_EOL;
            } else
                echo __FUNCTION__ . ': Method ' . __CLASS__ . '->' . $method . '() does not exist' . PHP_EOL . PHP_EOL;
        }
    }

    public function functionAA()
    {
        echo __FUNCTION__ . ": I've been called" . PHP_EOL;
    }

    public function functionBB($a, $b, $c)
    {
        echo __FUNCTION__ . ": I've been called with these arguments (" . $a . ', ' . $b . ', ' . $c . ')' . PHP_EOL;
    }
}

$myClass = new MyClass();

$myClass->callMethod('functionAA');
$myClass->callMethod('functionBB', 1, 2, 3);
$myClass->callMethod('functionCC');
$myClass->callMethod();

以下是输出结果:

callMethod: 我将执行此行,然后调用 MyClass->functionAA()
functionAA: 我已被调用
callMethod: 我已完成 MyClass->functionAA() 的调用,现在我将执行此行
callMethod: 我将执行此行,然后调用 MyClass->functionBB() functionBB: 我已使用这些参数(1、2、3)被调用 callMethod: 我已完成 MyClass->functionBB() 的调用,现在我将执行此行
callMethod: 方法 MyClass->functionCC() 不存在
callMethod: 没有指定方法!

您甚至可以进一步创建一个方法白名单,但为了简化示例,我将其保留如此。

您将不再被强制将方法设置为私有并通过 __call() 使用它们。 我假设可能会出现需要在没有包装器的情况下调用方法或者您希望您的 IDE 仍然自动完成方法的情况,如果您将方法声明为私有,则最有可能不会发生。


3
<?php

class test
{

    public function __call($name, $arguments)
    {
        $this->test1(); // Call from here
        return call_user_func_array(array($this, $name), $arguments);
    }

    // methods here...

}

?>

尝试在类中添加此方法覆盖...


谢谢您的建议,但我不能在每个函数定义中调用“test1”函数,因为可能有很多函数。我需要一种在调用类的任何函数之前自动调用函数的方法。 - Nik
+1是最好的方法,因为它不违反OOP原则,尽管可以作为公共方法访问,但指定为私有方法。 - poke

2

如果你真的很勇敢,可以使用runkit扩展实现。(http://www.php.net/manual/zh/book.runkit.php)。您可以尝试使用runkit_method_redefine(您可能需要Reflection来检索方法定义),或者结合使用runkit_method_rename(旧函数) / runkit_method_add (新函数,将调用test1函数和旧函数封装起来)。


0
唯一的方法是使用神奇的__call。您需要将所有方法设为私有,以便外部无法访问。然后定义__call方法来处理方法调用。在__call中,您可以在调用原本被调用的函数之前执行任何想要执行的函数。

-2

让我们来试试这个:

class test
{
    function __construct()
    {

    }

    private function test1()
    {
        echo "In test1";
    }

    private function test2()
    {
        echo "test2";
    }

    private function test3()
    {
        echo "test3";
    }

    function CallMethodsAfterOne($methods = array())
    {
        //Calls the private method internally
        foreach($methods as $method => $arguments)
        {
            $this->test1();
            $arguments = $arguments ? $arguments : array(); //Check
            call_user_func_array(array($this,$method),$arguments);
        }
    }
}

$test = new test;
$test->CallMethodsAfterOne('test2','test3','test4' => array('first_param'));

这就是我会做的事情


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