Extending final Classes and Methods by manipulating the AST

We know that we should always write unit tests for our code, and mock dependencies; but that isn’t always easy when we need to mock classes define as final, or that contain final methods. This isn’t normally a problem when we’re only working with classes within our own libraries and applications, because we control whether they are final or not, and we can type-hint these dependencies to interfaces. However, when the dependencies that we use are from external libraries, we lose that control; and it can become harder to test our own classes if we do need to mock final classes adn they haven’t been built to interfaces.

Libraries like Mockery do provide a proxy approach to mocking final Classes, which works well, but with one major limitation.

In a compromise between mocking functionality and type safety,
Mockery does allow creating "proxy mocks" from classes marked
final, or from classes with methods marked final. This offers
all the usual mock object goodness but the resulting mock will
not inherit the class type of the object being mocked, i.e. it
will not pass any instanceof comparison.

Unfortunately, this limitation still causes problems when passing the mock to a method with type-hinted arguments to the class rather than to an interface if we need to Dependency-inject the mock; or if we don’t have much choice because the the final class wasn’t implementing an interface. Library writers take heed if you define any of your classes as final; always write them with an interface that can be type-hinted.

So is there any way we can create a test double that matches type-hinting when we need to pass it into the class that we’re testing? I’ve been looking at an alternative approach to mocking that does allow me to create a test double that extends a final class.

I’ve been working for a while on developing my own mocking library (codenamed “Phpanpy“), for building test doubles. As part of that development, I’ve been exploring the functionality of Nikita Popov’s (@nikita_ppv’s) PHP_Parser library for  working with PHP’s Abstract Syntax Tree (or AST). In a previous incarnation of my library, I used Reflection for building those doubles, made easier with Roave’s BetterReflection. But nikita_ppv’s PHP_Parser library is really growing on me: not only does it allow me to analyse a class definition more cleanly and efficiently than using Reflection; but it also allows me to modify the code, or (in this case) to build a new class definition based on the original code, very easily. This is particularly useful when it is necessary to create a test double for a class that is define as final, or that contains public final methods that we need to mock, because we can rebuild the class definition itself to “de-finalise” it.


The first point to note is that PHP_Parser loads from a file. This is what I want as well: I don’t want to try parsing a class that’s already been loaded by PHP, because it’s already been lexed, parsed and compiled at that point, and I can’t modify it; so if I need to “de-finalise” the class or its methods so that it can be extended, then it’s already too late. It means that I need to identify the file containing the class that I want to mock without including/requiring it (as an autoloader would do); and I can then load the file using file_get_contents() and pass it to PHP_Parser.

If I want to stub a class in a library that is loaded through Composer, then there’s a rather neat (and very useful) feature in Roave’s BetterReflection that uses Composer’s ClassLoader to locate the source of a class without actually loading it, so I have a simple ComposerClassFileLocator which takes advantage of this to identify the source file.


use Composer\Autoload\ClassLoader;
use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflection\ReflectionClass;
use Roave\BetterReflection\Reflector\ClassReflector;
use Roave\BetterReflection\SourceLocator\Type\ComposerSourceLocator;

class ComposerClassFileLocator {
    protected $classLoader;

    public function __construct(ClassLoader $classLoader) {
        $this->classLoader = $classLoader;
    }

    public function getFileName(string $className) : string {
        $astLocator = (new BetterReflection())->astLocator();
        $reflector = new ClassReflector(new ComposerSourceLocator($this->classLoader, $astLocator));
        try {
            $reflectionClass = $reflector->reflect($className);
        } catch (\Exception $e) {
            throw new Exception("Unable to locate class {$className}");
        }

        return realpath($reflectionClass->getFileName());
    }
}

and a ComposerClassReader class


use Composer\Autoload\ClassLoader;
use PhpParser\Error;
use PhpParser\ParserFactory;

class ComposerClassReader {
    protected $reflectionClassLocator;
    protected $astParser;

