Goblin Injection: A Novel Way Of Testing Static Services In PHP ๐ŸงŒ

Goblin Injection: A Novel Way Of Testing Static Services In PHP ๐ŸงŒ

A crafty helper on the road to a testable codebase

ยท

9 min read

If you have ever worked with (legacy) PHP codebases then you might have come across (service) classes filled without state and filled with static functions. While this pattern allows you to easily use any service method from any place, it is generally considered hard to test because of the impossibility to reliably mock static methods. In this article, we explore a novel way that might help us move gradually toward sanity!

Note: this article assumes a working knowledge of Dependency Injection and Mocking.

The problem

Let us start with an example:

<?php

class StaticService {
    public static function isMeaningOfLife(): bool {
        return ExternalAPI::expensive() === 42;
    }
}

class StaticService2 {
    public static function isNotMeaningOfLife(): bool {
        return !StaticService::isMeaningOfLife();
    }
}

class ExternalAPI {
    public static function expensive(): int {
        wait(23);
        return 42;
    }
}

Outlined are three services that expose only static methods. Say we want to test whether StaticService::isMeaningOfLife() behaves as expected, but we don't want to wait 23 seconds. How could we go about this? Because the call is static there is no way to mock it because there is no way to inject it. There are a few options, such as 'monkey patching' where you dynamically instruct the autoloader to load a completely different class or by creatively using late static binding like phake offers. However, these methods are notoriously unstable and hard to maintain for a number of reasons, including:

  1. In the case of monkey patching you need to specify a real class in a real file for every class you want to mock, creating a lot of boilerplate and complexity (to the point where you might want to start testing your tests...).

  2. Both methods introduce state in opaque ways that will persist over multiple tests unless you specifically reset it. Making it hard to reason about and isolate tests.

So given the current situation, we are out of luck and will either have to wait the full 23 seconds or embark on a potentially treacherous path...

<?php

class StaticServiceTest extends TestCase {
    //This will take a very long time :(
    public function testIsMeaningOfLife() {
        $this->assertTrue(StaticService::isMeaningOfLife());
    }
}

The problematic solution

A much more versatile and therefore preferred way of talking to services is via 'Dependency Injection' (DI). DI allows you to inject all dependencies from the outside, enabling very flexible testing strategies and several other advantages (too in-depth to cover here, there are lots of other excellent resources to learn). However, refactoring an application with a static architecture toward DI can be quite a challenge. Often, classes with dependencies are themselves dependencies of other classes and introducing a constructor would require you to also refactor those classes (clients) because it's now their responsibility to provide the dependencies. And like pulling a thread on a shirt, it starts small but before long the whole shirt is gone.

Continuing with our previous example. To not wait 23 seconds we would have to change the behavior ExternalAPI, but since StaticService is static we have no way of modifying this dependency from the outside. Luckily for us, you can call static methods on objects in PHP, so we don't have to refactor ExternalAPI::expensive() as it can remain static. However, if we refactor StaticService towards DI by introducing a constructor, we also have to refactor StaticService2 since it's using ::isMeaningOfLife(). Even more problematic, in order to use this constructor StaticService2 now needs to know and get its hands on ExternalAPI (a detail neatly abstracted away before).

As you might begin to understand, in a real-life codebase with many interdependencies, refactoring one class towards DI might force you to refactor a large part of the application at once.

In software engineering, as a rule, we try to prevent large overhauls and aim to introduce change incrementally. That way it's easier to write the code, easier to review before deployment, and easier to find out what went wrong when things eventually go sideways.

The crafty solution

In most languages you are out of luck, but PHP has some strange properties that allow us to cheat a bit and refactor toward DI incrementally. I, somewhat mockingly, call this method Goblin Injection. Because let's be honest, even though it's crafty it does feel a bit dirty...

However, by using this method we can refactor towards DI on a per-class basis. This means that any dependencies or clients of this class do not need to be refactored. Effectively circumventing the whole thread-pulling-scenario described earlier.

First we will look at the solution and then we will try to explain why exactly this works.

<?php

class StaticService {
    //If nothing is injected we will default to ExternalAPI::class
    private static $externalAPI = ExternalAPI::class;

    public function __construct(?ExternalAPI $externalAPI) {    
        if ($externalAPI !== null) self::$externalAPI = $externalAPI;
    }

    public function isMeaningOfLife(): bool {
        //Very strange syntax, I know, we will come back to this
        return self::$externalAPI::expensive() === 42;
    }
}

//The client class StaticService2 will remain untouched
//The dependency class ExternalAPI will remain untouched as well
<?php

class StaticServiceTest extends TestCase {
    //Still works as a static service and is still slow
    public function testIsMeaningOfLifeSlow() {
        //Clients like StaticService2 do not need to be refactored
        $this->assertTrue(StaticService::isMeaningOfLife());
    }

    //This will be super fast!
    public function testIsMeaningOfLifeFast() {
        //Create a mock of our dependency
        $mockExternalAPI = new class extends ExternalAPI {
            public static function expensive(): int { return 42; }
        }

        //Inject mock into our class to be tested
        $service = new StaticService($mockExternalAPI);
        $this->assertTrue($service::isMeaningOfLife());
    }
}

