Dependency injection
Note
Further on, we'll refer to the dependency injection container simply as "container".
As previously mentioned here, the container can be configured differently for each application area.
For global configuration, create a file named Di.php in directory Configuration under a module's root directory.
For area-specific configuration, create a file named Di.php in one of the following directories under a module's root directory.
Configuration/BackendConfiguration/FrontendConfiguration/Cli
Tip
You can use the configurators variable to add other configurator classes that should be processed. This allows
you to have multiple smaller configurators.
Binding
In the process of binding, we bind a type to an implementation which we call a "concrete".
A type can be a text identifier or an interface, while the concrete can be a classname or a closure. The result of a bind operation is a binding object wrapped in a closure, which will be executed to configure or instantiate the concrete implementation when required directly or by the dependency injection chain.
Binding objects determine how instances are created, which arguments are injected, which calls are made on the instances immediately after instantiation, are they supposed to be instantiated only once, and more.
Injecting a concrete implementation, like an instantiable class, requires no configuration. Also, by default, the class will be instantiated only once, meaning it will become a shared instance.
Important
All binding objects are configured to create shared instances by default, unless explicitly reconfigured otherwise.
Let's take a look at some common examples. Check the annotations to know more.
Binding with an interface
- Always use the bind builder and always fetch it from the container from within the closure.
- The interface class becomes a unique identifier for this binding.
- The concrete implementation which implements the interface.
- The closure must return a binding object, so we call the builder's
buildmethod.
Binding with an id
Binding with an id is exactly the same as when binding with an interface, the key difference being the binding identifier.
Important
To be able to inject an identifier which is not a classname of interface or a class, the argument must be explicitly defined
in the binding object or specified using the
Amarant\Framework\Attribute\Di\Inject or Amarant\Framework\Attribute\Di\InjectIfDefined attributes on constructor parameters.
| Vendor/ModuleName/Configuration/Di.php | |
|---|---|
Note
If we created both of these bindings, we would end up with two different AnImplementation class instances.
Inside the container, one would be available under Vendor\ModuleName\Contract\AnInterface and the other under some.unique.binding.identifier id.
Specifying concrete as closure
Instead of specifying the implementation class, a closure can be used, allowing manual instantiation with access to the container.
| Vendor/ModuleName/Configuration/Di.php | |
|---|---|
Arguments
Constructor arguments can be explicitly defined. All arguments that are not defined, will be injected.
| Vendor/ModuleName/Configuration/Di.php | |
|---|---|
Here we are injecting a simple text value into the constructor parameter someArgument of class Vendor\ModuleName\Implementation\AnImplementation.
We can do a lot more than that, by using different types of injections instead.
Calls
Calls added to a binding will be executed once on an object, immediately after its instantiation. It allows execution of methods on a new object.
| Vendor/ModuleName/Configuration/Di.php | |
|---|---|
The method name and arguments must be provided. Method arguments support injection the same way as constructor arguments do.
Shared instances
To set the bound instance as shared, thus producing a singleton, set the shared to true.
| Vendor/ModuleName/Configuration/Di.php | |
|---|---|
This value is true by default. Setting it to false will create a new instance every time a binding
is requested.
Templates
A binding can be made a template only. This means it's not meant to be instantiated but rather used as a template by another binding.
- The template binding.
- Template must be set to true if we want the binding to be a template.
- We can add constructor arguments, the same as with regular binding.
- The binding that will use the template.
- Parent must be specified, using the identifier of the template binding.
- Constructor arguments can be added or overridden.
Bindings using a template do not specify type or concrete as they are inferred from the template.
Factories
A binding can specify another binding as its factory. This works similarly as calls. A binding id, method name and arguments are specified. The container fetches the instance under the given binding id, calls the method using the provided arguments and binds the result of the method as an instance.
Given the example above, we assume that the class AnImplementationFactory exists, has a public method
create that accepts parameter someArgument and returns an instance that implements AnInterface interface.
If defined, this factory will be called every time an instance of AnInterface is requested by the container, unless
it's shared and an instance of it already exists in the container.
Extending
An existing binding can be extended, allowing its configuration to be overridden.
| Vendor/ModuleName/Configuration/Di.php | |
|---|---|
The closure receives the binding object Amarant\Framework\Contract\Di\BindInterface, allowing change to its
parametrization. In the example above, the constructor argument someArgument is changed to a different value.
Tagging
Binding identifiers can be tagged, so they can be injected as a list. Tags themselves are just text labels.
Optionally, priority can be defined, affecting the order of objects in an injected list. Objects with higher priority are listed first.
| Vendor/ModuleName/Configuration/Di.php | |
|---|---|
- Optional index which acts as priority, mapped to a binding identifier.
- A list of tags.
Injection
Injection can be done with:
- binding id
- classname of class or interface
- tags
- multiple tags
- id named tags
- class named tags
- call
- environment variables
All of these can be used wherever injection is supported:
- constructor arguments in bindings
- calls in bindings
- factory in bindings
- inject attributes on constructor arguments
These can be used as block arguments in the presentation layer layouts:
- binding id
Warning
Always use Amarant\Framework\Enum\DiEnum static methods for injection. Do not hardcode injection prefixes.
Inject by id or classname
Inject using DI configurator
Inject using an attribute
Important
Avoid using attribute for injection, to allow modules to define and override (extend) DI configuration using configurators.
A valid case for using an attribute would be to inject one specific implementation that wouldn't need to be changed by configurators. For example, using a specific logger instance.
Inject using an attribute - if defined
By default, a constructor argument that has a default value like null, and whose value isn't explicitly defined
by a configurator and has no Inject attribute, will not be injected.
If a binding may not have been defined at all, you can use a different attribute for injection.
In this case, if AnotherImplementation has a binding defined in the container, it will be injected. Otherwise,
the argument stays intact with its default value of null.
Inject using tags
Inject object instances of binding identifiers tagged with one or many tags.
Check the annotations to know more.
Important
The constructor argument that receives the list, must be one of the following types:
arrayiterableTraversableAmarant\Framework\Contract\Di\ArgumentIteratorInterface
Unless the type is array, Amarant\Framework\Contract\Di\ArgumentIteratorInterface will be injected.
Configure the injection
- Inject instances of binding identifiers, tagged by a tag, into a list with numeric keys.
- Inject instances of binding identifiers, tagged by any of multiple tags, into a list with numeric keys.
- Inject instances of binding identifiers, tagged by a tag, into a list where keys are the binding identifiers.
- Inject instances of binding identifiers, tagged by a tag, into a list where keys are the object class names.
Use an injected list
- An instance of
Amarant\Framework\Contract\Di\ArgumentIteratorInterfacecontaining object instances of all binding identifiers which were tagged bytag.atag. - An instance of
Amarant\Framework\Contract\Di\ArgumentIteratorInterfacecontaining object instances of all binding identifiers which were tagged bytag.bortag.ctags. -
An instance of
Amarant\Framework\Contract\Di\ArgumentIteratorInterfacecontaining object instances of all binding identifiers which were tagged bytag.dtag.Iterator keys are binding identifiers.
-
An instance of
Amarant\Framework\Contract\Di\ArgumentIteratorInterfacecontaining object instances of all binding identifiers which were tagged bytag.etag.Iterator keys are class names.
Note
Objects in a list are instantiated lazyly, when the list is iterated or an offset is retrieved.
Call injection
This type of injection instantiates a binding identifier, calls a method with parameters on it, and uses the result as injection value.
Warning
The parameters should only have scalar values.
Environment injection
To inject environment variables, use the following example.
Using the container
Usually, you wouldn't inject and use a container directly. If you have a good reason to do so, you can use the following methods.
Note
The container is PSR-11 compliant.
First, inject the container into your class:
Amarant\Framework\Contract\Di\ContainerInterface
Get an instance using binding identifier (id or classname):
$instance = $this->container->get('some.id');
Get instances for bindings tagged with a tag:
$instances = $this->container->getTagged('some.tag');
Create a new instance using binding identifier (id or classname):
$instance = $this->container->create('some.id');
Remove all instances from the container:
$this->container->reset();
Remove all instances from the container, except the ones set with the instance method:
$this->container->resetExceptSetInstances();
Get the configuration storage object:
$configuration = $this->container->getConfigStorage();
Setting an instance of an object
During application initialization, a configurable container is passed on to various application loaders. At that time, an already instantiated object can be set on the container.
$container->instance('some.binding.identifier.or.class.name', $instance);
Note
After the application initializes, it cannot access the configurable container anymore. This means the instance
method won't be available anymore. The reason for this is that an instance cannot be changed after it's already
injected into other objects. The same reason applies why bindings and tags can only be applied on the
configurable container.
The kernel allows adding a post load callback which has access to the configurable container. An instance can also be set using the callback.
See post load callbacks.