PHP - 每次都使用__call方法

11
假设我们有一个类,其中包含多个受保护和/或公共方法。每次调用方法时我需要执行一个检查。我可以在每次调用方法时进行这个检查:
class Object
{
    // Methods
}

$o = new Object();

if($mayAccess) $o->someMethod();

或者
if($mayAccess) $this->someMethod();

但我希望开发人员既不需要考虑它,也不需要编写它。我想过使用 __call 来实现这样的功能:

class Object
{
    public function __call($methodName, $args)
    {
        if($mayAccess) call_user_func_array($this->$methodName, $args);
    }
}

不幸的是,如果我从类内部调用该方法,则 __call 不会被调用,因为它仅在调用非可见方法时起作用。

有没有一种简洁的方法来隐藏这个检查,无论是内部还是外部调用?目标仍然是确保开发人员在调用方法时不会忘记执行此操作。

提前感谢:)

EDIT :

我有另外一种做法:

class Object
{
    public function __call($methodName, $args)
    {
        if($mayAccess) call_user_func_array($methodName, $args);
    }
}

function someMethod() { }

但我将不能再使用$this了,这意味着没有保护方法,而我确实需要。


请查看call_user_func_array手册页面。我在这篇文章中纠正了三次同样的错误:如果您想要一个方法可调用,必须使用array($object, $method)来描述它。 - Alain Tiemblo
1
@Ninsuo 不要这样做。你绝对不应该纠正问题中的代码。提问者发布的代码需要保持原样。 - user229044
2个回答

9
不,我认为不行。但你可以编写一个代理模式(proxy)来实现:
class MayAccessProxy {

    private $_obj;

    public function __construct($obj) {
        $this->_obj = $obj;
    }

    public function __call($methodName, $args) {
        if($mayAccess) call_user_func_array(array($this->_obj, $methodName), $args);
    }
}

这意味着您必须为想要检查的每个对象实例化一个代理:
$obj = new MayAccessProxy(new Object());
$obj->someMethod();

当然你也希望代理行为与对象本身完全一致。因此,你还需要定义其他魔法方法。

为了让开发人员更加方便,你可以这样做:

class Object {

    /**
     * Not directly instanciable.
     */
    private __construct() {}  

    /**
     * @return self
     */
    public static function createInstance() {
        $obj = new MayAccessProxy(new self());
        return $obj;
    }
}

$obj = Object::createInstance();

感谢您的帮助。这仍然需要开发人员考虑代理并编写代码,因此我可能需要考虑另一种方法来完成这个任务。 - Virus721
如果你要包含一个命名构造函数,你可能想确保真正的构造函数不是公共的。这将大大防止裸露的对象漂浮。 - cHao
真的吗?在这种情况下是什么? - Alain Tiemblo
“命名构造函数”只是一个返回其类的新实例的静态成员函数。因此,您可以使用Object::createInstance()代替new Object。是的,这就像在C++中一样。 - cHao
这个“Object”实际上是我的网站控制器(一个单例)。它的方法根据GET或POST中的HTTP变量的值被调用,或者由开发人员从其中一个方法内部调用。例如,当尝试登录时,访问者通过表单操作将调用“submitLogin”方法,而“submitLogin”将在失败的情况下调用“viewLoginForm”方法。我需要根据访问者被授予的权限限制对某些方法的访问,以防止他执行某些操作或查看某些页面。 - Virus721
显示剩余7条评论

0

如果你将所有方法都设为受保护或私有的,会怎么样?(我知道这是一个老问题并已被“解答”)

__call魔术方法可以拦截所有不存在或非公共方法,因此将所有方法设置为非公共的将允许您拦截所有方法。

public function __call( $func, $args )
{
  if ( !method_exists( $this, $func ) ) throw new Error("This method does not exist in this class.");

  Handle::eachMethodAction(); // action which will be fired each time a method will be called

  return $this->$func( ...$args );
}

感谢这个方法,您不需要对代码做任何修改(除了添加__call并进行快速的替换所有),如果您的类有共同的父类,那么您只需将其添加到父类中,就不用再关心它了。

但是

这种解决方案会带来两个主要问题:

  • 受保护/私有方法会自动变为公共方法
  • 错误将指向__call而不是正确的文件

我们该怎么办?

自定义私有/受保护方法

您可以添加一个所有受保护/私有方法的列表,并在调用之前检查该方法是否可以返回给公共方法:

public function __call( $func, $args )
{
  $private = [
    "PrivateMethod" => null
  ];

  if ( !method_exists( $this, $func ) ) throw new Error("This method does not exist in this class.");
  if ( isset( $private[$func] )       ) throw new Error("This method is private and cannot be called");

  Handle::eachMethodAction(); // action which will be fired each time a method will be called

  return $this->$func( ...$args );
}

对于许多人来说,这可能是致命缺陷,但我个人仅在只有公共方法(我将其设置为受保护的方法)的类中使用此方法。因此,如果可能的话,您可以将方法分开成 publicClassprivateClass 并消除此问题。

自定义错误和堆栈

为了更好地处理错误,我创建了这个方法:

/**
  *    Get parent function/method details
  *
  *    @param int counter [OPT] The counter allows to move further back or forth in search of methods detalis
  *
  *    @return array trace It contains those elements :
  *       - function - name of the function
  *       - file     - in which file exception happend
  *       - line     - on which line
  *       - class    - in which class
  *       - type     - how it was called
  *       - args     - arguments passed to function/method
  */

protected function getParentMethod( int $counter = 0 ) {
  $excep = new \Exception();
  $trace = $excep->getTrace();
  $offset = 1;
  if ( sizeof( $trace ) < 2 ) $offset = sizeof( $trace ) - 1;
  return $trace[$offset - $counter];
}

这将返回有关调用受保护方法的先前方法/函数的详细信息。

public function __call( $func, $args )
{
  $private = [
    "PrivateMethod" => null
  ];

  if ( !method_exists( $this, $func ) ) {
    $details = (object) $this->getParentMethod();
    throw new Error("Method $func does not exist on line " . $details->line . ", file: " . $details->file . " invoked by " . get_class($this) . $details->type . $func . " () ");
  }

  if ( isset($private[$func]) ) {
    $details = (object) $this->getParentMethod();
    throw new Error("Method $func is private and cannot be called on line " . $details->line . ", file: " . $details->file . " invoked by " . get_class($this) . $details->type . $func . " () ");
  }

  return $this->$func( ...$args );
}
  

这并不是什么大问题,但在调试时可能会导致一些混淆。

结论

此解决方案允许您控制来自类外部的所有私有/受保护方法的调用。任何this->Method将省略__call并通常调用私有/受保护方法。

class Test {
  
  public function __call( $func, $args )
  {
    echo "__call! ";
    if ( !method_exists( $this, $func ) ) throw new Error("This method does not exist in this class.");

    return $this->$func( ...$args );
  }

  protected function Public()
  {
    return "Public";
  }

  protected function CallPublic()
  {
    return "Call->" . $this->Public();
  }

}

$_Test = new Test();
echo $_Test->CallPublic(); // result: __call! Call->Public - it uses two methods but __call is fired only once

如果你想要在静态方法中添加类似的东西,可以使用__callStatic魔术方法。

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