Rather than defining injection via $di->params
, $di->setters
, $di->values
or $di->types
, you can also annotate
parameters using the native attributes available from PHP 8.0.
For now, only constructor parameters can be annotated.
The advantage would be that you have the injection decisions included in the class itself, keeping it all
together. Moreover, when your application grows, the length of your code constructing the container might grow to such size
that maintaining it becomes a problem. This might even be the case when you have separated it with ContainerConfigInterface
instances.
With attributes such a problem does not exist.
Moreover, you can define attributes that modify the container. These will be discussed in the end of this chapter.
If a parameter is annotated with the #[Service]
attribute, you define that a service should be injected.
For example, look at the following class; the $foo
constructor parameter has such an annotation:
use Aura\Di\Attribute\Service;
class Example
{
public function __construct(
#[Service('foo.service')]
Foo $foo
) {
// ...
}
}
The Container will inject foo.service
for $foo
. It is basically the same as if you would write the following:
$di->params['Example']['foo'] = $di->lazyGet('foo.service');
If the parameter is annotated with the #[Instance]
attribute, you define a new instance of a class that should be injected.
For example, look at the following class; the $foo
constructor parameter has such an annotation:
use Aura\Di\Attribute\Instance;
class Example
{
public function __construct(
#[Instance(Foo::class)]
FooInterface $foo
) {
// ...
}
}
The Container will inject a new instance Foo::class
for $foo
. It is basically the same as if you would write the following:
$di->params['Example']['foo'] = $di->lazyNew(Foo::class);
If a parameter is annotated with the #[Value]
attribute, you define that a value should be injected.
For example, look at the following class; the $foo
constructor parameter has such an annotation:
use Aura\Di\Attribute\Instance;
class Example
{
public function __construct(
#[Value('foo.value')]
string $foo
) {
// ...
}
}
The Container will inject the value foo.value
for $foo
. It is basically the same as if you would write the following:
$di->params['Example']['foo'] = $di->lazyValue('foo.value');
It is also possible to create your own custom attribute. All you have to do is create a class using the native PHP 8.0 attribute
syntax. On top, it has to implement the Aura\Di\Attribute\AnnotatedInjectInterface
class.
Suppose you want to inject a config key coming from the ConfigBag
object below, into other classes.
namespace MyApp;
class ConfigBag {
private array $bag = [];
public function __construct(array $bag)
{
$this->bag = $bag;
}
public function get(string $key, mixed $default = null): mixed
{
return $this->bag[$key] ?? $default;
}
}
You could create an Config
attribute class like this.
namespace MyApp\Attribute\Config;
use Attribute;
use Aura\Di\Injection\Lazy;
use Aura\Di\Injection\LazyGet;
use Aura\Di\Injection\LazyInterface;
#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements AnnotatedInjectInterface
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function inject(): LazyInterface
{
$callable = [new LazyGet('config'), 'get'];
return new Lazy($callable, [$this->name]);
}
}
You set the service config
in the container.
$di->set('config', new ConfigBag(['foo' => 'bar']));
And then you could annotate a constructor parameter with your own #[Config]
attribute.
use MyApp\Attribute\Config;
class Example
{
public function __construct(
#[Config('foo')]
string $foo
) {
// ...
}
}
It is basically the same as if you would write the following:
$di->params['Example']['foo'] = $di->lazyGetCall('config', 'get', 'foo');
When you define both an annotation and an injection via code, the injection via code has precedence over the annotation.
Using the last example of the custom attribute, the following code will overwrite the #[Config('foo')]
annotation on
the $foo
constructor parameter, and hence inject the value "bravo"
and not "bar"
.
$di->set('config', new ConfigBag(['foo' => 'bar', 'alpha' => 'bravo']));
$di->params['Example']['foo'] = $di->lazyGetCall('config', 'get', 'alpha');
The above annotations for injecting the right parameters into a class work out-of-the-box when working with the container. But there might be occasions that you want to an annotation to change the configuration of the container.
Examples are a #[Route('GET', '/order/{id}')]
attribute that adds a route to your routes, or a
#[ListenFor(OrderWasPlaced::class)]
annotation that adds a listener to your event manager.
Configuring the container with attributes requires building the container with the
ClassScannerConfig
. When done so, the builder will scan the
passed directories for classes and annotations. Every class that is annotated with #[AttributeConfigFor]
and implements AttributeConfigInterface
can modify the container.
In the following example we create our own a #[Route]
attribute that also implements the AttributeConfigInterface
.
The attribute #[AttributeConfigFor]
is referencing the Route class. It is basically a self-reference because the attribute is
attached to the Route class. Now, methods annotated with the new #[Route]
will cause a RealRoute
to be appended in the routes array.
use Aura\Di\Attribute\AttributeConfigFor;
use Aura\Di\ClassScanner\AttributeConfigInterface;
use Aura\Di\ClassScanner\AttributeSpecification;
use Aura\Di\ClassScanner\ClassSpecification;
use Aura\Di\Container;
#[\Attribute]
#[AttributeConfigFor(Route::class)]
class Route implements AttributeConfigInterface {
public function __construct(private string $method, private string $uri) {
}
public static function define(Container $di, AttributeSpecification $attribute, ClassSpecification $class): void
{
if ($attribute->getAttributeTarget() === \Attribute::TARGET_METHOD) {
/** @var self $route */
$route = $specification->getAttributeInstance();
// considering the routes key is an array, defined like this
// $resolver->values['routes'] = [];
$di->values['routes'][] = new RealRoute(
$route->method,
$route->uri,
$container->lazyLazy(
$di->lazyCallable([
$di->lazyNew($attribute->getClassName()),
$attribute->getTargetMethod()
])
)
);
}
}
}
class Controller {
#[Route('GET', '/method1')]
public function method1() {}
#[Route('GET', '/method2')]
public function method2() {}
}
class RouterFactory {
public function __construct(
#[Value('routes')]
private array $routes
) {
// $routes contains an array of RealRoute objects
}
}
If your attribute cannot implement the AttributeConfigInterface
, e.g. the attribute is defined in an external package,
you can create an implementation of AttributeConfigInterface
yourself, and annotate it with #[AttributeConfigFor(ExternalAttribute::class)]
.
use Aura\Di\Attribute\AttributeConfigFor;
use Aura\Di\ClassScanner\AttributeConfigInterface;
use Aura\Di\ClassScanner\AttributeSpecification;
use Aura\Di\ClassScanner\ClassSpecification;
use Aura\Di\Container;
use Symfony\Component\Routing\Attribute\Route;
#[AttributeConfigFor(Route::class)]
class SymfonyRouteAttributeConfig implements AttributeConfigInterface
{
public static function define(Container $di, AttributeSpecification $attribute, ClassSpecification $class): void
{
if ($attribute->isMethodAttribute()) {
/** @var Route $route */
$route = $attribute->getAttributeInstance();
$invokableRoute = $di->lazyCallable([
$container->lazyNew($annotatedClassName),
$attribute->getTargetMethod()
]);
// these are not real parameters, but just examples
$di->values['routes'][] = new Symfony\Component\Routing\Route(
$route->getPath(),
$route->getMethods(),
$route->getName(),
$invokableRoute
);
}
}
}
Reflection is used by the container to get information of classes, e.g. what
parameters are used by the constructor. This information is used to create a class that in this package is called
a Blueprint
.
When you annotate a constructor parameter with #[Service]
, #[Instance]
, #[Value]
or with an attribute implementing
Aura\Di\Attribute\AnnotatedInjectInterface
then the class automatically gets the marker that it needs to compiled
into a Blueprint
when you call newCompiledInstance
method on the ContainerBuilder
. This also applies to using
code like $container->params
and $container->setters
for configuring your container.
There might be classes however, that are not configured using attributes or code but need to be instantiated by the
container somewhere in your code anyhow. Take for instance the class below. This class does not have an injection
attribute like #[Service]
, #[Config]
or #[Value]
, and there might not also be a $di->params[]
call to configure
this class. So the class is unknown to the container.
If you want to create a Blueprint
for this class during container compilation, annotate it with #[Blueprint]
.
use Aura\Di\Attribute\Blueprint;
#[Blueprint]
class OrderController
{
public function __construct(private Connection $databaseConnection)
{
}
}
To prevent, many classes have to be annotated with the #[Blueprint]
attribute, you can also use the
#[BlueprintNamespace]
attribute, typically annotated to an Application
, Kernel
or Plugin
class.
namespace MyPlugin;
use Aura\Di\Attribute\BlueprintNamespace;
#[BlueprintNamespace(__NAMESPACE__ . '\\Controllers')]
#[BlueprintNamespace(__NAMESPACE__ . '\\Command')]
class Plugin {
}
You should not compile all namespace in your application or plugin. That would be overkill, because there are classes like entities, models and DTOs that are never being instantiated by the container.
Working with compiled blueprints require using the ClassScannerConfig
.