cspray / annotated-container

Dependency Injection framework to configure a PSR-11 Container with Attributes!
MIT License
39 stars 1 forks source link

Allow retrieving a collection of Services matching a given type #33

Closed cspray closed 5 months ago

cspray commented 2 years ago

Problem

There are scenarios where having multiple concrete services is not only expected but desired functionality. In such a use-case it would be challenging to actually inject any arbitrary number of services that could come from multiple sources, consider that the concrete service might be satisfied by a vendor or the user of your package.

A real use-case exists in Labrador Core where a Plugin interface exists. Multiple implementations are expected to be created and the piece of the framework responsible for interacting with these implementations require all of them. Currently there is no way to accomplish this with Annotated Container out of the box. You could create a #[ServiceFactory] but that might be a heavy-handed approach to simply get an array of objects that match a specific type. Additionally, it would still require some knowledge ahead of time as to what Plugins should be created and any new Plugin would require that configuration to change.

Solution

To solve this challenge a new Attribute called #[ServiceCollection] will be introduced. The Attribute could be annotated on the following targets:

Additionally, the type on the parameter or property MUST satisfy either an array or a variadic object type representing a service. Depending on the type of the parameter or property an argument will be required to satisfy the type of service that should be included in the collection.

Technical Details

There are technical challenges to overcome depending on the backing container implementation. There isn't a straightforward way to supply an array of objects without resorting to some code that feels hack-ish. More thought will need to be given on how to accomplish this feature.

Code Example

<?php declare(strict_types=1);

namespace Cspray\AnnotatedContainer\ServiceCollectionDemo;

use Cspray\AnnotatedContainer\Attribute\Service;
use Cspray\AnnotatedContainer\Attribute\ServiceCollection;
use Cspray\AnnotatedContainer\Attribute\ServicePrepare;
use Cspray\AnnotatedContainer\Attribute\Configuration;

#[Service]
interface Widget {}

#[Service]
class FooWidget implements Widget {}

#[Service]
class BarWidget implements Widget {}

#[Service]
class BazWidget implements Widget {}

#[Service]
class ConstructorWidgetConsumer {

    public function __construct(#[ServiceCollection(Widget::class)] array $widgets) {}

}

#[Service]
class ServicePrepareWidgetConsumer {

    #[ServicePrepare]
    public function addWidgets(#[ServiceCollection] Widget... $widgets) {}

}

#[Configuration]
class WidgetConfig {

    #[ServiceCollection(Widget::class)]
    public readonly array $widgets;

}

Note that because the ServicePrepareWidgetConsumer is using a variadic argument representing another service we can resolve the type for the collection by the type-hint.

cspray commented 2 years ago

An unforeseen complication is a requirement that we implicitly create our own service delegate when we create the Container. We can't just pass an array of strings and expect Auryn, or any other backing implementation, to handle that correctly. We need to have more complete control over how an object is created when it has been annotated with #[ServiceCollection]. In addition, I'm not yet certain if Auryn, or any other backing implementation, would allow us to easily provide this collection when #[ServiceCollection] is used on a non-construct method parameter in conjunction with #[ServicePrepare]. Due to these limitations it was decided to hold off on this feature and push it out of 0.3. We will revisit this feature either before a 1.0 push or when users are involved to get other opinions.

cspray commented 2 years ago

Regarding the #[ServicePrepare] issue I believe that this is not a concern. The most current implementation of the AurynContainerFactory can effectively handle this by directly determining which parameters to pass to Injector::execute(). I believe we'll learn more when add the ability to have different containers but I believe any container that would support the already existing functionality would be able to support this as well.

cspray commented 2 years ago

The only real concern with this is that you could potentially define a #[ServiceDelegate] for a type that also has a #[ServiceCollection] annotation. We'll need to figure out whether to fail early or allow the object to be constructed with a warning and defer to the explicit #[ServiceDelegate] annotated.

cspray commented 2 years ago

When you think about it, the #[Inject] Attribute is already responsible for deciding what values get used when they can't be resolved implicitly. Instead of introducing a new Attribute, and everything else that entails, we should instead adjust the way the #[Inject] Attribute is handled to support this. Really, there's nothing to do from the #[InjectAttribute] or InjectDefinition side. Instead, need to determine if variadic arguments are supported (probably not at first) and to adjust the ContainerFactory tests and implementations to handle this use case.