Woohoo Labs. Zen is a very fast and simple, Container-Interop (PSR-11) compliant DI Container.
- Introduction
- Install
- Basic Usage
- Advanced Usage
- Examples
- Versioning
- Change Log
- Testing
- Contributing
- Credits
- License
Although Dependency Injection is one of the most fundamental principles of Object Oriented Programming, it doesn't get as much attention as it should. To make things even worse, there are quite some misbeliefs around the topic which can prevent people from applying the theory correctly.
Besides using Service Location, the biggest misbelief certainly is that Dependency Injection requires very complex tools called DI Containers. And we all deem to know that their performance is ridiculously low. Woohoo Labs. Zen was born after the realization of the fact that these fallacies seem to be true indeed, or at least our current ecosystem endorses unnecessarily complex tools, sometimes offering degraded performance.
I believe that in the vast majority of the cases, very-very simple tools could do the job faster and more importantly, while remaining less challenging mentally than a competing tool offering "everything and more" out of the box. I consider this phenomenon as part of the simple vs. easy problem.
Zen doesn't - and probably will never - feature all the capabilities of the most famous DI Containers currently available. There are things that aren't worth the hassle. On the other hand, it will try hard to enforce the correct usage of Dependency Injection, and to make the configuration as evident and convenient as possible while providing an outstanding performance according to my benchmarks.
- Container-Interop (PSR-11) compliance
- Supports constructor and property injection
- Supports the notion of scopes (Singleton and Prototype)
- Supports autowiring, but only objects can be injected
- Generates a single class
- No caching is needed to get ultimate speed
As mentioned before, Zen is suitable for projects needing maximum performance and easy configuration but not requiring some common DI techniques, like method or scalar value injection. If performance is not a concern for you, but you want a fully featured container, please choose another project. In this case, I would recommend you to check out the awesome PHP-DI instead of Zen.
But if the functionality offered By Zen is enough for you then Zen will amaze you with its simplicity (the core of the project only consists of cc. 600 lines of code), high performance (it has similar speed when you manually instantiate your objects) and its easy configuration (Zen was designed to work with the least amount of configuration).
The steps of this process are quite straightforward. The only thing you need is Composer. Run the command below to get the latest version of Zen:
$ composer require woohoolabs/zen
This library needs PHP 7.0+.
As Zen is a Container-Interop (PSR-11) compliant container, it supports the $container->has()
and
$container->get()
methods as defined by
ContainerInterface
.
Only constructor and property injection of objects are supported by Zen.
In order to use constructor injection, you have to type hint the parameters or add a @param
PHPDoc tag for them. If a
parameter has a default value then this value will be injected. Here is an example of a valid constructor:
/**
* @param B $b
*/
public function __construct(A $a, $b, $c = true)
{
// ...
}
In order to use property injection, you have to annotate your properties with @Inject
(mind case-sensitivity) and
provide their type with a @var
PHPDoc tag in the following way:
/**
* @Inject
* @var A
*/
private $a;
As a rule of thumb, you should only rely on constructor injection, because using test doubles in your unit tests instead of your real dependencies becomes much easier this way. Property injection can be acceptable for those classes that aren't unit tested. I prefer this type of injection in my controllers, but nowhere else.
Zen is a compiled DI Container which means that every time you update a dependency of a class, you have to recompile the container in order for it to reflect the changes. This is a major weakness of compiled containers (Zen will certainly see major improvements in this regard in the future), but the trade-off had to be taken in order to be more performant than "dynamic" Containers.
Compilation is possible by running the following command:
$ vendor/bin/zen build CONTAINER_PATH COMPILER_CONFIG_CLASS_NAME
This results in a new file CONTAINER_PATH
which can be directly instantiated (assuming proper autoloading) in your
project. No other configuration is needed during runtime.
$container = new MyContainer();
It's up to you where you generate the container but please be aware that file system speed (referring to the Virtualbox FS) can affect the time consumption of the compilation as well as the performance of your application. On the other hand, it's much more convenient to put the container in a place where it is easily reachable as you will occasionally need to debug it.
What about the COMPILER_CONFIG_CLASS_NAME
argument? This must be the fully qualified name of a class which extends
AbstractCompilerConfig
. Let's see an
example!
class MyCompilerConfig extends AbstractCompilerConfig
{
public function getContainerNamespace(): string
{
return "MyApp\\Config";
}
public function getContainerClassName(): string
{
return "Container";
}
public function useConstructorInjection(): bool
{
return true;
}
public function usePropertyInjection(): bool
{
return true;
}
public function getContainerConfigs(): array
{
return [
new MyContainerConfig()
];
}
}
By providing the prior configuration to the zen build
command, a MyApp\Config\Container
class will be
generated and the compiler will resolve constructor dependencies via type hinting and PHPDoc comments as well as property
dependencies marked by annotations.
We only mentioned so far how to configure the compiler, but we haven't talked about container configuration. This can
be done by returning an array of AbstractContainerConfig
child instances in the getContainerConfigs()
method. Let's see an example
for the container configuration too!
class MyContainerConfig extends AbstractContainerConfig
{
protected function getEntryPoints(): array
{
return [
new WildcardEntryPoint(__DIR__ . "/Controller"),
];
}
protected function getDefinitionHints(): array
{
return [
ContainerInterface::class => MyContainer::class,
];
}
protected function getWildcardHints(): array
{
return [
new WildcardHint(
__DIR__ . "/Domain",
'WoohooLabs\Zen\Examples\Domain\*RepositoryInterface',
'WoohooLabs\Zen\Examples\Infrastructure\Mysql*Repository'
)
];
}
}
Configuring the container consist of the following two things: defining your Entry Points (in the getEntryPoints()
method) and passing Hints for the compiler (in the getDefinitionHints()
and getWildcardHints()
methods).
Entry Points are such classes that are to be directly retrieved from the DI Container (for instance Controllers and
Middleware usually fall in this category). This means that you can only fetch Entry Points from the Container with
the $container->get()
method.
Entry Points are important because their dependencies are automatically discovered during the compilation phase resulting in your full object graph (this feature is usually called "autowiring").
The following example shows a configuration which instructs the compiler to recursively search for all classes in the
Controller
directory (please note that only concrete classes are included by default) and discover all of their
dependencies.
protected function getEntryPoints(): array
{
return [
new WildcardEntryPoint(__DIR__ . "/Controller"),
];
}
But you are able to define Entry Points individually too:
protected function getEntryPoints(): array
{
return [
new ClassEntryPoint(UserController::class),
];
}
The first method is the preferred one, because it needs much less configuration.
Hints tell the compiler how to properly resolve a dependency. This can be necessary when you depend on an
interface or an abstract class because they are obviously not instantiatable. With hints, you are able to bind
implementations to your interfaces or concretions to your abstract classes. The following example binds the
MyContainer
class to ContainerInterface
(in fact, you don't have to bind these two classes together, because this
very configuration is automatically set during compilation).
protected function getDefinitionHints(): array
{
return [
ContainerInterface::class => MyContainer::class,
];
}
Wildcard Hints can be used when you want to bind your classes in masses. Basically, they recursively search for all your classes in a directory specified by the first parameter, and bind those classes together which can be matched by the provided patterns. The following example
protected function getWildcardHints(): array
{
return [
new WildcardHint(
__DIR__ . "/Domain",
'WoohooLabs\Zen\Examples\Domain\*RepositoryInterface',
'WoohooLabs\Zen\Examples\Infrastructure\Mysql*Repository'
)
];
}
will bind
UserRepositoryInterface
to MysqlUserRepository
.
Currently, only *
supported as a wildcard character because your patterns are much simpler to read this way than with
real regex.
Zen is able to control the lifetime of your container entries via the notion of scopes. By default, all entries retrieved
from the container have Singleton
scope, meaning that they are only instantiated at the first retrieval, and the same
instance will be returned on the subsequent fetches. Singleton
scope works well for stateless objects.
On the other hand, container entries of Prototype
scope are instantiated at every retrieval, so that is makes it
possible to store stateful objects in the container. You can hint a container entry as Prototype
with the
DefinitionHint::prototype()
construct as follows:
protected function getDefinitionHints(): array
{
return [
ContainerInterface::class => DefinitionHint::prototype(MyContainer::class),
];
}
You can use WildcardHint::prototype()
to hint your Wildcard Hints the same way too.
Please have a look at the examples folder for a complete example!
This library follows SemVer v2.0.0.
Please see CHANGELOG for more information what has changed recently.
Woohoo Labs. Zen has a PHPUnit test suite. To run the tests, run the following command from the project folder after you have copied phpunit.xml.dist to phpunit.xml:
$ phpunit
Additionally, you may run docker-compose up
in order to execute the tests.
Please see CONTRIBUTING for details.
The MIT License (MIT). Please see the License File for more information.