A powerful Container-Interop/PSR-11 implementation of dependency injection library for PHP.

2.0.1 2016-07-20 10:04 UTC

README

Build Status Code Quality Code Climate PHP 7 ready HHVM Latest Stable Version License

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 and MyCache 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 the MyCacheDriver instance) automatically when creating the MyCache 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 the driver 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 the di.service node.

Features

  • References

    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.

    • Parameter references

      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 references

      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 and mapping

    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

    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 the cache service definition in the form of [ callableOrMethodName, OptionalArgumentArray ], these methods will be executed right after cache 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
      ]);
  • Container delegation

    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 the ContainerInterface).

    • 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');
  • Object scope

    • 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 method one() or define the service with Container::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 each get() 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 the A, but also shared among different instances of A. What if user want to share C only under the A but not between A ?

      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
  • Array access and read only

    Both Phossa2\Di\Container and Phossa2\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 using set().

    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

  • Container related

    • get(string $id): object from ContainerInterface

      If 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 ExtendedContainerInterface

      Get a new instance for this $id. May provide new arguments for the object constructor.

    • run(mixed $callable, array $args = []): mixed from ExtendedContainerInterface

      Execute a callable or pseduo callable (with references) with provided arguments (may contain references also)

    • param(string $name, mixed $value): bool from ExtendedContainerInterface

      Set 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 ExtendedContainerInterface

      Aliasing an object as $id in the container. Difference with set() method is that getting this object will skip the execution of common methods.

      Returns true on success and false on failure.

    • auto(bool $flag): $this from AutoWiringInterface

      Set container autowiring ON or OFF.

    • isAuto(): bool from AutoWiringInterface

      Test container is auto wiring ON or OFF.

    • resolve(mixed &$data): $this from ReferenceResolveInterface

      Recursively resolving references in the $data. $data might be string or array. Other data type will be untouched.

    • share(bool $flag = true): $this from ScopeInterface

      Make container default scope to Container::SCOPE_SHARED or Container::SCOPE_SINGLE.

    • set(string $id, mixed $value): bool from WritableInterface

      Set 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. Returns true on success or false on failure. Container has to be writable, otherwise a LogicException will be thrown.

    • isWritable(): bool from WritableInterface

      Is this container writable ?

    • setWritable(bool $writable): bool from WritableInterface

      Set this container writable or readonly.

  • Delegator related

    • get(string $id): object from ContainerInterface

      Get 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 WritableInterface

      Set a service with $id into the delegator as long as the delegator is writable.

    • isWritable(): bool from WritableInterface

      Is this delegator writable ?

    • setWritable(bool $writable): bool from WritableInterface

      Set this delegator writable or readonly.

    • addContainer(ContainerInterface $container): $this from DelegatorInterface

      Add 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

License

MIT License