This library allows to override fully qualified function calls inside your class methods in order to be able to mock them during testing.

NOTE: The library can be used for other scenarios as well. But we recommend to use it for testing purposes only.

Requirements

Installation

$ composer require --dev adriansuter/php-autoload-override 1.0

Simple Example

Say you have the following class Clock which contains one method now(). That method returns the result of the php-function time() from the global scope (fully qualified function call).

namespace My\App;

class Clock
{
    public function now(): int
    {
        return \time(); // <- Fully qualified function call
    }
}

Furthermore say you have a very simple script that consumes that class in the following form

require_once __DIR__ . '/vendor/autoload.php';

$clock = new \My\App\Clock();
echo $clock->now();

Whenever you run this script, the output would be the current unix timestamp. Now if you want to override the \time() function, for example to make sure that the output is always 1574333284, you can use the PHP-Autoload-Override library and simply modify your script

/** @var \Composer\Autoload\ClassLoader $classLoader */
$classLoader = require __DIR__ . '/vendor/autoload.php';

\AdrianSuter\Autoload\Override\Override::apply($classLoader, [
    \My\App\Clock::class => [
        'time' => function () {
            return 1574333284;
        }
    ]
]);

$clock = new \My\App\Clock();
echo $clock->now();

The output would be 1574333284 no matter the actual timestamp you run this script.

How does it work?

First the PHP-Autoload-Override library collects all classes that would be affected by an override. Then the library registers a stream wrapper such that it can handle file loading. The library instructs the standard autoload class loader from composer to load these affected classes. The class loader would then load the classes as well as their dependencies. The PHP-Autoload-Override intercepts the file loading and if it detects an affected class, it loads the source code and modifies the fully qualified function calls. Of course at the end, the modified source code would be loaded into the php runtime.

The library uses the PHP Parser to find the fully qualified function calls (global scope) and to perform the code conversion. It tries to leave the format of the code as untouched as possible.

In our own tests, the coverage report did work as before (the overrides did not disturb the reporting).

Usage

It is possible to override the fully qualified function calls (global scope) inside one class, or even for all classes of a specific namespace. Note that sub-namespaces would not be affected.

To define the function calls that should be overridden for a whole namespace, instead of writing the fully qualified class name as key, simply write the fully qualified namespace name, e.g.

\AdrianSuter\Autoload\Override\Override::apply($classLoader, [
    'My\\App\\' => [
        'time' => function () {
            return 1574333284;
        }
    ]
]);

You can either define a closure as override (see above) or use the well-known namespace technique. This technique would allow you to define the functions inside a namespace (other than global scope) and the PHP-Autoload-Override would override the corresponding function calls to use that namespace. By default, the namespace is PHPAutoloadOverride.

\AdrianSuter\Autoload\Override\Override::apply($classLoader, [
    'My\\App\\' => ['time']
]);

So the code converter would convert all function calls to \time() inside all classes of the namespace My\App into function calls \PHPAutoloadOverride\time(). Of course you would have to define those functions.

You can even customize the default namespace using the third argument of the \AdrianSuter\Autoload\Override\Override::apply() method.

If you would like to set the namespace for one specific function call only, then you can do that by simply writing it as key-value pair.

\AdrianSuter\Autoload\Override\Override::apply($classLoader, [
    'My\\App\\' => ['time' => 'My\\Special\\Override']
]);

Usage with PHPUnit

Say we want to unit test the following class Probability.

namespace My\App;

class Probability
{
    public function pick(int $probability, string $color1, string $color2): string
    {
        if (\rand(1, 100) <= $probability) {
            return $color1;
        } else {
            return $color2;
        }
    }
}

The class has one method pick that takes a probability (between 0 and 100) and two color names as arguments. The method would then use the rand function of the global scope to generate a random number and if the generated number is smaller equal to the given probability, then the method would return the first color, otherwise the method would return the second color.

After installing the PHP-Autoload-Override library, we would open the bootstrap script of our test suite (see also PHPUnit Configuration). There we will write the following code

// tests/bootstrap.php

/** @var \Composer\Autoload\ClassLoader $classLoader */
$classLoader = require_once __DIR__ . '/../vendor/autoload.php';

\AdrianSuter\Autoload\Override\Override::apply($classLoader, [
    \My\App\Probability::class => [
        'rand' => function ($min, $max): int {
            if (isset($GLOBALS['rand_return'])) {
                return $GLOBALS['rand_return'];
            }

            return \rand($min, $max);
        }
    ]
]);

Now the class Probability would be loaded into the PHPUnit runtime such that all function calls to the global scoped rand() function in the class Probability get overridden by the closure given above.

Our test class can now be written as follows.

namespace My\App\Tests;

use My\App\Probability;
use PHPUnit\Framework\TestCase;

final class ProbabilityTest extends TestCase
{
    protected function tearDown()
    {
        if (isset($GLOBALS['rand_return'])) {
            unset($GLOBALS['rand_return']);
        }
    }

    public function testPick()
    {
        $p = new Probability();

        $GLOBALS['rand_return'] = 35;

        $this->assertEquals('blue', $p->pick(34, 'red', 'blue'));
        $this->assertEquals('red', $p->pick(35, 'red', 'blue'));
    }
}

The test case testPick would call the pick method two times. As we have overridden the \rand function, we can control its returned value to be always 35. So the first call checks, if the else-block gets executed. The second one checks, if the if-block gets executed. Hooray, 100% code coverage.

Note that this override would only be applied during the unit tests.

APC User Cache

APC User Cache is not supported.