Skip to content

craigjbass/Ray.Di

 
 

Repository files navigation

Dependency Injection framework for PHP

Latest Stable Version Build Status Scrutinizer Quality Score Code Coverage

Ray.Di was created in order to get Guice style dependency injection in PHP projects. It tries to mirror Guice's behavior and style. Guice is a Java dependency injection framework developed by Google.

  • Supports some of the JSR-250 object lifecycle annotations (@PostConstruct, @PreDestroy)
  • Provides an AOP Alliance-compliant aspect-oriented programming implementation.
  • Extends Aura.Di.
  • Doctrine.Common annotations.

Not all features of Guice have been implemented.

Getting Stated

Here is a basic example of dependency injection using Ray.Di.

use Ray\Di\AbstractModule;
use Ray\Di\Di\Inject;
use Ray\Di\Injector;

interface FinderInterface
{
}

class Finder implements FinderInterface
{
}

class Lister
{
    public $finder;

    /**
     * @Inject
     */
    public function setFinder(FinderInterface $finder)
    {
        $this->finder = $finder;
    }
}


class Module extends \Ray\Di\AbstractModule
{
    public function configure()
    {
        $this->bind('MovieApp\FinderInterface')->to('MovieApp\Finder');
    }
}
$injector = Injector::create([new Module]);
$lister = $injector->getInstance('MovieApp\Lister');
$works = ($lister->finder instanceof MovieApp\Finder);
echo(($works) ? 'It works!' : 'It DOES NOT work!');

// It works!

This is an example of Linked Bindings. Linked bindings map a type to its implementation.

Provider Bindings

Provider bindings map a type to its provider.

$this->bind('TransactionLogInterface')->toProvider('DatabaseTransactionLogProvider');

The provider class implements Ray's Provider interface, which is a simple, general interface for supplying values:

use Ray\Di\ProviderInterface;

interface ProviderInterface
{
    public function get();
}

Our provider implementation class has dependencies of its own, which it receives via a contructor annotated with @Inject. It implements the Provider interface to define what's returned with complete type safety:

use Ray\Di\Di\Inject;

class DatabaseTransactionLogProvider implements Provider
{
    private $connection;

    /**
     * @Inject
     */
    public function __construct(ConnectionInterface $connection)
    {
        $this->connection = $connection;
    }

    public function get()
    {
        $transactionLog = new DatabaseTransactionLog;
        $transactionLog->setConnection($this->connection);

        return $transactionLog;
    }
}

Finally we bind to the provider using the toProvider() method:

$this->bind('TransactionLogInterface')->toProvider('DatabaseTransactionLogProvider');

Named Binding

Ray comes with a built-in binding annotation @Named that takes a string.

use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;

/**
 *  @Inject
 *  @Named("processor=Checkout")
 */