It looks a bit funny, right? The strange thing about PHP is that you can call a static method on both a class instance and its Fully Qualified Namespace (FQN), using exactly the same syntax.

Let us illustrate that with some code:

<?php

class Example {
    public static function test(): int {
        return 23;
    }
}

//Both legal ways of calling static methods
$example = new Example();
$example::test(); //class instance
$example = ExternalAPI::class;
$example::test(); //FQN

Let's try to break down what we did, step by step:

  1. Introduce a constructor into the class that we want to test (StaticService). This constructor will take as a nullable argument the dependency we want to mock (ExternalAPI). Never mind that this dependency only exposes static methods. It's still a class so we can instantiate it.

  2. If we decide to inject this dependency class then we will set it as a static property called $externalAPI. However, if we do not inject this dependency class, the argument will become null and our static property to will default to ExternalAPIService::class. This is where the magic comes in: Because as I mentioned, you can call static methods on both Fully Qualified Namespaces AND instances of objects. It works either way with the same syntax.

  3. We replace all calls to our static dependency with the newly introduced static property. So in this case ExternalAPI::expensive() becomes self::$externalAPI::expensive(). This will make sure that if we do inject something, the method gets called on the injected class instance and if we don't inject anything it will be called on the FQN.

  4. In our test we can now make a mock of ExternalAPIService, overwrite the expensive method with something that allows us to test what we want and inject it into our newly introduced constructor. After this, we can call the method we want to test without having to wait 23 seconds! For creating the mock you can choose whatever library you want. For more information on making mocks without a library like I am doing here check out this excellent article.

That's all folks!

Via this road we have now satisfied the following properties:

  1. All clients of StaticClass can still use all of its methods statically and thus do not need to be refactored (see testIsMeaningOfLifeSlow()).

  2. Our dependency class ExternalAPI does not need to be altered.

  3. Thus any other clients of ExternalAPI do not need to be altered.

  4. Any other static dependencies of StaticService can remain as they are, they do not need to be injected. Allowing you to refactor every dependency incrementally also within a class.

  5. The introduced code for StaticService looks pretty similar to normal dependency injection, requiring minimal refactoring in the next step.

  6. Some IDEs (at least PHPStorm) are smart enough to recognize that the static property defaults to ExternalAPI::class and will provide full code intelligence out of the box (including automatic refactors).

In other words, we have introduced the ability to alter the behavior of ExternalAPI in our tests for StaticService in complete isolation. This way we can introduce tests with mocks for one or a few classes without having to do a huge overhaul all at once.

Next steps

While this is all good and well, we might not want to stay in Goblin territory forever. Let's say we want to refactor the whole codebase to DI. What could be some next steps?

  1. Refactor all other clients of a dependency class toward 'goblin injection', similar to what we did before.

  2. Refactor the dependency class itself toward normal non-static methods and update all clients.

    Example:

<?php

class StaticService {
    private ExternalAPI $externalAPI;

    public function __construct(?ExternalAPI $externalAPI = null) {
    //Building this locally gets very painful, very fast
    if ($externalAPI === null) $this->externalAPI = new ExternalAPI();
}

    public function isMeaningOfLife(): bool {
        return $this->externalAPI->expensive() === 42;
    }
}

class ExternalAPI {
    public function expensive(): int {
        wait(23);
        return 1337;
    }
}

Remember to also update your tests:

<?php

class StaticServiceTest extends TestCase {
    public function testIsMeaningOfLifeFast() {
        //Create a mock of our dependency
        $mockExternalAPI = new class extends ExternalAPI {
            public function expensive(): int { return 42; }
        }
        $service = new StaticService($mockExternalAPI);
        $this->assertTrue($service->isMeaningOfLife());
    }
}
  1. Rinse and repeat for all clients of those classes until the whole codebase is refactored.

class StaticService {
    public function __construct(private readonly ExternalAPI $externalAPI) {}

    public function isMeaningOfLife(): bool {
        return $this->externalAPI::expensive() === 42;
    }
}

class StaticService2 {
    public static function isNotMeaningOfLife(): bool {
        //It's already getting more painful
        return !(new StaticService(new ExternalAPI())->isMeaningOfLife;
    }
}

DI Containers

As you can see in the last examples: in step 2 we have to instantiate ExternalAPI in the StaticService constructor. In step 3 we even have to instantiate both StaticService and ExternalAPI in StaticService2. Since these example classes have no other dependencies this might not seem to be a big deal, but you will probably not be that lucky in real life.

This is an excellent time to start using a DI container, preferably one that supports auto-wiring (PHP-DI for instance). This way you don't have to worry about constructing dependencies yourself, best case scenario they will be automatically injected from the container.

In the end

The choice is yours. If you want to test just one or a few classes: you can refactor those in isolation and be done with it. But rinse and repeat steps 1 to 3 enough times and soon enough you'll be on your way to completely testable architecture. Goblin mode, engage!

Ideas for future research

Since it's such a clearly defined process it should be possible to automate this with something like Rector. I have not played around with this yet but plan to do so in the future.

Request for the reader

Do you have a favorite resource that explains DI and or mocking? Do you have comments or suggestions? Please do not hesitate to reach out via the comments or send me an email at

ย