动态覆盖父类中的所有方法


Dynamically override all methods in parent class

我正在尝试构建一个动态模拟(术语使用不严格),原因如下:

  1. 这个练习将帮助我了解更多关于测试的知识。
  2. 大多数嘲弄系统都满足不了我的需求。
  3. 大多数嘲弄系统都不能以我想要的方式做我需要的事情。

换句话说:请不要告诉我去使用一个mock库。我已经使用了mock库(我已经广泛使用了至少三个PHP库),我决定尝试创建自己的解决方案是有意识的。

所以:我正在尝试做一些概念上看起来相当简单的事情。

我如何动态地覆盖所有方法在我的模拟类存在于类的模拟扩展?

换句话说,如果我有包含方法a的类A,并且我有扩展类A的模拟B,我怎么能捕获对方法A的所有调用而不显式地在模拟类B中实现方法a ?

我试着用__call()魔法方法来做这件事,但这不起作用,因为__call()只捕获对不存在的方法的调用。

我想要避免需要大量架构更改的方法。我在这里的主要要求是,任何在其构造函数中需要类A实例的类必须不能告诉mock B不是类A的实例。因此,我初步选择让模拟类B扩展类A。我也不希望对类A进行大的更改,例如将其方法设置为private并让它使用__call()

像这样:

class A
{
    public function foo()
    {
        return __CLASS__;
    } 
}
class B extends A
{
    public function foo()
    {
        return __CLASS__;
    }
}
$b = new B();
$reflection = new ReflectionObject($b);
$parentReflection = $reflection->getParentClass();
$parentFooReflection = $parentReflection->getMethod('foo');
$data = $parentFooReflection->invoke($b);
echo $data;

注意,在invoke(object $object, methods argument1, methods argument2...)函数中,可以在第一个参数之后传递方法参数

你可以用Reflections做很多类似的事情,更多信息请查看链接http://php.net/manual/en/book.reflection.php

我接受了@dm03514的建议,跳进了Phokito的源代码。Phokito至少使用模拟构建器动态生成模拟类定义,该定义扩展基类并覆盖其所有方法,然后使用eval声明已定义的类。

下面是基于这种方法的基本mock构建器:
class MockFactory
{
    public function buildMock( $class )
    {
        $reflection = new 'ReflectionClass( $class );
        $mockedShortName = $reflection->getShortName();
        $mockShortName = "Mock$mockedShortName";
        $mockClass = "''$mockShortName";
        if ( ! class_exists($mockClass) ) {
            $this->declareMockClass( $reflection, $mockShortName, $mockedShortName );
        }
        return new $mockClass;
    }
    private function declareMockClass( $reflection, $mockShortName, $mockedShortName )
    {
        $php = [];
        $mockedNamespace = $reflection->getNamespaceName();
        $extends = $reflection->isInterface() ? 'implements' : 'extends';
        $php[] = <<<EOT
class $mockShortName $extends $mockedNamespace''$mockedShortName {
    public function setReturnValue( '$method, '$returnValue ) {
        '$this->'$method = '$returnValue;
    }
    public function getCalls ( '$method ) {
        '$callsProperty = '$method . "Calls";
        return '$this->'$callsProperty;
    }
EOT;
        foreach ( $reflection->getMethods() as $method ) {
            $methodName = $method->name;
            $params = [];
            foreach ( $method->getParameters() as $i => $parameter ) {
                if ( $parameter->isArray() ) $type = 'array ';
                else if ( $parameterClass = $parameter->getClass() ) $type = '''' . $parameterClass->getName() . ' ';
                else $type = '';
                $params[] = "$type '${$parameter->getName()}";
            }
            $paramString = implode( ',', $params );
            $php[] = <<<EOT
    private '${$methodName}Calls = [];
    public function $methodName($paramString) {
        '$this->{$methodName}Calls[] = func_get_args(); 
        return '$this->$methodName;
    }
EOT;
        }
        $php[] = '}';
        $toEval = implode( "'n'n", $php );
        eval( $toEval );
    }
}