Cartoon avatar of Charles Sprayberry
cspray.io

Injecting Service Collections with Annotated Container

The ability to inject a collection of services using idiomatic Annotated Container code has long been a desire of mine. In fact, Annotated Container's oldest open issue (at the time of this posting) tracks this exact feature. The issue is 2+ years old and originally targets the v0.3 release of Annotated Container. Sadly, every time I tried to implement this feature in the past I ran into a seemingly insurmountable hurdle. This was compounded by the fact I want to maintain feature parity across the different Container libraries that are supported.

Finally, after a change in how I was approaching a solution and a deep dive into each of the supported Containers, I managed to find an implementation that solves all of my wants for this feature! In the below example we'll take a look at how we can inject collections of services.

Injecting Array of Services

With how ubiquitous the array is in PHP, injecting an array of services is bound to be a common use-case. Let's assume we have the following code:

<?php declare(strict_types=1);

use Cspray\AnnotatedContainer\Attribute\Service;

#[Service]
interface Widget {}

#[Service]
final class CounterWidget implements Widget {}

#[Service]
final class BannerWidget implements Widget {}

#[Service]
final class ClockWidget implements Widget {}

#[Service]
final class WidgetDashboard {

    /**
     * @param list<Widget> $widgets
     */
    public function __construct(
        public readonly array $widgets
    ) {}

}

In Annotated Container prior to 2.4 it was extremely difficult to satisfy this requirement. None of the options are very good. If you used a #[ServiceDelegate], a way to mark a method as a factory for your services, you'd still need to know which implementations to use in the collection. The Observer system allowed for fetching collections of services, but it was only after the object had been created.

In versions 2.4 and above, you can now use the #[Inject] attribute and implementations of a new interface, Cspray\AnnotatedContainer\ContainerFactory\ListOf to support injecting collections in constructor parameters! After 2.4, we can make the following adjustment to our WidgetDashboard implementation.

<?php declare(strict_types=1);

use Cspray\AnnotatedContainer\Attribute\Service;
use Cspray\AnnotatedContainer\Attribute\Inject;
use Cspray\AnnotatedContainer\ContainerFactory\ListOfAsArray;

#[Service]
final class WidgetDashboard {

    /**
     * @param list<Widget> $widgets
     */
    public function __construct(
        #[Inject(new ListOfAsArray(Widget::class))]
        public readonly array $widgets
    ) {}

}

The new functionality comes where we're passing in an instance of ListOfAsArray, with the class type that we expect to be injected into this parameter. From an end user's perspective, that's basically it! Whatever services are defined that implement the Widget interface will be part of the injected array.

Injecting Custom Collection of Services

What if you don't pass around arrays and have a custom, type-safe collection object? No worries, Annotated Container's new feature supports this use case as well! Let's assume that our widget example looks like the following instead:

<?php declare(strict_types=1);

use Cspray\AnnotatedContainer\Attribute\Service;

#[Service]
interface Widget {}

#[Service]
final class CounterWidget implements Widget {}

#[Service]
final class BannerWidget implements Widget {}

#[Service]
final class ClockWidget implements Widget {}

final class WidgetCollection {

    public function __construct(Widget... $widgets) {}

}

#[Service]
final class WidgetDashboard {

    public function __construct(
        public readonly WidgetCollection $widgets
    ) {}

}

To support this use case we can use similar functionality as the array use case, but we'll have to provide our own implementation of ListOf. This is what that might look like:

<?php declare(strict_types=1);

use Cspray\AnnotatedContainer\Attribute\Service;
use Cspray\AnnotatedContainer\Attribute\Inject;
use Cspray\AnnotatedContainer\ContainerFactory\ListOf;
use Cspray\Typiphy\ObjectType;
use function Cspray\Typiphy\objectType;

final class ListOfAsWidgetCollection implements ListOf {

    /**
     * @param class-string $type
     */
    public function __construct(
        private readonly string $type
    ) {}

    public function type() : ObjectType {
        return objectType($this->type);
    }

    public function toCollection(array $widgets) : WidgetCollection {
        return new WidgetCollection(...$widgets);
    }

}

#[Service]
final class WidgetDashboard {

    /**
     * @param list<Widget> $widgets
     */
    public function __construct(
        #[Inject(new ListOfAsWidgetCollection(Widget::class))]
        public readonly WidgetCollection $widgets
    ) {}

}

In this scenario, we get our WidgetCollection injected thanks to our new ListOf implementation. With the provided ListOfAsArray or your own custom ListOf implementation a variety of use cases for injecting a collection of services can be satisfied. I anticipate releasing Annotated Container 2.4 over the next few days and will be sure to make use of this feature in my own apps.

Happy dependency injection!