Nathan Arthur Nathan Arthur - 1 month ago 8
PHP Question

Dynamically override all methods in parent class

I'm trying to build a dynamic mock (term used loosely) for a few reasons:


  1. The exercise will help me learn more about testing.

  2. Most mocking systems do too much for my needs.

  3. Most mocking systems don't do the things I do need in the way I'd like.



In other words: Please don't tell me to go use a mocking library. I already use mocking libraries (I've used at least three PHP libraries extensively), and my decision to try my hand at creating my own solution was a conscious one.

So: I'm trying to do something that conceptually seems rather simple.

How can I dynamically override all methods in my mock class that exist in the class the mock extends?

In other words, if I have class
A
which includes method
a
, and I have a mock
B
which extends class
A
, how can I catch all calls to method
A
without explicitly implementing method
a
in mock class
B
?

I've tried to do this with the
__call()
magic method, but this won't work because
__call()
only catches calls to methods which don't exist.

I'd like to avoid approaches that require large architectural changes. My main requirement here is that any class which requires an instance of class
A
in its constructor must not be able to tell that mock
B
is not an instance of class
A
. Hence my preliminary choice of having mock class
B
extend class
A
. I'd also rather not have to make large changes to class
A
, such as setting its methods to private and having it use
__call()
, as well.

Answer

I took @dm03514's advice and jumped into Phokito's source. Phokito, at least, uses a mock builder to generate a mock class definition on the fly which extends the base class and overrides all its methods, and then uses eval to declare the defined class.

Here's my basic mock builder based on this approach:

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 );
    }
}