Annotated Container Without Attributes
In blog articles and documentation about Annotated Container I talk a
lot about using the library with Attributes. I like wiring my Container this way and believe it is a neat feature. However,
it isn't required to use Attributes with Annotated Container. Attributes are just one way to configure
Cspray\AnnotatedContainer\Definition\ContainerDefinition
implementation. I actually consider this interface, and the
other interfaces in the Cspray\AnnotatedContainer\Definition
namespace, to be the most important feature and a key
design goal with the library.
The ContainerDefinition
is a way to declare what your Container should have in it without defining how that Container
is created. I believe that this has a tremendous amount of value.
- You can create a variety of different Container implementations. Annotated Container has support for 3 implementations at the date of this article, and has plans for more.
- Static analysis and other validation can happen on your Container wiring before any objects are created or your app
actually runs. I've started some of this already with the
./vendor/bin/annotated-container validate
command that runs a series of checks on yourContainerDefinition
. There are plans to expand on the checks and add support for other static analysis tools. - You can create IDE and other tooling to inspect and see more information about what's in your Container. This is a long-term goal, but I want to have IDE plugins that allow for easily searching and viewing details of your Container wiring.
I gave all this context to show that I don't think Attributes are all that important in the grand scheme of things. They are the way I prefer to wire my Container, but you don't have to follow my preferences! The rest of this document shows how you can use the library without Attributes at all or how you can use Attributes while highly limiting their spread across your codebase.
Annotated Container Without Attributes
In the docs "Adding Third-Party Services"
I talk about how there are some situations you simply can't put Attributes on code you want to wire. After the library
has analyzed your code for Attributes a Cspray\AnnotatedContainer\Definition\DefinitionProvider
is invoked, if you've provided one. Using a series of low-level functions you can do everything that you otherwise might
do with an Attribute.
<?php declare(strict_types=1);
namespace Acme\MyContainerLayer;
use Cspray\AnnotatedContainer\StaticAnalysis\DefinitionProvider;
use Cspray\AnnotatedContainer\StaticAnalysis\DefinitionProviderContext;
use function Cspray\AnnotatedContainer\alias;
use function Cspray\AnnotatedContainer\service;
use function Cspray\AnnotatedContainer\serviceDelegate;
use function Cspray\AnnotatedContainer\servicePrepare;
use function Cspray\AnnotatedContainer\injectMethodParam;
use function Cspray\AnnotatedContainer\injectProperty;
final class MyDefinitionProvider implements DefinitionProvider {
public function consume(DefinitionProviderContext $context) : void {
// use the imported functions to wire up your Container
}
}
If you're using Annotated Container's default functionality you should ensure this class is defined in your
annotated-container.xml
file. Otherwise, you should ensure it is added to your Container's bootstrapping using
whatever mechanism you've designed. With implementations of DefinitionProvider
you can define your Container
without ever making use of Attributes.
Annotated Container With Limited Attributes
Perhaps you aren't opposed to using Attributes, you just don't want them spread across your codebase. You'd prefer the
places you're marking are limited in scope and not part of your domain code. This is possible with judicious use of the
#[ServiceDelegate]
Attribute. This Attribute allows you to define a class method, static or instance, that will be
invoked when the service is created. You can keep these factory methods in their own namespace and ensure service wiring
Attributes are only used in this namespace.
In this example I'm also going to demonstrate another core principle behind Annotated Container. Do not use the service locator pattern! Generally speaking, you should not declare your Container as a type in constructor dependencies, setter dependencies, or methods. Even in factories! Using a Container like this leads to code that's harder to reason about, refactor, and test.
<?php declare(strict_types=1);
namespace Acme\MyServiceLayer {
class Foo {}
class Bar {
public function __construct(private readonly Foo $foo) {}
}
class Baz {
public function __construct(
private readonly Foo $foo,
private readonly Bar $bar
) {}
}
}
namespace Acme\MyContainerLayer {
use Acme\MyServiceLayer\Foo;
use Acme\MyServiceLayer\Bar;
use Acme\MyServiceLayer\Baz;
use Cspray\AnnotatedContainer\Attribute\ServiceDelegate;
class MyFactory {
#[ServiceDelegate]
public static function createFoo(): Foo {
return new Foo();
}
#[ServiceDelegate]
public static function createBar(Foo $foo): Bar {
return new Bar($foo);
}
#[ServiceDelegate]
public static function createBaz(Foo $foo, Bar $bar): Baz {
return new Baz($foo, $bar);
}
}
}
In v2.2 and lower, you would also need to declare each type created by a #[ServiceDelegate] as an explicit Service; either with Attributes or with the functional API. In v2.3+ this requirement has been lifted and declaring a #[ServiceDelegate] for a type that is not a defined Service will implicitly make it a defined Service.
In this example we're using a minimal amount of Attributes to define our Container wiring. The use of those Attributes is confined to its own namespace and isn't polluting your service or domain layers. Additionally, and most importantly in my opinion, we're constructing an object with dependencies and not passing a Container around!
Attributes are a means to an end... use different means!
Although I prefer Attributes, they're just a means to an end. That end being a declaration of what your Container should have in it so that a variety of Container implementations, static analysis, and tooling can be built with it. There are other means to this end. If you don't like my preference, you should use those instead!