    public function __construct(ClassLoader $classLoader) {
        $this->reflectionClassLocator = new Phpanpy\ComposerClassFileLocator($classLoader);
        $this->astParser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
    }

    public function getAst(string $className) : array {
        $fileName = $this->reflectionClassLocator->getFileName($className);
        try {
            $ast = $this->astParser->parse(file_get_contents($fileName));
        } catch (Error $error) {
            throw new Exception("File Parser error: {$error->getMessage()}");
        }

        return $ast;
    }
}

which I can then call using


$composerClassLoader = require "../vendor/autoload.php";

$ast = (new ComposerClassReader($composerClassLoader))
    ->getAst('Geodetic\\Line');

(with appropriate exception handling in case the file isn’t found, or contains parser errors, or other problems.)

The Roave BetterReflection library can return me the AST directly, but only for the class code itself, and I want the whole file (with any declares, uses, etc), and any additional classes that may be defined inside that file (not always considered good practise, but it does happen), not simply the class code.

Things are slightly different if the class that I want to mock isn’t loaded through Composer, which is normally the case for the classes of the library that I’m testing. The composer.json for that library is so that Composer can setup the autoloading of the files for library users, but not for myself as the developer. BetterReflection does provide an option to search through a list of folders and subfolders, so I could use that as an alternative to the ComposerSourceLocator; but I prefer a more direct approach.

When I test these classes, my unit test bootstrap already sets up a custom autoloader script; and I’ve modified that autoloader to provide a public method for locating the file without actually loading it.


class Autoloader
{
    public static function Register() {
        if (function_exists('__autoload')) {
            spl_autoload_register('__autoload');
        }
        return spl_autoload_register(array('Geodetic\\Autoloader', 'Load'));
    }

    public static function Locate(string $pClassName) : string {
        return realpath(
            __DIR__ . DIRECTORY_SEPARATOR .
            'src' . DIRECTORY_SEPARATOR .
            str_replace('\\', '/', str_replace('Geodetic\\', '', $pClassName)) .
            '.php'
        );
    }

    public static function Load(string $pClassName) {
        if ((class_exists($pClassName, FALSE)) || (strpos($pClassName, 'Geodetic\\') !== 0)) {
            return FALSE;
        }

        $pClassFilePath = self::Locate($pClassName);
        if ((file_exists($pClassFilePath) === FALSE) || (is_readable($pClassFilePath) === FALSE)) {
            return FALSE;
        }
        require($pClassFilePath);
    }
}

And my AST reader can then call the Locate() method to retrieve the filename, and load the AST from there.


use PhpParser\Error;
use PhpParser\ParserFactory;

class TestClassReader {
    protected $autoLoader;
    protected $astParser;

    public function __construct(string $autoLoader) {
        $this->autoLoader = new $autoLoader();
        $this->astParser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
    }

    public function getAst(string $className) : array {
        $fileName = $this->autoLoader::Locate($className);
        try {
            $ast = $this->astParser->parse(file_get_contents($fileName));
        } catch (Error $error) {
            throw new Exception("File Parser error: {$error->getMessage()}");
        }

        return $ast;
    }
}

and then call it using


$ast = (new TestClassReader('Geodetic\\Autoloader'))
    ->getAst('Geodetic\\Line');

 


So, having loaded the Abstract Syntax Tree for the .php file that contains the class definition, my next step is to modify the code that it represents in the way I need. Conveniently, PHP_Parser provides a Traverser, that allows me to add pre- and post-hooks to every node that’s visited, allowing me to write code that modifies those nodes, changing their properties, or even removing their sub-nodes, through a NodeVisitor class with enterNode() and leaveNode() methods.

As a first step, I want to traverse the AST looking for the code for the class that I’m interested in mocking, “de-finalising” it if it’s defined as final, and the same for any methods in that class. So I start by writing my NodeVisitor.


use PhpParser\NodeVisitorAbstract;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;

