Download the PHP package aimfeld/zend-di-compiler without Composer
On this page you can find all versions of the php package aimfeld/zend-di-compiler. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download aimfeld/zend-di-compiler
More information about aimfeld/zend-di-compiler
Files in aimfeld/zend-di-compiler
Package zend-di-compiler
Short Description A Zend Framework 2 module that uses auto-generated factory code for dependency-injection.
License BSD-3-Clause
Homepage https://github.com/aimfeld/ZendDiCompiler
Informations about the package zend-di-compiler
NB: This repo is no longer actively maintained. Since laminas-di has changed a lot as of version 3 and now has better support for ahead-of-time (aot) factory code generation, I suggest using laminas-di directly.
Table of Contents
- Introduction
- Features
- Caveats
- Installation
- Usage
- Dependency injection container vs. service locator
- Configuration
- Shared instances
- Type preferences
- Examples
- Using ZendDiCompiler to create a controller
- Using the DiFactory to create runtime objects with dependencies
- Passing all runtime parameters in a single array
- Passing custom runtime parameters
- Generated code and info
- Factory code
- Code scan log
- Component dependency info
Introduction
Are you tired of writing tons of factory code (closures) for the Laminas\ServiceManager in your Zend Framework 2 application? Are outdated factory methods causing bugs? This can all be avoided by using ZendDiCompiler!
ZendDiCompiler is a Zend Framework 2 module that uses auto-generated factory code for dependency-injection. It saves you a lot of work, since there's no need anymore for writing Laminas\ServiceManager factory closures and keeping them up-to-date manually.
ZendDiCompiler scans your code using Laminas\Di and creates factory methods automatically. If the factory methods are outdated, ZendDiCompiler updates them in the background. Therefore, you develop faster, avoid bugs due to outdated factory methods, and experience great performance in production!
Features
- Code scanning for creating DI definitions and automatic factory code generation.
- Can deal with shared instances and type preferences.
- Allows for custom code introspection strategies (by default, only constructors are scanned).
- Can be used as a complement to Laminas\ServiceManager.
- Detection of outdated generated factory code and automatic rescanning (great for development).
- Can create new instances or reuse instances created before.
- Can be used as a factory for runtime objects combining DI and passing of runtime parameters.
- Greater perfomance and less memory consumption, as compared to using Laminas\Di\Di with cached definitions.
Caveats
- Setter injection and interface injection are not supported yet. Instances must be injected via constructor injection (which I recommend over the two other methods anyway).
- Using ZendDiCompiler makes sense if you develop a large application or a framework. For smaller applications, ZendDiCompiler may be overkill and you should handle instantiation using Laminas\ServiceManager callback methods.
Installation
This module is available on Packagist.
In your project's composer.json
use:
For PHP 7, install version 2.x, for PHP 5.4-5.6, use version 1.x.
Make sure you have a writable data
folder in your application root directory, see
ZendSkeletonApplication. Put a .gitignore
file in it with
the following content:
Add 'ZendDiCompiler'
to the modules array in your application.config.php
. ZendDiCompiler must be loaded after the
modules where it is used:
Usage
Dependency injection container vs. service locator
Is ZendDiCompiler a dependency injection container (DiC) or a service locator (SL)? Well, that depends on where you use it. ZendDiCompiler can be used as a DiC to create your controllers in your module class and inject the controller dependencies from outside. Pure DiC usage implies that ZendDiCompiler is used only during the bootstrap process and disposed before the controller is dispatched. This has been coined the "Register Resolve Release pattern" and is the recommended way by experts like Mark Seemann and others.
As soon as you inject the ZendDiCompiler itself into your controllers and other classes, you are using it as a service locator.
The ZF2 MVC architecture is based on controller classes with action methods. Given this architecture, controller dependencies become
numerous very quickly. In order to avoid bloated controller constructors, it makes sense to inject ZendDiCompiler as a
single dependency into ZF2 controller classes and use it to pull the other dependencies from inside the controllers.
This means using it as a service locator, just like Laminas\ServiceManager
is typically used.
ZendDiCompiler is also used as a service locator inside of the provided ZendDiCompiler\DiFactory
which is very useful for
creating runtime objects with dependencies. This
avoids a lot of abstract factory code you would otherwise have to write.
Besides ZF2 controllers, I recommend not to inject ZendDiCompiler directly anywhere. If you need a service in one of your
classes, just ask for it in the constructor. If you need to create runtime objects with dependencies, inject
DiFactory or your extended version of it with custom creation methods.
Configuration
ZendDiCompiler uses standard Laminas\Di configuration (which is not well documented yet). To make things easier, see example.config.php for examples of how to specify:
- Directories for the code scanner
- Instance configuration
- Type preferences
For a full list of configuration options, see module.config.php
ZendDiCompiler creates a GeneratedServiceLocator
class in the data
directory and automatically refreshes it when constructors change during
development. However, if you e.g. change parameters in the instance configuration,
you have to manually delete data/GeneratedServiceLocator.php
to force a refresh. In your staging and production
deployment/update process, make sure that data/GeneratedServiceLocator.php
is deleted!
Shared instances
You need to provide shared instances to ZendDiCompiler::addSharedInstances() in your application module's onBootstrap() method in the following cases (also see example below):
- The object to be injected is an instance of a class outside of the scanned directories.
- The object to be injected requires some special bootstrapping (e.g. a session object).
Note that ZendDiCompiler provides some default shared instances automatically (see ZendDiCompiler::getDefaultSharedInstances()). The following default shared instances can be constructor-injected without explicitly adding them:
ZendDiCompiler\ZendDiCompiler
ZendDiCompiler\DiFactory
Laminas\Mvc\MvcEvent
Laminas\Config\Config
Laminas\View\Renderer\PhpRenderer
Laminas\Mvc\ApplicationInterface
Laminas\ServiceManager\ServiceLocatorInterface
Laminas\EventManager\EventManagerInterface
Laminas\Mvc\Router\RouteStackInterface
Type preferences
It is common to inject interfaces or abstract classes. Let's have a look at interface injection (for abstract classes, it works the same).
We need to tell ZendDiCompiler which implementing class to inject for ExampleInterface
. We specify ExampleImplementor
as
a type preference for ExampleInterface
in our example config:
ZendDiCompiler will now always inject ExampleImplementor
for ExampleInterface
. Calling
ZendDiCompiler::get('ZendDiCompiler\Example\ExampleInterface')
will return the ExampleImplementor
.
Type preferences can not only be used for interfaces and abstract classes, but for substituting classes in general. They can even be used to deal with non-existing classes:
Calling ZendDiCompiler::get('ZendDiCompiler\Example\NotExists')
will return a ZendDiCompiler\Example\Exists
instance.
Believe it or not, there are actually some good use cases for this.
Examples
All examples sources listed here are included as source code.
Using ZendDiCompiler to create a controller
Let's say we want to use the ZendDiCompiler to create a controller class and inject some
dependencies. For illustriation, we also inject the ZendDiCompiler itself into the controller.
As mentioned above, it
is a moot topic whether this is a good idea or not. But if we decide to use the ZendDiCompiler inside the controller to
get other dependencies, we can either inject it in the constructor or pull it from the ZF2 service locator
using $this->serviceLocator->get('ZendDiCompiler')
.
In our example, we have the following classes:
ExampleController
ServiceA with a dependency on ServiceB
ServiceB with a constructor parameter of unspecified type:
ServiceC which requires complicated runtime initialization and will be added as shared instance.
We add the example source directory as a scan directory for ZendDiCompiler. Since ServiceB
has a parameter of unspecified type, we
have to specify a value to inject. A better approach for ServiceB
would be to require the Config
in its constructor
and retrieve the parameter from there, so we wouldn't need to specify an instance configuration. The
configuration for our example
looks like this:
Now we can create the ExampleController
in our application's module class.
For convenience, we retrieve the ZendDiCompiler from the service manager and assign it to a local variable ($this->zendDiCompiler = $sm->get('ZendDiCompiler')
).
This makes it easier for writing getControllerConfig()
or getViewHelperConfig()
callbacks.
Since the ServiceC
dependency requires some complicated initialization, we need to initialize it and add it as a shared instance to
ZendDiCompiler.
Using the DiFactory to create runtime objects with dependencies
It is useful to distinguish two types of objects: services and runtime objects. For services, all parameters should
be specified in the configuration (e.g. a config array wrapped in a Laminas\Config\Config
object). If class constructors
e.g. in third party code require some custom parameters, they can be specified in the
instance configuration).
Runtime objects, on the other hand, require at least one parameter which is determined at runtime only.
ZendDiCompiler provides ZendDiCompiler\DiFactory
to help you create runtime objects and inject their dependencies.
Passing all runtime parameters in a single array
If you follow the convention of passing runtime parameters in a single array named $zdcParams
as in RuntimeA
,
things are very easy (the array name(s) can be configured in
module.config.php):
ZendDiCompiler automatically injects ZendDiCompiler\DiFactory
as a default shared instance. So
we can just use it to create RuntimeA
objects in ServiceD
. RuntimeA
's dependencies (the Config
default shared instance
and ServiceA
) are injected automatically, so you only need to provide the runtime parameters:
Passing custom runtime parameters
If you can't or don't want to follow the convention of passing all runtime parameters in a single $zdcParams
array,
ZendDiCompiler still is very useful. In that case, you can just extend a custom factory from ZendDiCompiler\DiFactory
and
add your specific creation methods. RuntimeB
requires two separate run time parameters:
So we extend ExampleDiFactory
from ZendDiCompiler\DiFactory
and write a creation method createRuntimeB
:
In ServiceE
, we inject our extended factory. If the extended factory is located in a directory scanned by ZendDiCompiler,
we don't need to provide it as a shared instance. Now we can create RuntimeB
objects as follows:
Generated code and info
Factory code
ZendDiCompiler will automatically generate a service locator in the data/ZendDiCompiler
directory and update it if constructors are changed
during development. Services can be created/retrieved using ZendDiCompiler::get()
. If you need a new dependency in one of your
classes, you can just put it in the constructor and ZendDiCompiler will inject it for you.
Just for illustration, this is the generated service locator created by ZendDiCompiler and used in ZendDiCompiler::get()
.
Code scan log
ZendDiCompiler logs problems found during code scanning in data/ZendDiCompiler/code-scan.log
. If you can't retrieve an object from ZendDiCompiler, you will probably find the reason in this log. The most common problem is that you have untyped scalar parameters instead of a Laminas\Di configuration. Here's an example of the code scan log showing some problems:
In case of simple value objects without any service dependencies, I do not use dependency injection but create then with new
, e.g. ConditionResult::__construct($isTrue, $isCacheable, $allowFlip = true)
. These objects are not meant to be created with the DiFactory and therefore, the DEBUG
notice can be ignored.
Component dependency info
As a bonus, ZendDiCompiler will write a component-dependency-info.txt
file containing information about
which of the scanned components depend on which classes.
Scanned classes are grouped into components (e.g. the Laminas\Mvc\MvcEvent class belongs to the Laminas\Mvc component). For every component, all constructor-injected classes are listed. This helps you analyze which components depend on which classes of other components. Consider organizing your components into layers. Each layer should depend on classes of the same or lower layers only. Note that only constructor-injection is considered for this analysis, so the picture might be incomplete.
Here's an example of what you might see:
All versions of zend-di-compiler with dependencies
laminas/laminas-config Version >=2.6
laminas/laminas-di Version >=2.6
laminas/laminas-servicemanager Version ^3.0