public RealBillingService(CreditCardProcessor $processor)
{
...

To bind a specific name, pass that string using the annotatedWith() method.

protected function configure()
{
    $this->bind('CreditCardProcessorInterface')->annotatedWith('Checkout')->to('CheckoutCreditCardProcessor');
}

Instance Bindings

protected function configure()
{
    $this->bind('UserIntetrface')->toInstance(new User);
}

You can bind a type to an instance of that type. This is usually only useful for objects that don't have dependencies of their own, such as value objects:

protected function configure()
{
    $this->bind()->annotatedWith("login_id")->toInstance('bear');
}

Constructor Bindings

Occasionally it's necessary to bind a type to an arbitrary constructor. This arises when the @Inject annotation cannot be applied to the target constructor. eg. when it is a third party class.

class TransactionLog
{
    public function __construct($db)
    {
     // ....
protected function configure()
{
    $this->bind('TransactionLog')->toConstructor(['db' => new Database]);
}

Scopes

By default, Ray returns a new instance each time it supplies a value. This behaviour is configurable via scopes. You can also configure scopes with the @Scope annotation.

protected function configure()
{
    $this->bind('TransactionLog')->to('InMemoryTransactionLog')->in(Scope::SINGLETON);
}

Object life cycle

@PostConstruct is used on methods that need to get executed after dependency injection has finalized to perform any extra initialization.

use Ray\Di\Di\PostConstruct;

/**
 * @PostConstruct
 */
public function onInit()
{
    //....
}

@PreDestroy is used on methods that are called after script execution finishes or exit() is called. This method is registered by using register_shutdown_function.

use Ray\Di\Di\PreDestroy;

/**
 * @PreDestroy
 */
public function onShutdown()
{
    //....
}

Automatic Injection

Ray.Di automatically injects all of the following:

  • instances passed to toInstance() in a bind statement
  • provider instances passed to toProvider() in a bind statement

The objects will be injected while the injector itself is being created. If they're needed to satisfy other startup injections, Ray.Di will inject them before they're used.

Aspect Oriented Programing

To mark select methods as weekdays-only, we define an annotation .

/**
 * NotOnWeekends
 *
 * @Annotation
 * @Target("METHOD")
 */
final class NotOnWeekends
{
}

...and apply it to the methods that need to be intercepted:

class BillingService
{
    /**
     * @NotOnWeekends
     */
    chargeOrder(PizzaOrder $order, CreditCard $creditCard)
    {

Next, we define the interceptor by implementing the org.aopalliance.intercept.MethodInterceptor interface. When we need to call through to the underlying method, we do so by calling $invocation->proceed():

use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;

class WeekendBlocker implements MethodInterceptor
{
    public function invoke(MethodInvocation $invocation)
    {
        $today = getdate();
        if ($today['weekday'][0] === 'S') {
            throw new \RuntimeException(
          		$invocation->getMethod()->getName() . " not allowed on weekends!"
            );
        }
        return $invocation->proceed();
    }
}

Finally, we configure everything. In this case we match any class, but only the methods with our @NotOnWeekends annotation:

use Ray\Di\AbstractModule;

class WeekendModule extends AbstractModule
{

    protected function configure()
    {
        $this->bindInterceptor(
            $this->matcher->any(),
            $this->matcher->annotatedWith('NotOnWeekends'),
            [new WeekendBlocker]
        );
    }
}

$injector = Injector::create([new WeekendModule]);
$billing = $injector->getInstance('BillingService');
try {
    echo $billing->chargeOrder();
} catch (\RuntimeException $e) {
    echo $e->getMessage() . "\n";
    exit(1);
}

Putting it all together, (and waiting until Saturday), we see the method is intercepted and our order is rejected:

RuntimeException: chargeOrder not allowed on weekends! in /apps/pizza/WeekendBlocker.php on line 14

Call Stack:
    0.0022     228296   1. {main}() /apps/pizza/main.php:0
    0.0054     317424   2. Ray\Aop\Weaver->chargeOrder() /apps/pizza/main.php:14
    0.0054     317608   3. Ray\Aop\Weaver->__call() /libs/Ray.Aop/src/Weaver.php:14
    0.0055     318384   4. Ray\Aop\ReflectiveMethodInvocation->proceed() /libs/Ray.Aop/src/Weaver.php:68
    0.0056     318784   5. Ray\Aop\Sample\WeekendBlocker->invoke() /libs/Ray.Aop/src/ReflectiveMethodInvocation.php:65

You can bind interceptors in variouas ways as follows.

use Ray\Di\AbstractModule;

class TaxModule extends AbstractModule
{
    protected function configure()
    {
        $this->bindInterceptor(
            $this->matcher->annotatedWith('Tax'),
            $this->matcher->any(),
            [new TaxCharger]
        );
    }
}
use Ray\Di\AbstractModule;

class AopMatcherModule extends AbstractModule
{
    pro
    protected function configure()
    {
        $this->bindInterceptor(
            $this->matcher->any(),                 // In any class and
            $this->matcher->startWith('delete'), // ..the method start with "delete"
            [new Logger]
        );
    }
}

Installation

A module can install other modules to configure more bindings.

  • Earlier bindings have priority even if the same binding is made later.
  • The module can use an existing bindings by passing in $this. The bindings in that module have priority.
protected function configure()
{
    $this->install(new OtherModule);
    $this->install(new CustomiseModule($this);
}

Injection in the module

You can use a built-in injector in the module which uses existing bindings.

protected function configure()
{
    $this->bind('DbInterface')->to('Db');
    $dbLogger = $this->requestInjection('DbLogger');
}

Best practice

Your code should deal directly with the Injector as little as possible. Instead, you want to bootstrap your application by injecting one root object. The class of this object should use injection to obtain references to other objects on which it depends. The classes of those objects should do the same.

Performance

For performance boosts you can use the CacheInjector which performs caching on the injected object, or in order to increase the performance of object creation the DiCompiler.

Caching dependency-injected objects

Storing dependency-injected objects in a cache container has huge performance boosts. CacheInjector also handles object life cycle as well as auto loading of generated aspect weaved objects.

$injector = function()  {
    return Injector::create([new AppModule]);
};
$initialization = function() {
    // initialize per system startup (not per each request)
};
$injector = new CacheInjector($injector, $initialization, 'cache-namespace', new ApcCache);
$app = $injector->getInsntance('ApplicationInterface');
$app->run();

Dependency-injection Compiler

The Di Compiler speeds up object creation/compilation by taking the creation methods and dependency relationships from the injection logs. Excelling in memory usage and speed.

Of cource there is no cost of reading annotations at run time. It doesn't use the injector or the injection settings (modules)

Limitations

  • Because objects are created from the injection log, there is a need to create all objects using the injector.[^1] @PreDestroy is not supported.
$cache = new ApcCache;
$cacheKey = 'context-key';
$tmpDir = '/tmp';
$moduleProvider = function() {
    return new DiaryAopModule;
};
$injector = DiCompiler::create($moduleProvider, $cache, $cacheKey, $tmpDir);
$injector->getInstance('Ray\Di\DiaryInterface');

Pro Tip

You can run compile() before deployment and the first usage of the class. Then at runtime there is no compile cost.

$injector = DiCompiler::create($moduleProvider, $cache, $cacheKey, $tmpDir);
$injector->compile('Koriym\RayApp\Model\Author');
$injector->compile('Koriym\RayApp\Model\Diary');
...

Cacheable class example

use Ray\Di\Di\Inject;
use Ray\Di\Di\PostConstruct;

class UserRepository
{
    private $dependency;

    /**
     * @Inject
     */
    public function __construct(DependencyInterface $dependency)
    {
        // per system startup
        $this->dependency = $dependency;
    }

    /**
     * @PostConstruct
     */
    public function init()
    {
        // per each request
        //
        // In this @PostConstruct method, You can expect
        // - All injection is completed.
        // - This function is called regardless object cache status unlike __construct or __wakeup.
        // - You can set unserializable item to property such as closure or \PDO object.
    }

    public function getUserData($Id)
    {
        // The request is stateless.
    }
}

Annotation Caching

If working with large legacy codebases it might not be feasible to cache entire class instances as the CacheInjector does. You can still achieve speed improvements by caching the annotations of each class if you pass an object implementing Doctrine\Common\Cache as the second argument to Injector::create

Requirements

  • PHP 5.4+

Documentation

Available at Google Code.

http://code.google.com/p/rayphp/wiki/Motivation?tm=6

Installation

The recommended way to install Ray.Di is through Composer.

# Install Composer
$ curl -sS https://getcomposer.org/installer | php

# Add Ray.Di as a dependency
$ php composer.phar require ray/di:*

Testing Ray.Di

Here's how to install Ray.Di from source and run the unit tests and samples.

$ git clone git://github.com/koriym/Ray.Di.git
$ cd Ray.Di
$ composer install
$ phpunit
$ php doc/sample/00-newsletter.php
$ php doc/sample/01-db/main.php
$ cd doc/zf2-di-tests-clone/
$ php runall.php

About

Guice style dependency injection framework for PHP

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • PHP 100.0%