Yet Another (PHP) Dependency Injection Framework
PHP
Switch branches/tags
Nothing to show
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
tests
Container.php
Exception.php
LICENSE.txt
README.md

README.md

Yadif - Yet Another Dependency Injection Framework

Inject dependencies via a very simple configuration mechanism.

Table of Contents

  1. Basic Syntax
  2. Object Configuration
  3. Scope Config
  4. Setting non-object parameters
  5. Creating entities or value objects through Container
  6. Zend Framework Front Controller Example
  7. Zend_Config Support
  8. TODOS and Open Questions
  9. Instantiation with Factory methods
  10. Injecting Container Reference or Clones

1. Basic Syntax

Take this constructor and setter-less class:

class Foo
{
}

Creating a Yadif Container configured to create this class looks:

$config = array('Foo' => array());
$yadif  = Yadif_Container::create($config);
$foo    = $yadif->getComponent('Foo');

####2. Object Configuration This current fork has a slighty different configuration syntax than the original:

class Foo
{
    public function __construct($arg1, $arg2) { }

    public function setA($arg1) { }

    public function setB($arg2, $arg3) { }
}

$config = array(
    'Foo' => array(
        'class' => 'Foo',
        'arguments' => array('ConstructorArg1', 'ConstructorArg2'),
        'methods' => array(
            array(
                'method' => 'setA',
                'arguments' => array('Inject1'),
            ),
            array(
                'method' => 'setB',
                'arguments' => array('Inject2', 'Inject3'),
            ),
        ),
        'scope' => 'singleton',
    ),
    'ConstructorArg1' => array('class' => 'ConstructorArg1'),
    'ConstructorArg2' => array('class' => 'ConstructorArg2'),
    'Inject1'         => array('class' => 'Inject1'),
    'Inject2'         => array('class' => 'Inject2'),
    'Inject3'         => array('class' => 'Inject3'),
);

$yadif = new Yadif_Container($config);
$foo   = $yadif->getComponent("Foo");

Would do internally:

$foo   = new Foo($yadif->getComponent('ConstructorArg1'), $yadif->getComponent('ConstructorArg2'));
$foo->setA($yadif->getComponent('Inject1'));
$foo->setB($yadif->getComponent('Inject2'), $yadif->getComponent('Inject3'));

Now 'ConstructorArg1', 'ConstructorArg2', 'Inject1', 'Inject2' and 'Inject3' would also have to be defined as classes to be constructed correctly.

3. Scope Config

Currently there are two different scopes: 'singleton' and 'prototype'. The first one enforces the creation of only one object of the given type. The second one creates new objects on each call of getComponent().

4. Setting non-object parameters

Non-object parameters are bound to methods and constructors in a PDO like binding syntax. For any non-oject parameter the syntax "double-colon name" has to be used to indicate the parameter as non-object parameter.

As an example we take this Class:

class Foo {
    public function __construct($bar, $number) { }

    public function setNumber($number) { }
}

There are different scopes for these bounded parameters. A global scope and a method aware only scope.

i.) Global Scope

Via Yadif_Container::bindParam() it is possible to set global parameters to a container.

$config = array(
    'Foo' => array(
        'class' => 'Foo',
        'arguments' => array(':bar', ':number'),
    ),
);

$yadif = new Yadif_Container($config);
$yadif->bindParam(':bar', 'BarName');
$yadif->bindParam(':number', 1234);

$foo = $yadif->getComponent('Foo');

ii.) Method aware scope

Via the configuration mechanism you can bind parameters to a specific method where the parameters occour. An example for the object constructor looks like this:

$config = array(
    'Foo' => array(
        'class' => 'Foo',
        'arguments' => array(':bar', ':number'),
        'parameters' => array(':bar' => 'BarName', ':number' => 1234),
    ),
);

$yadif = new Yadif_Container($config);
$foo   = $yadif->getComponent('Foo');

For a non-object parameter in a method the syntax looks like this:

$config = array(
    'Foo' => array(
        'class' => 'Foo',
        'arguments' => array(':bar', ':number'),
        'parameters' => array(':bar' => 'BarName', ':number' => 1234),
        'methods' => array(
            array(
                'method' => 'setNumber',
                'arguments' => array(':number'),
                'parameters' => array(':number' => 1138),
            ),
        ),
    ),
);

