phossa2 / di
A powerful Container-Interop/PSR-11 implementation of dependency injection library for PHP.
Requires
- php: >=5.4.0
- container-interop/container-interop: ~1.0
- phossa2/config: ^2.0.12
- phossa2/shared: ^2.0.19
Requires (Dev)
- phpunit/phpunit: 4.*
Provides
Replaces
This package is not auto-updated.
Last update: 2020-01-24 16:09:47 UTC
README
PLEASE USE phoole/di library instead
phossa2/di is a fast and powerful Container-Interop or PSR-11 implementation of dependency injection library for PHP. It builds upon the versatile phossa2/config library and supports autowiring, container delegation, object decorating, object scope and more.
It requires PHP 5.4, supports PHP 7.0+ and HHVM. It is compliant with PSR-1, PSR-2, PSR-4, and proposed PSR-5, PSR-11.
Installation
Install via the composer
utility.
composer require "phossa2/di=2.*"
or add the following lines to your composer.json
{ "require": { "phossa2/di": "2.*" } }
Usage
-
With autowiring of classes.
A couple of simple classes as follows,
// file cache.php class MyCache { private $driver; public function __construct(MyCacheDriver $driver) { $this->driver = $driver; } // ... } class MyCacheDriver { // ... }
Instead of creating
MyCacheDriver
andMyCache
instances manually, you may get the both instances automatically using the DI container,use Phossa2\Di\Container; // should be aware of these classes require_once __DIR__ . '/cache.php'; // create the container $container = new Container(); // 'MyCache' classname as the service id if ($container->has('MyCache')) { $cache = $container->get('MyCache'); }
With autowiring is turned on by default, the container will look for the
MyCache
class if no service defined as 'MyCache', and resolves its dependency injection (here, is theMyCacheDriver
instance) automatically when creating theMyCache
instance. -
With manual service addition using
set()
Services can be added to the container manually by using
set()
method.use Phossa2\Di\Container; // should be aware of these classes require_once __DIR__ . '/cache.php'; // create the container $container = new Container(); // turn off autowiring $container->auto(false); // define service with an array $container->set('cache', [ 'class' => 'MyCache', // classname 'args' => ['${#driver}'] // constructor arguments ]); // add service 'driver' with a callback $container->set('driver', function() { return new \MyCacheDriver(); }); // get the service var_dump($container->get('cache') instanceof \MyCache); // true
A service reference
'${#driver}'
used in the constructor arguments here indicating it is thedriver
service instance from the container. -
With configuration from files or array
Container uses a
Phossa2\Config\Interfaces\ConfigInterface
instance as its definition resolver for parameter and service definitions. The config instance may either read configs from files or get configs from an array as follows,use Phossa2\Di\Container; use Phossa2\Config\Config; $configData = [ // container service definitions 'di.service' => [ // cache service 'cache' => ['class' => 'MyCache', 'args' => ['${#driver}']], // cache driver, classname directly 'driver' => 'MyCacheDriver', ], // common methods to run after each instantiation 'di.common' => [ [ function($obj) { return $obj instanceof \MyCacheDriver; }, // tester function($obj, $container) { echo "ok"; } // runner ], ], // init methods after container created 'di.init' => [ // default section 'default' => [ ['setLogger', ['${#logger}']], // ... ], // my own section 'mystuff' => [ ], ], ]; // create $config instance with provided data $config = new Config(null, null, $configData); // instantiate container with $config instance and definition base node 'di' $container = new Container($config, 'di'); // get service by id 'cache' (di.service.cache) $cache = $container->get('cache'); // true var_dump($cache instanceof \MyCache);
By default, container related configurations are under the node
di
and service definitions are under thedi.service
node.
Features
-
References in the form of '${reference}' can be used to refer to predefined parameters from the config or services in the container.
Characters of
'$', '{', '}', '.'
are not allowed in reference name. Characters of'#', '@'
have special meanings, such that should not be part of normal service names.-
See phossa2/config reference for detail. Parameter references are read from configuration files or can be defined by container method
param()
as follows,// define a new parameter for the container $container->param('cache.dir', '${system.tmpdir}/cache'); // use the cache.dir parameter defined above $container->set('cache', [ 'class' => '${cache.class}', // predefined in file 'args' => ['${cache.dir}'] // just defined above ]);
-
Service reference in the form of '${#service_id}' can be used to referring a service instance in the container (or in the delegator).
$container->set('cache', [ 'class' => '${cache.class}', 'args' => ['${#cache_driver}'] // service reference ]);
Two reserved service references are '${#container}' and '${#config}'. These two are refering the container instance itself and the config instance it is using. These two can be used just like other service references.
-
Using references
References can be used anywhere in the configs or as the arguments for all container methods (except for the paramter
$id
of the methods).// run(callable, arguments) with references $container->run(['${#logger}', 'warning'], ['warning from ${log.facility}']); // resolve references $data = ['${system.dir}', '${#logger}']; $container->resolve($data); // all references in $data are now resolved
-
-
Autowiring is the ability of container instantiating objects and resolving its dependencies automatically by their classname. The base for autowiring is the PHP function parameter type-hinting.
By reflecting on class, constructor and methods, phossa2/di is able to find the right class for the instance (user need to use the classname as the service id) and right classes for its dependencies (type-hinted with the classnames).
If interface name is used for dependency type-hint, users may set up the mapping of interfaces to the right classnames as follows,
// map an interface to a classname $container->set( 'Phossa2\\Cache\\CachePoolInterface', // MUST NO leading backslash 'Phossa2\\Cache\\CachePool' ); // map an interface to a service id reference $container->set('Phossa2\\Cache\\CachePoolInterface', '${#cache}'); // map an interface to a parameter reference $container->set('Phossa2\\Cache\\CachePoolInterface', '${cache.class}'); // map an interface to a callback $container->set('Phossa2\\Cache\\CachePoolInterface', function() { return new \Phossa2\Cache\CachePool(); });
Or define mappings in the config node
di.service
as follows,$configData = [ // ... 'di.service' => [ 'Phossa2\\Cache\\CachePoolInterface' => '${cache.class}', // ... ], // ... ];
Autowiring can be turned on/off. Turning off autowiring will enable user to check any defintion errors without automatic loading.
// turn off auto wiring $container->auto(false); // turn on auto wiring $container->auto(true);
-
Object decorating is to apply decorating changes (executing methods etc.) right after the instantiation of a service instance.
-
Decorating methods for individual instance only
$container->set('cache', [ 'class' => 'Phossa2\\Cache\\Cache', 'args' => ['${#cachedriver}'], // constructor arguments 'methods' => [ ['clearCache'], // method of $cache ['setLogger', ['${#logger}']], // method with arguments [[$logger, 'setLabel'], ['cache_label']], // callable with arguments [['${#driver}, 'init']], // pseduo callable // ... ], ]);
By adding
methods
section into thecache
service definition in the form of[ callableOrMethodName, OptionalArgumentArray ]
, these methods will be executed right aftercache
instantiation.callableOrMethodName
here can be,-
method name of current instance
-
a valid callable
-
a psedudo callable with references (after resolving the references, it is a valid callable).
OptionalArgumentArray
here can be,-
empty
-
array of values or references
-
-
Common decorating methods for all instances
$configData = [ // common methods for all instances 'di.common' => [ // [ tester(): bool, method ] [ function($object, $container) { return $object instanceof 'Psr\\Log\\LoggerAwareInterface' }, ['setLogger', ['${#logger}']] ], // ... ], ];
Common methods can be configured in the 'di.common' node to apply to all the instances right after their instantiation. The definition consists of two parts, the first is a tester callable takes current instance and the container as parameters and returns a boolean value. The second part is in the same method format as in the service definition 'methods'.
To skip execution of common methods for one service, define it with
skip
as follows,$container->set('logger', [ 'class' => 'Phossa2\Logger\Logger', 'skip' => true ]);
-
-
According to Interop Container Delegate Lookup, container may register a delegate container (the delegator), and
-
Calls to the
get()
method should only return an entry if the entry is part of the container. If the entry is not part of the container, an exception should be thrown (as requested by theContainerInterface
). -
Calls to the
has()
method should only return true if the entry is part of the container. If the entry is not part of the container, false should be returned. -
If the fetched entry has dependencies, instead of performing the dependency lookup in the container, the lookup is performed on the delegate container (delegator).
-
Important By default, the lookup SHOULD be performed on the delegate container only, not on the container itself.
phossa2/di fully supports the delegate feature.
use Phossa2\Di\Delegator; // create delegator $delegator = new Delegator(); // create container $container = new Container(); // insert container into delegator $delegator->addContainer($container); // get from delegator now $cache = $delegator->get('cache');
-
-
-
Shared or single scope
By default, service instances in the container are shared inside the container. Actually they have the scope of
Container::SCOPE_SHARED
. If users want different instance each time, they may either use the methodone()
or define the service withContainer::SCOPE_SINGLE
scope.// cache service by default is in shared scope $cache1 = $container->get('cache'); // get again $cache2 = $container->get('cache'); // same var_dump($cache1 === $cache2); // true // a new cache instance with 'one()' $cache3 = $container->one('cache'); // different instances var_dump($cache1 !== $cache3); // true // but both share the same cacheDriver dependent service var_dump($cache1->getDriver() === $cache3->getDriver()); // true
Or define the service as
Container::SCOPE_SINGLE
$container->set('cache', [ 'class' => '\\Phossa2\\Cache\\CachePool'), 'scope' => Container::SCOPE_SINGLE ]); // each get() will return a new cache $cache1 = $container->get('cache'); $cache2 = $container->get('cache'); // different instances var_dump($cache1 !== $cache2); // true // dependent service are shared var_dump($cache1->getDriver() === $cache->getDriver()); // true
Set the container's default scope to
Container::SCOPE_SINGLE
will cause eachget()
returns a new instance (unless 'scope' is explicitly defined as for this service).// set default scope to SCOPE_SINGLE $container->share(false); // a new copy of cache service $cache1 = $container->get('cache'); // another new cache service $cache2 = $container->get('cache'); // different instances var_dump($cache1 !== $cache2); // true // dependencies are different var_dump($cache1->getDriver() !== $cache->getDriver()); // true
-
Use your own scope
You may get an instance in your own scope as follows, no matter whatever the default scope or the defined scope for this instance,
// instance in scope 'myScope' $cacheOfMyScope = $container->get('cache@myScope'); // new instance in single scope, even though you specified one $cacheOfSingle = $container->one('cache@myScope'); // instance in shared scope $cache = $container->get('cache');
Service references can also have scope defined as follows,
$container->set('cache', [ 'class' => 'Phossa2\\Cache\\Cache', 'args' => ['${#driver@myScope}'] // use driver of myScope ]);
-
Share instance only in certain object
Sometimes, user may want to share one instance only inside certain object.
class A { private $b, $c; public function __construct(B $b, C $c) { $this->b = $b; $this->c = $c; } public function getB() { return $this->b; } public function getC() { return $this->c; } } class B { private $c; public function __construct(C $c) { $this->c = $c; } } class C { } // an instance of A $a1 = $container->one('A'); // another instance of A $a2 = $container->one('A'); // $a1 and $a2 is different var_dump($a1 !== $a2); // true // C is the same under A var_dump($a1->getC() === $a1->getB()->getC()); // true // C is also shared among different A var_dump($a1->getC() === $a2->getC()); // true
In previous code,
C
is not only shared under theA
, but also shared among different instances ofA
. What if user want to shareC
only under theA
but not betweenA
?By setting scope of
C
to '#A' as follows,// this scope only takes effect when under service A $container->set('C', [ 'class' => 'C', 'scope' => '#A']); // an instance of A $a1 = $container->one('A'); // another instance of A $a2 = $container->one('A'); // C is different among different A var_dump($a1->getC() !== $a2->getC()); // true // C is same under one A var_dump($a1->getC() === $a1->getB()->getC()); // true
-
-
Both
Phossa2\Di\Container
andPhossa2\Di\Delegator
implements\ArrayAccess
interface.$container = new Container(); $delegator = new Delegator(); $delegator->addContainer($container); // equals to $delegator->has('A') if (isset($delegator['A'])) { var_dump($delegator['A'] === $container['A']); // true }
By default
Phossa2\Di\Container
is writable which means user can add new service definitions to the container manually by usingset()
.Note If container is set readonly, auto wiring is turned off automatically.
To get a readonly container,
$container = new Container(); $container->setWritable(false); var_dump($container->isWritable() === false); // true // delegator also $delegator = new Delegator(); $delegator->setWritable(false); var_dump($delegator->isWritable() === false); // true
APIs
-
-
get(string $id): object
from ContainerInterfaceIf not found,
Phossa2\Di\Exception\NotFoundException
is thrown.$id
may have '@scope' appended. An array can be used as the second parameter for object constructor's arguments. -
has(string $id): bool
from ContainerInterface$id
may have '@scope' appended. -
one(string $id, array $args = []): object
from ExtendedContainerInterfaceGet a new instance for this
$id
. May provide new arguments for the object constructor. -
run(mixed $callable, array $args = []): mixed
from ExtendedContainerInterfaceExecute a callable or pseduo callable (with references) with provided arguments (may contain references also)
-
param(string $name, mixed $value): bool
from ExtendedContainerInterfaceSet a parameter in the container's config tree for later to be used as a reference. Returns a bool value to indicate status.
-
alias(string $id, object|string $object): bool
from ExtendedContainerInterfaceAliasing an object as
$id
in the container. Difference withset()
method is that getting this object will skip the execution of common methods.Returns
true
on success andfalse
on failure. -
auto(bool $flag): $this
from AutoWiringInterfaceSet container autowiring
ON
orOFF
. -
isAuto(): bool
from AutoWiringInterfaceTest container is auto wiring
ON
orOFF
. -
resolve(mixed &$data): $this
from ReferenceResolveInterfaceRecursively resolving references in the
$data
.$data
might bestring
orarray
. Other data type will be untouched. -
share(bool $flag = true): $this
from ScopeInterfaceMake container default scope to
Container::SCOPE_SHARED
orContainer::SCOPE_SINGLE
. -
set(string $id, mixed $value): bool
from WritableInterfaceSet a service with
$id
into the container.$value
can be an array in the form of array as follows,$value = [ 'class' => mixed, // classname/object/callback etc. 'args' => array, // arguments for the constructor or callback 'scope' => string, // default scope for this service 'skip' => bool, // skip common methods for the instance ];
or
$value
can be classname, object, callback etc. Returnstrue
on success orfalse
on failure. Container has to be writable, otherwise aLogicException
will be thrown. -
isWritable(): bool
from WritableInterfaceIs this container writable ?
-
setWritable(bool $writable): bool
from WritableInterfaceSet this container writable or readonly.
-
-
-
get(string $id): object
from ContainerInterfaceGet service from the delegator. If not found,
Phossa2\Di\Exception\NotFoundException
is thrown.$id
may have '@scope' appended as long as underlying container supports this feature. -
has(string $id): bool
from ContainerInterface$id
may have '@scope' appended as long as underlying container supports this feature. -
set(string $id, mixed $value): bool
from WritableInterfaceSet a service with
$id
into the delegator as long as the delegator is writable. -
isWritable(): bool
from WritableInterfaceIs this delegator writable ?
-
setWritable(bool $writable): bool
from WritableInterfaceSet this delegator writable or readonly.
-
addContainer(ContainerInterface $container): $this
from DelegatorInterfaceAdd a container to the delegator.
-
Change log
Please see CHANGELOG from more information.
Testing
$ composer test
Contributing
Please see CONTRIBUTE for more information.
Dependencies
-
PHP >= 5.4.0
-
phossa2/config >= 2.0.12
-
phossa2/shared >= 2.0.21