When I started this autowiring dependency injection (AWDI) container comparison, I presumed that aside from ancillary and additional features, the differences between the systems would not be in the core of what they do. I expected the differences would be in how they do it.
As it turns out, I was wrong. What I considered to be fundamental capabilities were just not available in some AWDI systems -- at least, not without some indirection, extra effort, or workarounds.
The scenario of fundamental features I imagined, thinking it would be easy to work through in every AWDI system, was this:
-
Define an instance of a Foo class, to be retrieved later via container.
-
Override a single constructor argument to replace one of originally configured values for Foo with a different value. (This simulates the loading of multiple config files.)
-
Lazy-resolve one or more values in a dependent object for Foo, e.g. from the environment. (This shows off lazy resolution features.)
But what I thought would be easy turned out to be difficult or impossible in some AWDI containers.
Below you will find code for the scenario using Auryn, League Container,
Illuminate Container, PHP-DI, Symfony Dependency Injection and Ray.Di. Each container
example uses the samesetup with an output()
function for
inspecting the results.
Since Capsule is the focal point for these comparisons, it makes sense to start with it. You can find the Capsule code at https://github.com/capsulephp/di. The example code for the scenario is here.
Capsule does complete the scenario:
-
Each definition is a dynamic property on the container, addressed using the
{}
notation. -
The PDO arguments are lazy-resolved from the environment using the
env()
method on the class definition. -
The explicit Foo arguments are literal; the $bar argument is purposely set to "wrong" so that we can see later if it is overridden properly.
-
The Foo $bar argument is re-defined, to simulate a new configuration being loaded with override values.
The output()
is correct:
PDO
bar-right
baz-right
You can find the Auryn code at https://github.com/rdlowrey/auryn. The example code for the scenario is here.
Auryn does not complete the scenario:
-
There appears to be no lazy-loading facility to read environment variables, or anything else. Thus, the PDO arguments cannot be specified directly on the container. Instead, the injector requires a delegate factory closure, which does mean the PDO arguments get lazy-loaded in a second-hand sort of way.
-
The explicit Foo argument names have to be prefixed with a
:
to indicate they are literals; the $bar argument is purposely set to "wrong" so that we can see later if it is overridden properly. -
As there appears to be no way to address an individual argument,
define()
is called again to redefine the $bar argument. Unforturnately,define()
overwrites the entire Foo definition, not just the one argument.
As a result, the output()
fails, showing the default $baz value instead of the
explicitly configured one:
PDO
bar-right
baz-wrong
You can find the Illuminate Container code (a Laravel component) at https://github.com/illuminate/container. The example code for the scenario is here.
Illuminate Container does complete the scenario, but only with some extra effort:
-
The PDO arguments have to be specified individually, using a
$
prefix on the parameter names, via thenwhen()->needs()
idiom. -
The PDO arguments have to be drawn from a configuration source via
giveConfig()
, not from the environment per se. (See point 5 below.) -
The explicit Foo arguments likewise have to be specified individually; the $bar argument is purposely set to "wrong" so that we can see later if it is overridden properly.
-
The Foo $bar argument is re-defined, to simulate a new configuration being loaded with override values.
-
The
giveConfig()
method expects a container entry object called'config'
to be present, with aget()
method on it. To honor this, a config container factory closure is created and bound to the main container; the necessary config values are retrieved from the environment. Thus, the environment values are lazy-loaded, but indirectly and in a second-hand sort of way.
The output()
is correct:
PDO
bar-right
baz-right
You can find the League Container code at https://github.com/thephpleague/container. The example code for the scenario is here.
The League Container does not complete the scenario, even with extra effort:
-
The container itself will not autowire unless you set up a ReflectionContainer as a fallback delegate.
-
The PDO arguments are specified as lazy-resolvable, though that resolution must be via the container, not directly from the environment. (See point 6 below.)
-
The Foo $pdo argument is specified as lazy-resolvable, but this isn't necessary in any of the other containers presented here; this seems at odds with the advertised autowiring capability.
-
The Foo $bar argument is specified as lazy-resolvable, because there is no way to override an individual constructor argument. As a workaround, the value is lazy-resolved out of the container, in hopes that it can be defined and then re-defined later.
-
The Foo $baz argument is specified as a literal string object, to tell the container not to try to resolve it any further.
-
The container then gets set with values: the initial value for Foo $bar, and the PDO arguments lazy-loaded as closures using
getenv()
. -
The container then gets re-set with a new value for Foo $bar, to simulate the loading of multiple configs, some with overrides.
Unfortunately, because of the way the League container works internally, the
later setting in point 7 cannot override the earlier one in point 4, so the
output()
fails:
PDO
bar-wrong
baz-right
Specifically, it's because the DefinitionAggregate::getDefinition()
loop stops
after finding the first matching key; the override value comes later, so it is
never encountered:
public function getDefinition(string $id): DefinitionInterface
{
foreach ($this->getIterator() as $definition) {
if ($id === $definition->getAlias()) {
return $definition->setContainer($this->getContainer());
}
}
// ...
}
You can find the PHP-DI code at https://github.com/PHP-DI/PHP-DI. The example code for the scenario is here.
PHP-DI does complete the scenario, but with a little extra effort.
-
The container itself needs to be told to use autowiring, and not to use annotations.
-
The PDO arguments are lazy-loaded directly from the environment.
-
The Foo $bar argument is specified as lazy-resolvable, because there is no way to override an individual constructor argument. As a workaround, the value is lazy-resolved out of the container, in hopes that it can be defined and then re-defined later.
-
The Foo $baz argument is specified as a literal string.
-
A
Foo:bar
entry is added with the initial Foo $bar value. -
The container then gets re-set with a new value for Foo $bar, to simulate the loading of multiple configs, some with overrides.
The output()
is correct:
PDO
bar-right
baz-right
(Thanks to ahundiak for providing the code in this example.)
You can find the Symfony Dependency Injection code at https://github.com/symfony/dependency-injection. The example code for the scenario is here.
Symfony does not complete the scenario. The Symfony container must
be compiled before it can be used; in turn, the environment variables must be in
place before the container is compiled. Compiling the container before the
environment variables are available results in EnvNotFoundException
s.
The only way the Symfony code can run at all is to define the environment variables before compiling and using the container, something that is not required by any of the other systems under consideration.
-
The PDO arguments are specified as environment variables via a special string notation, e.g.
'%env(DB_DSN)%'
. -
The Foo class is marked as autowired, and further marked as a public service that can be retrieved from anywhere.
-
The explicit Foo arguments likewise have to be specified individually; the $bar argument is purposely set to "wrong" so that we can see later if it is overridden properly.
-
The Foo $bar argument is re-defined, to simulate a new configuration being loaded with override values.
-
In order to avoid
EnvNotFoundException
s, the environment variables are defined before compiling the container. -
The container is compiled before use.
The output in the modified scenario is correct ...
PDO
bar-right
baz-right
... because the environment has to be loaded before the container is compiled and used, I don't think this counts as "lazy loading" of the environment variables. As a result, I have to count this as "does not complete the scenario".
You can find the Ray.Di code at https://github.com/ray-di/Ray.Di. The example code for the scenario is here.
Ray.Di does complete the scenario.
- The PDO arguments are bound to a Provider class that provides the values for them in runtime.
- The Foo $bar argument is purposely set to "wrong" with instance bidning so that we can see later if it is overridden properly.
- The Foo $bar argument is re-defined, to simulate a new configuration being loaded with override values.
The output()
is correct:
PDO
bar-right
baz-right
Thus, the final tally of which container systems completed the scenario:
- Capsule: yes
- Auryn: no
- League: no
- Illuminate: yes, with workarounds
- PHP-DI: yes, with workarounds
- Symfony: no
- Ray.Di: yes
Does this mean the containers that could not complete the scenario are somehow "bad" or "wrong"? No -- but it does mean I was wrong to think that the features highlighted by the scenario are commonplace. Different scenarios might show off these containers in a better or worse light.
Regardless, this exercise does show some of the differences between the example container systems using a practical example, which was the only point of doing the comparison in the first place.
You can run the comparison code for yourself.
First, install the packages being compared ...
cd psr-11-v1; composer install
cd psr-11-v2; composer install
... then run the example code of your choice:
php capsule.php
php php-di.php
# etc