$yadif = new Yadif_Container($config);
$foo   = $yadif->getComponent('Foo');

5. Creating entities or value objects through Container

The creation of entity or value objects mostly requires lots of arguments passed to the constructor or setter methods, paired with dependencies for example in an Active Record example with the Database Connection.

class UserActiveRecord
{
    public function setAdapter($db) { }

    public function setName($name) { }

    public function setEmail($email) { }

    public function setId($id) { }
}

$config = array(
    'DatabaseConn' => array(
        'class' => 'PDO',
        'arguments' => array('mysql://localhost', 'username', 'password'),
        'scope' => 'singleton',
    ),
    'User' => array(
        'class' => 'UserActiveRecord',
        'arguments' => array(),
        'methods' => array(
            array('method' => 'setAdapter', 'arguments' => array('DatabaseConn'),
            array('method' => 'setName',    'arguments' => ':name'),
            array('method' => 'setEmail',   'arguments' => ':email'),
            array('method' => 'setId',      'arguments' => ':id'),
        ),
        'scope' => 'prototype' // instantiate new object on each call of getComponent()
    ),
);

$yadif = new Yadif_Container($config);
$db    = $yadif->getComponent('DatabaseConn');
$stmt = $db->query("SELECT * FROM Users");

$users = array();
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    $user = $yadif->bindParam(':name', $row['name'])
                  ->bindParam(':id',   $row['id'])
                  ->bindParam(':email', $row['email'])
                  ->getComponent('User');
}

You could also use:

$user = $yadif->bindParams(array(':name' => $row['name'], ':id' => $row['id'], ':email' => $row['email']))->getComponent('User');

6. Zend Framework Front Controller Example

$config = array(
    'Request' => array(
        'class' => 'Zend_Controller_Request_Http',
    ),
    'Response' => array(
        'class' => 'Zend_Controller_Response_Http',
    ),
    'Router' => array(
        'class' => 'Zend_Controller_Router_Rewrite',
        'methods' => array(
            array('method' => 'addConfig', 'arguments' => array('RouterConfig', 'routes')),
        ),
    ),
    'RouterConfig' => array(
        'class' => 'Zend_Config_Ini',
        'arguments' => array(':routerConfigFile'),
        'parameters' => array(':routerConfigFile' => '/var/www/application/config/routes.ini'),
    ),
    'FrontController' => array(
        'class' => 'Zend_Controller_Front',
        'arguments' => array(),
        'methods' => array(
            // Inject Request, Response and Router
            array('method' => 'setRequest',  'arguments' => array('Request')),
            array('method' => 'setResponse', 'arguments' => array('Response')),
            array('method' => 'setRouter',   'arguments' => array('Router')),

            // Inject Plugins
            array('method' => 'registerPlugin', 'arguments' => array('ControllerPluginAccessControl')),
            array('method' => 'registerPlugin', 'arguments' => array('ControllerPluginLogging')),

            // Inject Parameters which will be used throughout controllers
            array('method' => 'setParam', 'arguments' => array('db', 'DatabaseConn')),
            array('method' => 'setParam', 'arguments' => array('logger', 'Logger')),

            // Set Controller Directory
            array(
                'method' => 'setControllerDirectory',
                'arguments' => array(':controllerDirectory')
                'parameters' => array(':controllerDirectory' => '/var/www/application/controllers/'),
            ),
        ),
        'factory' => array('Zend_Controller_Front', 'getInstance'),
    ),
    'ControllerPluginAccessControl' => array('class' => 'ControllerPluginAccessControlImplementation'),
    'ControllerPluginLogging'       => array(
        'class' => 'ControllerPluginAccessControlLogging',
        'arguments' => array('Logger'),
    ),
    'Logger' => array(
        'class' => 'Zend_Log',
        'methods' => array(
            array('method' => 'addWriter', 'arguments' => array('LoggerWriter')),
        ),
    ),
    'LoggerWriter' => array(
        'class' => 'Zend_Log_Writer_Stream',
        'arguments' => array(':loggerStream'),
        'parameters' => array(':loggerStream' => '/var/log/myapplication/errors.log'),
    ),
    'DatabaseConn' => array(
        'class' => 'Zend_Db_Adapter_Mysql',
        'arguments' => array( '%database.config%'),
    ),
);

