An alternative for a DI-container in WordPress plugin development

Briefly, the dependency injection helps us to work with dependencies: makes them observable and enables to substitute them whenever we need it.

My choice of preference is to inject dependencies through a constructor. Here, we have both the observability and the replaceability. As a bonus, the object is completely instantiated and ready for future work. However, it becomes harder to create new objects and our code significantly suffers if we try to do that in a straightforward fashion by adding a few arguments to the constructor call. As we (usually) want to maintain testability and flexibility of our code, we have to extract the creation of the object in a factory.

The popular approach here is to use the DI-container for that purpose. The major PHP frameworks, like Laminas and Symfony, have their own implementations. Sometimes they can handle everything for you: you just have to configure the container and follow a few simple rules. There are also framework-agnostic solutions that you can add to the app.

About a year ago, I joined Automattic and dived into WordPress plugin development. I found a lot of traditions and conventions here. From my perspective, some of them are outdated, sometimes are toxic (in terms of software health) and need a revision. For example, it is really hard to write unit tests there. If you check popular plugins’ code you probably find some unit tests, but those are a mixture of integration tests and unit tests. I will not develop this topic here, but it might be interesting to discuss it later.

I started experimenting with writing testable code, but the conventional approach of creating objects and introducing dependencies was blocking. To write a “pure” unit test, you need to have an opportunity to substitute the dependency, and that’s hard when it is instantiated deep in the roots of your logic.

I tried to introduce a DI-container in my experimental project, but it turned out that you need to carry the container everywhere through the code to make it available. One option was to make it global: use global variable sounded terrible for me. Another version of the global state here was to use a function from the global state or a singleton.

I tried to find a tradeoff. But then I realized that I don’t need a container as an external library. If I’m okay with a static factory, it is easier to write the simple one on my own.

I remembered that approach from The Art of Unit Testing by Roy Osherove.

You create a class with a static method that creates an instance of a class and returns it. However, you also have an option to substitute the returning value: for tests, for example.

If you’re interested in how to implement this approach, you can check WPAL, my attempt to build an abstraction over the WordPress API.

Here is the excerpt from the ServiceFactory class that creates the implementation for the Hooks abstraction:

class ServiceFactory {
    /**
     * @var Hooks|null
     */
    private static $custom_hooks;

    public static function set_custom_hooks( ?Hooks $hooks ): void {
        self::$custom_hooks = $hooks;
    }

    public static function create_hooks(): Hooks {
        if ( self::$custom_hooks ) {
            return self::$custom_hooks;
        }
        return new WpHooks();
    }
}

The worst part of this approach, from my point of view, is that you create dependencies inside the constructor of a dependant. Not providing those dependencies through constructor’s arguments, we make them less visible. If you look at the constructor’s code, everything is well observable, and the object is completely ready for use after the constructor call. So, that’s my trade off here.

This is how the usage of the ServiceFactory looks like:

class Example {
    /**
     * @var Hooks
     */
    private $hooks_api;

    /**
     * @var string
     */
    private $plugin_file;

    public function __construct( string $plugin_file ) {
        $this->plugin_file = $plugin_file;
        $this->hooks_api = ServiceFactory::create_hooks();
    }

    public function init() {
        $this->hooks_api->add_action( 'init', [ $this, 'register_taxonomies' ] );
        $this->hooks_api->add_action( 'init', [ $this, 'register_post_types' ] );
    }

    public function register_post_types(): void { /* ... */ }

    public function register_taxonomies(): void { /* ... */ }
}

And the test for the init method:

final class ExampleTest extends TestCase {
    public function testInit_WhenCalled_AddsAction(): void {
        $hooks_api = $this->createMock( Hooks::class );
        ServiceFactory::set_custom_hooks( $hooks_api );
        $contacts = new Contacts( 'a' );

        $hooks_api
            ->expects( $this->exactly( 2 ) )
            ->method( 'add_action' )
            ->withConsecutive(
                [ 'init', [ $contacts, 'register_taxonomies' ] ],
                [ 'init', [ $contacts, 'register_post_types' ] ]
            );
        $contacts->init();
    }
}

Talking about downsides of this approach, I can think about the global scope where the factory operates. On the other hand, DI-containers represent the same global scope problem, but covered with object-oriented tricks.

Using a static factory looks preferably for me in the WordPress plugin development as we don’t have to bring another external dependency to the app, and the complexity of the solution looks relatively lower (comparing to learning, introducing and using the DI-container).


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.