dev-master
9999999-deveArc - the explizit architecture framework - dependency injection container
MIT
The Requires
- php ^7.2
- psr/container ^1.0.0
by Thomas Koudela
eArc - the explizit architecture framework - dependency injection container
Standalone lightweight dependency injection component of the eArc libraries., (*1)
If you need to decouple your components, restrict injection access or want to make your app components explicit use the earc/component-di library which builds on the top of earc/di., (*2)
di_*
functions by
any logic you like.) Install the earc dependency injection library via composer., (*3)
$ composer require earc/di
You can even use it with symfony., (*4)
A new dependency resolver can be initialized with no arguments. Use this in your
index.php
, bootstrap or configuration script., (*5)
use eArc\DI\DI; DI::init();
After that classes and parameters can be accessed via the di_*
functions., (*6)
di_get(SomeClass::class); // returns an instance of SomeClass::class di_param('some.parameter'); // returns the value of the parameter `some.parameter`
di_get
returns the same instance in successive calls. If you need a new object
use di_make
. It returns a new instance on each call. In both functions the use
of the fully qualified class name as parameter is mandatory., (*7)
Classes injected by di_get
or di_make
must not have constructor parameters.
Surprisingly *cough* there is no need for constructor parameters., (*8)
class alphaCentauri { //... public function construct() { $this->dependency1 = di_get(DependencyOne::class); $this->dependency2 = di_get(DependencyTwo::class); $this->parameterAlpha = di_param('alpha'); $this->parameterBeta = di_param('beta'); $this->parameterGamma = di_param('gamma'); } //... }
Method and even function injection is supported., (*9)
class Example { public function getResult($param) { $math = di_get(Math::class); return $math->calculate($param, di_param('pi')); } } function depending_on_injection($param) { $math = di_get(Math::class); return $math->calculate($param, di_param('pi')); } $result = depending_on_injection(42);
There is no need for any further dependency configuration!, (*10)
Of course you need to import the parameters though., (*11)
A parameter is key value pair. The key is a string
and the value can be of any
type - even a closure. There may be other restricting factors though. If you choose
to organize your parameters in YAML-files you might be restricted to string
, int
,
float
, bool
and array., (*12)
di_param('key_name')
returns the parameter value that belongs to the key key_name
., (*13)
You can check for existence by di_has_param('key_name')
., (*14)
If a parameter is generated dynamically you can use di_set_param('key_name', $value)
to make it globally available. A immutable request object would make in most use
cases a valuable dynamically generated parameter., (*15)
Hint: A mutable global parameters is no parameter at all. Otherwise it would introduce really huge side effects., (*16)
If the parameter is not set a NotFoundException
is thrown by di_param
. You can
suppress this behaviour by supplying a default parameter other than null as second
argument. Instead of throwing an exception the default parameter is returned., (*17)
In big applications the parameter key names can cause naming conflicts. Therefore it is a good idea to organize your parameter keys in a tree hierarchy like the namespaces in php. Arrays give you that tree hierarchy for free but it is not easy to see the tree behind, (*18)
di_param('base_key')['level1']['level2']['parameter_name'];
It gets even harder if you have to search parameter_name
in unknown code as it
may be used by several different parameters. Therefore earc/di supports the dot
syntax for di_param
, di_has_param
and di_set_param
., (*19)
In dot syntax the above would be, (*20)
di_param('base_key.level1.level2.parameter_name');
di_import_param
takes an (potentially multidimensional) array as argument. This
keeps it flexible for all implementation details and frameworks. You can hardcode
your Parameters, (*21)
# config.php di_import_param([ 'data' => [ 'server' => [ 'mysql' => [ 'user' => 'foo', 'db' => 'bar', 'password' => 'x23W!_bxTff', //... ], //... ], //... ], //... ]);
or use the popular YAML-format, (*22)
# config.yml data: server: mysql: user: 'foo' db: 'bar' password: 'x23W!_bxTff' ...
# bootstrap.php di_import_param( Yaml::parse( file_get_contents('/path/to/config.yml') ) );
di_import_param
is different to di_set_param
as parameters may be overwritten.
Thus libraries are able to offer default parameter which may (or may not) be changed
by software using this library., (*23)
Define in your Project one ParameterInterface
per module. Define all parameter
keys (dot syntax) via constants in the ParameterInterface
. Let all classes using
parameters implement the ParameterInterface
of the module., (*24)
For example the ParameterInterface
of the
earc/event-tree
let you know all Parameters that might be relevant to you. You don't need to know
all the documentation, just one file. Such explicit programming could be a live
saver in sparsely documented projects., (*25)
interface ParameterInterface { const VENDOR_DIR = 'earc.vendor_directory'; const ROOT_DIRECTORIES = 'earc.event_tree.directories'; const BLACKLIST = 'earc.event_tree.blacklist'; }
If you want to use objects of third party libraries which are not using earc/di,
they are sometimes not easy to build. Think of the doctrine EntityManager
. It is
such a common object in apps using doctrine, it is a serious obstacle to inject
a factory instance instead of the real object., (*26)
Use di_register_factory
instead to registers a callable
to a fully qualified
class name., (*27)
di_register_factory(SomeInterface::class, [Factory::class, 'build']);
Thereafter the callable
is used for building the object., (*28)
If an instance was retrieved by get
already, you must call clear_cache($fCQN)
manually to get
the instance from the factory., (*29)
If you register a second factory to the same fully qualified class name, the second
factory is used. By passing null
as factory parameter you can unregister a factory., (*30)
Using an interface as argument of di_get
or di_make
is a good idea. earc/di uses
the argument as type hint. A class as argument states that all potentially injected
classes have to inherit from the type hinted class. An interface as argument results
in a potentially wider range of classes to inject as they only need to implement the
interface. But that freedom comes with a trade-off: earc/di does not know the class
to build and inject anymore. Therefore each interface used as parameter for di_get
or di_make
must be decorated first., (*31)
di_decorate(SomeInterface::class, ThisImplementsTheInterface::class);
But decoration is much more. It is where the last decision is made what to inject and this as many times as it is needed., (*32)
It is where the inversion of control takes place, the service locator part., (*33)
Assume you write a library. You add some cool features and bugfixes every month. You have a job, a wife, a few kids, so your reaction time on issues and merge requests does not perform very well. Your library is used as third party library in production nevertheless. After an upgrade an error in a method is detected in production. The programming engineers need a quick way to fix your library. They do not want to fork your library, because keeping a forked library up to date could be a big task. Wouldn't it be nice if they could just exchange the method., (*34)
di_decorate
does the job. Extending the class, overwriting the erroneous method
and decorating the original class is all it needs., (*35)
di_decorate(ServiceContainingAnError::class, ServiceDecorator::class); get_class(di_get(ServiceContainingAnError::class)); // equals ServiceDecorator::class get_class(di_make(ServiceContainingAnError::class)); // equals ServiceDecorator::class
Everywhere the error-prone service was injected now the class with the fix demands its place., (*36)
earc/di enables you to decorate abstract classes., (*37)
$staticService = di_static(AbstractService::class); $staticService::staticMethod(); // calls AbstractService::staticMethod() di_decorate(StaticService::class, AbstractServiceDecorator::class); $staticService = di_static(StaticService::class); $staticService::staticMethod(); // calls AbstractServiceDecorator::staticMethod()
Decoration is something you do in the configuration part of your software. To be not
too cumbersome there is a way to do this in a configuration file. On calling DI::importParameter()
the parameter earc.di.class_decoration
is imported., (*38)
# config.yml earc: di: class_decoration: namespace\\A\\SomeServiceClass: 'namespace\\B\\SomeServiceClass' namespace\\A\\SomeOtherServiceClass: 'namespace\\B\\SomeOtherServiceClass' ...
use eArc\DI\DI; DI::init(); di_import_param( Yaml::parse( file_get_contents('/path/to/config.yml') ) ); DI::importParameter();
Please note the order of the calls is important., (*39)
For debugging purpose di_is_decorated
and di_get_decorator
are handy functions.
But be aware that it debugs the current decoration, not the result of a decorator
chain., (*40)
di_decorate(Service::class, ServiceDecorator::class); di_decorate(ServiceDecorator::class, MegaDecorator::class); di_get_decorator(Service::class); // equals ServiceDecorator::class get_class(di_get(Service::class)); // equals MegaDecorator::class
To clear a decoration decorate a class by itself., (*41)
di_decorate(Service::class, Service::class); di_is_decorated(Service::class); // equals false get_class(di_get(Service::class)); // equals Service::class
Sometimes you mirror the file structure from one project in another project. Then
it may be desirable to decorate a class automatically by (re)placing it in the mirrored
file structure. That is what namespace decoration is for. You can activate this feature
by setting the earc.di.namespace_decoration
parameter before calling DI::importParameter()
., (*42)
# config.yml earc: di: namespace_decoration: ['namespace\\of\\dir\\project\\A', 'namespace\\of\\mirrored\\dir\\project\\B'] # you can decorate as many namespaces you want. # but you can even chain them. You have to name # every namespace decoration explicitly: ['namespace\\of\\mirrored\\dir\\project\\B', 'namespace\\of\\mirrored\\dir\\project\\C'] ['namespace\\of\\dir\\project\\A', 'namespace\\of\\mirrored\\dir\\project\\C']
use eArc\DI\DI; DI::init(); di_import_param( Yaml::parse( file_get_contents('/path/to/config.yml') ) ); DI::importParameter();
In most testing libraries mocks are objects. Thus you can't use decoration for replacing
the original class. di_mock
is made for this use cases., (*43)
$getObj = di_get(Service::class); $makeObj = di_get(Service::class); $mockedService = TestCase::createMock(Service::class); di_mock(Service::class, $mockedService); Assert::assertSame($mockedService, di_get(Service::class)); // passes Assert::assertSame($mockedService, di_make(Service::class)); // passes Assert::assertSame($getObj, di_get(Service::class)); // fails Assert::assertSame($makeObj, di_make(Service::class)); // fails
Keep in mind: Static accessed methods (via di_static
) can be mocked via decoration only
as di_static
returns a string
and di_mock
takes an object
., (*44)
di_static(StaticService::class)::staticMethod(); // calls StaticService::staticMethod() di_decorate(StaticService::class, StaticMock::class); di_static(StaticService::class)::staticMethod(); // calls StaticMock::staticMethod()
You can check if an service is mocked by di_is_mocked
., (*45)
Assert::assertSame(true, di_is_mocked(ServiceClass)); // passes Assert::assertSame(true, di_is_mocked(AnotherService::class)); // fails
If you need the real service again use di_clear_mock
., (*46)
di_clear_mock(Service::class); Assert::assertSame($mockedService, di_get(Service::class)); // fails Assert::assertSame($mockedService, di_make(Service::class)); // fails Assert::assertSame($getObj, di_get(Service::class)); // passes Assert::assertSame($makeObj, di_make(Service::class)); // fails
You can even clear all existing mocks., (*47)
di_clear_mock();
Mocking is applied after decoration. But the di_*mock
functions does not take
any decoration into account. That gives you more grip on the mocking process but
more care is also needed. Keep in mind, you have to mock the decorator not the decorated
class., (*48)
di_mock(Service::class, (object) ['iAmMock' => 1]); di_mock(ServiceDecorator::class, (object) ['iAmMock' => 2]); di_decorate(Service::class, ServiceDecorator::class); Assert::assertSame(true, di_is_mocked(Service::class)); // passes Assert::assertSame(true, di_is_mocked(ServiceDecorator::class)); // passes Assert::assertSame(1, di_get(Service::class)->iAmMock); // fails Assert::assertSame(2, di_get(Service::class)->iAmMock); // passes Assert::assertSame(2, di_get(ServiceDecorator::class)->iAmMock); // passes
As you see in the example code mocks are not forced to follow the type hints. This means you can pass as mock whatever you want. (As long as your code do not pass your Services as type hinted arguments around.), (*49)
Maybe you solve a problem by implementing the chain of responsibility - design pattern. Only the third party software knows which services add to this implementation. This leaves four questions:, (*50)
Three answers solves earc/di for you through tagging., (*51)
The third party can store the relevant piece of information by executing, (*52)
di_tag('tag.name', Service002::class); di_tag('tag.name', Service007::class); di_tag('tag.name', Service014::class);
Your base class can retrieve the service classes by iterating over di_get_tagged('tag.name')
, (*53)
foreach (di_get_tagged('tag.name') as $handlerName => $argument) { $handler = di_get($handlerName); if ($handler->canHandleTask($task)) { $result = $handler->handleTask($task); return $result; } } throw new TaskNotHandledException($task);
If you would have pass a third argument to di_tag
$argument
would hold this
argument instead of null
., (*54)
Of course the services should implement an interface and the base service should check for it to avoid failure on name conflicts or forgotten methods. And yes, logging handlers not implementing the interface is a good idea. But you know about the architecture your software needs (and can afford) best., (*55)
Hint: Decoration is not applied to the tags but to di_get
of course., (*56)
earc/di has dropped circular dependency detection in favour of performance. If you experience an error like the following in the earc/di code its cause is most likely a circular dependency. Search for a circular class dependencies in your code., (*57)
$ PHP Fatal error: Uncaught Error: Maximum function nesting level of '256' reached, aborting!
All exceptions thrown inherit from BaseException
., (*58)
An InvalidArgumentException
is thrown if, (*59)
init()
is called and the classes identified by the parameter does not implement
the ResolverInterface
and the ParameterBagInterface
respectively.di_make()
or di_get()
use decorators that do not respect the type hint given
by the argument. (Please note, this check is not done on calling di_decorate
to avoid early loading of the class files.)di_set_param
would overwrite an existing parameter. An MakeClassException
is thrown if some Exception is thrown while calling the
class constructor., (*60)
An NotFoundDIException
is thrown if a parameter should be retrieved that never
was set/imported., (*61)
Some usages are not obvious but desirable, like function decoration. If you call
your own functions with di_static
they can be decorated using di_decorate
., (*62)
function something_cool($times) { return $times.' x icecream'; } di_static('something_cool')(42); // returns '42 x icecream' function something_cool_but_its_winter($times) { return $times.' x hot tea'; } di_decorate('something_cool', 'something_cool_but_its_winter'); di_static('something_cool')(42); // returns '42 x hot tea'
The classes that handle the behaviour of earc/di have in sum round about 250 lines of code, fast array calculations mainly. But nevertheless some big app or server limitations may force a second thought on performance., (*63)
Objects retrieved by di_get
can not be garbage collected until someone calls di_clear_cache
.
If you use an object only once or for a tiny moment it might save a bit of memory
if you use di_make
instead., (*64)
Calling di_has
performs existence checks for classes and interfaces. This checks
can trigger an autoload. Use them wisely. The same is true for di_is_decorated
in
the case you use namespace decoration., (*65)
Namespace decoration uses string substitution which is considerable slower than key lookups on arrays. Nevertheless if you have to decorate hundreds of classes it outperforms explicit configuration since it is used only on classes which are active for the request, wheres configuration must be processed on every request for all decorated classes., (*66)
Don't rely on the singleton behaviour of di_get
. Its main purpose are performance
considerations. If your architecture need to get always the same instance for a
class make it explicit and use a real singleton instead., (*67)
In earc/di each type hint is set globally. This means each type hint forces exactly
one class. If you feel pushed to use di_decorate
outside of the configuration part
of your app to change that behaviour you experience a bad architecture smell. It shows
that some of your classes demand more than they type hint. Maybe your interfaces
do not follow the interface segregation principle, maybe you need just another
interface or maybe some of your classes do not follow the single responsibility
principle., (*68)
You are able to completely rewrite the behavioral logic behind the scene. This enables
earc/di to integrate with nearly all dependency injection systems. Extend the Resolver::class
or the ParameterBag::class to your need or implement the corresponding Interfaces.
Register your class(es) by the DI::init
method. Now the di_*
functions
follow the logic you have implemented., (*69)
If the third party di-system uses a container then integration is a beginners task.
You can find a ready to use example for symfony in the bridge folder. Don't forget
to switch your symfony service definitions to public
., (*70)
If you use symfony register the SymfonyDICompilerPass and you are ready go (or to migrate step by step)., (*71)
There is no limit. Create your own one to rule them all and make your live easy again., (*72)
di_get
, di_make
and di_static
complete rewrite based on a new view on dependency injection, (*73)
usage of global functions for injection, (*74)
support for container merging dropped - in favour of customization (via extending or interfaces), (*75)
support for flags dropped - all can now be done explicit, (*76)
support for tree typed dependencies dropped - extend your classes with different constructors or use a factory to support different injection types (its more explicit by the way), (*77)
support for circular dependency detection dropped - in favour of php doing the job, (*78)
all types of dependency configuration are dropped - in favour of pure injection (Yes, its true! There is no dependency configuration anymore just parameter import, decoration, tagging and mocking. Dependency configuration is reduced to the service locator pattern it is based on. Each type hint is set globally, which reduces the information you must cope with significantly.), (*79)
dependency on other libraries dropped - in favour of a lightweight architecture, (*80)
support for flags., (*81)
support for container merging and container merging at construction time only., (*82)
announced new feature subset generation is dropped in favour of earc/components-di, (*83)
NotFoundException
and OverwriteException
dropped in favour of
ItemNotFoundException
and ItemOverwriteException
from the
earc/payload-container package., (*84)
DependencyContainer::loadFile()
is no longer supported. You can emulate it
by DependencyContainer::load(include ...)
., (*85)
the first official release, (*86)
eArc - the explizit architecture framework - dependency injection container
MIT