$yadif = new Yadif_Container($config);
$front = $yadif->getComponent('FrontController');
$front->dispatch();

Will do the following internally:

$front = Zend_Controller_Front::getInstance(); // because 'factory' config is set for this one

$request = new Zend_Controller_Request_Http();
$front->setRequest($request);

$response = new Zend_Controller_Response_Http();
$front->setResponse($response);

$routerConfig = new Zend_Config('/var/www/application/config/routes.ini');
$router = new Zend_Controller_Router_Rewrite();
$router->addConfig($routerConfig);
$front->addRouter($router);

$aclPlugin = new ControllerPluginAccessControlImplementation();
$front->registerPlugin($aclPlugin);

$logger = new Zend_Log();
$writer = new Zend_Log_Writer_Stream('/var/log/myapplication/errors.log');
$logger->addWriter($writer);
$loggerPlugin = new ControllerPluginAccessControlLogging($logger);
$front->registerPlugin($loggerPlugin);

$db = new Zend_Db_Adapter_Mysql('pdo_mysql', array(..));
$front->setParam('db', $db);

// $logger already exists and is singleton
$front->setParam('logger', $logger);

$front->setControllerDirectory('/var/www/application/controllers/');

7. Zend_Config Support

You can use Zend_config objects as the primary configuration mechanism:

$config = new Zend_Config_Xml("objects.xml");
$yadif  = new Yadif_Container($config);

Also you can use specific arguments marked with %arg% to replace with values inside a given application config object.

$components = array(
    'YadifBaz' => array(
        'class' => 'YadifBaz',
        'methods' => array(
            array('method' => 'setA', 'arguments' => array('%foo.bar%'))
        ),
    ),
);
$config = new Zend_Config(array('foo' => array('bar' => 'baz')));

$yadif = new Yadif_Container($components, $config);
$baz = $yadif->getComponent("YadifBaz");

8. TODOS and Open Questions

  1. String and Component Name Ambigoutiy - How to solve it? Solved! Allow non-object parameters only through 'arguments' key of configuration and bound via ':name' syntax. See point 4 with more details on this topic.

  2. Components Nested in Arrays, should they be instantiated through the Container? Currently they are not: $foo = new Foo(array('bar' => new Bar())); <- not possible Solved! Through introduction of the injectParameters() method it is possible to implement this through recursion and even support both object and non-object parameters in the nested array.

  3. Calling static registries for example not possible: Zend_Registry::set('foo', $foo); Zend_Controller_Action_HelperStack::addHelper($helper);

  4. How to solve references to the container itsself? Example: The Front Controller is using a container to instantiate the main part of the application and then goes on in the Dispatcher to use another container to instantiate the controller. How should back-references to containers be injected into an object tree?

    $container = $container->getComponent('ThisContainer'); <- magic key?

  5. Session Scope: Add a third parameter to the Container which accepts a Zend_Session_Namespace. All objects inside this namespace are added to the list of instances where the session key corresponds to the Component Name. Instantiated objects that are scoped "session" are saved inside the session, not just the container.

  6. Currently no loading of the Classes through Zend_Loader::loadClass() - How to handle this?

  7. Known Contexts There are known contexts in which a container already knows he has to have implementations of x,y,z different components. For example a Zend_Front_Controller at least needs request, response, router.

    How should the configuration of known contexts be handled. Maybe a class "MyContext extends Yadif_Container" but in this case the addComponent() functionality has to be extended to allow for a convention over configuration compatible merging of existing with added component definitions.

9. Instantiation with Factory methods

Sometimes you have to create certain objects through a factory method or a singleton creation facility. You can do that with a specific factory key:

$options = array(
    'Foo' => array(
        'class' => 'Foo',
        'factory' => array('FooFactory', 'create'),
    ),
);

The factory key has to hold a valid PHP callback. Then not the constructor, but the factory method is called to create the object. No check is performed if the object is created successfully.

10. Injecting Container Reference or Clones

Using 'ThisContainer' or 'CloneContainer' creates a reference to the current container or clones the container and injects it.