$nodeVisitor = new class extends NodeVisitorAbstract {
    protected $parsingTargetClass = false;
    public $className;

    public function enterNode(Node $node) {
        if (($node instanceof ClassMethod) && $node->isPublic() && $this->parsingTargetClass) {
            // If this node is a public method node within the class that we want to mock
            //    remove any final flag, because we want to override this method in our extension
            $node->flags &= ~Class_::MODIFIER_FINAL;
        } elseif (($node instanceof Class_) && ($node->name == $this->className)) {
            $this->parsingTargetClass = true;
        }
    }

    public function leaveNode(Node $node) {
        if ($node instanceof Class_) {
            // If the node is a class node
            if ($node->name == $this->className) {
                // If this is the class that we want to mock,
                //    remove any final flag, because we need to able to extend the class
                $node->flags &= ~Class_::MODIFIER_FINAL;
            }
            $this->parsingTargetClass = false;
        }
    }
};

This defines the logic that I want to be executed while I’m traversing the node tree. Next, I need to set up the Traverser itself, and attach my Node Visitor so that my code will be executed to modify the AST as each node is visited.


use PhpParser\NodeTraverser;
$nodeVisitor->className = 'Line';

$traverser = new NodeTraverser();
$traverser->addVisitor($nodeVisitor);

And finally, I actually traverse the AST, modifying it according to the logic that I’ve implemented, building a new tree.


$newClassAst = $traverser->traverse($ast);

If appropriate, I can also use these node hooks to modify any self references to static in those methods, if I want to override them in my extending mock.

In a similar manner, I also walk the AST using a different (and more complex) visitor that builds a new AST which will create an anonymous class that extends the original, with the bodies of each public method stubbed with the code that I want to use in the mock. This isn’t as difficult as it might sound, because I make extensive use of Traits that are added to the class; and have template method code that I can just “copy” directly in place of the original method code.


Now that I’ve taken that original AST, and used it to build a new AST with the class and all relevant public methods “de-finalised”; it’s  time to rebuild the code that’s mapped in $newClassAst. Again, PHP_Parser provides most of the logic for this, starting with its PrettyPrinter class.


use PhpParser\PrettyPrinter;

$prettyPrinter = new PrettyPrinter\Standard;
$newClassCode = $prettyPrinter->prettyPrintFile($newClassAst), PHP_EOL;

This returns the code that I re-built in the AST, assigning it as a string to $newClassCode (including the opening <?php tag. All that remains is for me to execute it, and my modified version of the class will be defined instead of the original version from the source file.

If the allow_url_include directive is enabled in php.ini, then it’s possible to execute this code using


include "data://text/plain;base64," . base64_encode($newClassCode);

but this setting is disabled by default, and cannot be changed within user code, but only through editing the php.ini file; so unless explicitly enabled in php.ini (and there normally isn’t any good reason why it should be), then it isn’t really an option.
An alternative is to create a temporary file, write the code there, and then execute it using include:


$tempFilename = tempnam("/tmp", "Phpanpy");
file_put_contents($tempFilename, $newClassCode);
include $tempFilename;
unlink($tempFilename);

while a third option is to use eval().


eval('?>' . $newClassCode);

Any of these approaches will load the new class definition into PHP, effectively bypassing the autoloader. And with the parent class now loaded, I can do the same for with the AST that I have built for my mock that extends the parent. I could equally use PHPUnit’s own mock builder (or Mockery) because the class that’s being mocked is no longer a final class; and any code that uses instanceof (or any typehints) will see the mock as a match.

So taking this approach, creating test doubles for final classes or classes with final methods isn’t such a problem.


Just for reference if you’re a library developer, here’s some advice on when it might be appropriate to use final in your classes. It’s an older article  by Marco Pivetta (aka @Ocramius); and while I may not agree with everything that he says, his comments about using Composition in preference to Extension are solid.

Advertisements
This entry was posted in PHP and tagged , , , . Bookmark the permalink.

One Response to Extending final Classes and Methods by manipulating the AST

  1. Pingback: PHP Annotated Monthly – December 2017 | PhpStorm Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s