tenkoma / Ray.Di

Guice style dependency injection framework for PHP

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Ray.Di

Dependency Injection framework

Scrutinizer Code Quality codecov Type Coverage Build Status Build Status Total Downloads

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.

Overview

The Ray.Di package provides a dependency injector with the following features:

  • constructor and setter injection

  • automatic injection

  • post-construct initialization

  • raw PHP factory code compiler

  • dependency naming

  • injection point meta data

  • instance factories

  • annotation is optionable

  • AOP integration

Getting Started

Creating Object graph

With dependency injection, objects accept dependencies in their constructors. To construct an object, you first build its dependencies. But to build each dependency, you need its dependencies, and so on. So when you build an object, you really need to build an object graph.

Building object graphs by hand is labour intensive, error prone, and makes testing difficult. Instead, Ray.Di can build the object graph for you. But first, Ray.Di needs to be configured to build the graph exactly as you want it.

To illustrate, we'll start the BillingService class that accepts its dependent interfaces in its constructor: ProcessorInterface and LoggerInterface.

class BillingService
{
    private $processor;
    private $logger
    
    public function __construct(ProcessorInterface $processor, LoggerInterface $logger)
    {
        $this->processor = $processor;
        $this->logger = $logger;
    }
}

Ray.Di uses bindings to map types to their implementations. A module is a collection of bindings specified using fluent, English-like method calls:

class BillingModule extends AbstractModule
{
    protected function configure()
    {
        $this->bind(ProcessorInterface::class)->to(PaypalProcessor::class); 
        $this->bind(LoggerInterface::class)->to(DatabaseLogger::class);
    }
}

The modules are the building blocks of an injector, which is Ray.Di's object-graph builder. First we create the injector, and then we can use that to build the BillingService:

$injector = new Injector(new BillingModule);
$billingService = $injector->getInstance(BillingService::class);

By building the billingService, we've constructed a small object graph using Ray.Di.

Injections

Constructor Injection

Constructor injection combines instantiation with injection. This constructor should accept class dependencies as parameters. Most constructors will then assign the parameters to properties. You do not need @Inject annotation in constructor.

    public function __construct(DbInterface $db)
    {
        $this->db = $db;
    }

Setter Injection

Ray.Di can inject methods that have the @Inject annotation. Dependencies take the form of parameters, which the injector resolves before invoking the method. Injected methods may have any number of parameters, and the method name does not impact injection.

use Ray\Di\Di\Inject;
    /**
     * @Inject
     */
    public function setDb(DbInterface $db)
    {
        $this->db = $db;
    }

Property Injection

Ray.Di does not support property injection.

Assisted Injection

It is also possible to inject dependencies directly in the invoke method parameter(s). When doing this, add the dependency to the end of the arguments and annotate the method with @Assisted with having assisted parameter(s). You need null default for that parameter.

use Ray\Di\Di\Assisted;
    /**
     * @Assisted({"db"})
     */
    public function doSomething($id, DbInterface $db = null)
    {
        $this->db = $db;
    }

You can also provide dependency which depends on other dynamic parameter in method invocation. MethodInvocationProvider provides MethodInvocation object.

class HorizontalScaleDbProvider implements ProviderInterface
{
    /**
     * @var MethodInvocationProvider
     */
    private $invocationProvider;

    public function __construct(MethodInvocationProvider $invocationProvider)
    {
        $this->invocationProvider = $invocationProvider;
    }

    public function get()
    {
        $methodInvocation = $this->invocationProvider->get();
        list($id) = methodInvocation->getArguments()->getArrayCopy();
        
        return new UserDb($id); // $id for database choice.
    }
}

Bindings

The injector's job is to assemble graphs of objects. You request an instance of a given type, and it figures out what to build, resolves dependencies, and wires everything together. To specify how dependencies are resolved, configure your injector with bindings.

Creating Bindings

To create bindings, extend AbstractModule and override its configure method. In the method body, call bind() to specify each binding. These methods are type checked in compile can report errors if you use the wrong types. Once you've created your modules, pass them as arguments to Injector to build an injector.

Use modules to create linked bindings, instance bindings, provider bindings, constructor bindings and untargetted bindings.

class TweetModule extends AbstractModule
{
    protected function configure()
    {
        $this->bind(TweetClient::class);
        $this->bind(TweeterInterface::class)->to(SmsTweeter::class)->in(Scope::SINGLETON);
        $this->bind(UrlShortenerInterface)->toProvider(TinyUrlShortener::class)
        $this->bind('')->annotatedWith(Username::class)->toInstance("koriym")
    }
}

Linked Bindings

Linked bindings map a type-hint to its implementation. This example maps the interface TransactionLogInterface to the implementation DatabaseTransactionLog:

class BillingModule extends AbstractModule
{
    protected function configure()
    {
        $this->bind(TransactionLogInterface::class)->to(DatabaseTransactionLog::class);
    }
}

Provider Bindings

Provider bindings map a type-hint to its provider.

$this->bind(TransactionLogInterface::class)->toProvider(DatabaseTransactionLogProvider::class);

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

namespace Ray\Di;

interface ProviderInterface
{
    public function get();
}

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

use Ray\Di\Di\Inject;
use Ray\Di\ProviderInterface;

class DatabaseTransactionLogProvider implements ProviderInterface
{
    private $connection;

    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::class)->toProvider(DatabaseTransactionLogProvider::class);

Contextual Provider Bindings

You may want to create an object using the context when binding with Provider. For example, you want to inject different connection destinations on the same DB interface. In such a case, we bind it by specifying the context (string) with toProvider ().

$dbConfig = ['user' => $userDsn, 'job'=> $jobDsn, 'log' => $logDsn];
$this->bind()->annotatedWith('db_config')->toInstance(dbConfig);
$this->bind(Connection::class)->annotatedWith('usr_db')->toProvider(DbalProvider::class, 'user');
$this->bind(Connection::class)->annotatedWith('job_db')->toProvider(DbalProvider::class, 'job');
$this->bind(Connection::class)->annotatedWith('log_db')->toProvider(DbalProvider::class, 'log');

Providers are created for each context.

class DbalProvider implements ProviderInterface, SetContextInterface
{
    private $dbConfigs;

    public function setContext($context)
    {
        $this->context = $context;
    }

    /**
     * @Named("db_config")
     */
    public function __construct(array $dbConfigs)
    {
        $this->dbConfigs = $dbConfigs;
    }

    /**
     * {@inheritdoc}
     */
    public function get()
    {
        $config = $this->dbConfigs[$this->context];
        $conn = DriverManager::getConnection($config);

        return $conn;
    }
}

It is the same interface, but you can receive different connections made by Provider.

/**
 * @Named("userDb=user,jobDb=job,logDb=log")
 */
public function __construct(Connection $userDb, Connection $jobDb, Connection $logDb)
{
  //...
}

Injection Point

An InjectionPoint is a class that has information about an injection point. It provides access to metadata via \ReflectionParameter or an annotation in Provider.

For example, the following get() method of Psr3LoggerProvider class creates injectable Loggers. The log category of a Logger depends upon the class of the object into which it is injected.

class Psr3LoggerProvider implements ProviderInterface
{
    /**
     * @var InjectionPoint
     */
    private $ip;

    public function __construct(InjectionPointInterface $ip)
    {
        $this->ip = $ip;
    }

    public function get()
    {
        $logger = new \Monolog\Logger($this->ip->getClass()->getName());
        $logger->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));

        return $logger;
    }
}

InjectionPointInterface provides following methods.

$ip->getClass();      // \ReflectionClass
$ip->getMethod();     // \ReflectionMethod
$ip->getParameter();  // \ReflectionParameter
$ip->getQualifiers(); // (array) $qualifierAnnotations

Instance Bindings

protected function configure()
{
    $this->bind(UserInterface::class)->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');
}

Untargeted Bindings

You may create bindings without specifying a target. This is most useful for concrete classes. An untargetted binding informs the injector about a type, so it may prepare dependencies eagerly. Untargetted bindings have no to clause, like so:

protected function configure()
{
    $this->bind(MyConcreteClass::class);
    $this->bind(AnotherConcreteClass::class)->in(Scope::SINGLETON);
}

note: annotations are not supported for Untargeted Bindings

Binding Annotations

Occasionally you'll want multiple bindings for a same type. For example, you might want both a PayPal credit card processor and a Google Checkout processor. To enable this, bindings support an optional binding annotation. The annotation and type together uniquely identify a binding. This pair is called a key.

Define qualifier annotation first. It needs to be annotated with @Qualifier annotation.

use Ray\Di\Di\Qualifier;

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

To depend on the annotated binding, apply the annotation to the injected parameter:

/**
 * @PayPal
 */
public function __construct(CreditCardProcessorInterface $processor)
{
}

You can specify parameter name with qualifier. Qualifier applied all parameters without it.

/**
 * @PayPal("processor")
 */
public function __construct(CreditCardProcessorInterface $processor)
{
 ....
}

Lastly we create a binding that uses the annotation. This uses the optional annotatedWith clause in the bind() statement:

protected function configure()
{
    $this->bind(CreditCardProcessorInterface::class)
        ->annotatedWith(PayPal::class)
        ->to(PayPalCreditCardProcessor::class);

By default your custom @Qualifier annotations will only help injecting dependencies in constructors on when you annotate you also annotate your methods with @Inject.

Binding Annotations in Setters

In order to make your custom @Qualifier annotations inject dependencies by default in any method the annotation is added, you need to implement the Ray\Di\Di\InjectInterface:

use Ray\Di\Di\InjectInterface;
use Ray\Di\Di\Qualifier;

/**
 * @Annotation
 * @Target("METHOD")
 * @Qualifier
 */
final class PaymentProcessorInject implements InjectInterface
{

    public $optional = true;

    public $type;

    public function isOptional()
    {
        return $this->optional;
    }
}

The interface requires that you implement the isOptional() method. It will be used to determine whether or not the injection should be performed based on whether there is a known binding for it.

Now that you have created your custom injector annotation, you can use it on any method.

/**
 * @PaymentProcessorInject("type=paypal")
 */
public setPaymentProcessor(CreditCardProcessorInterface $processor)
{
 ....
}

Finally, you can bind the interface to an implementation by using your new annotated information:

protected function configure()
{
    $this->bind(CreditCardProcessorInterface::class)
        ->annotatedWith(PaymentProcessorInject::class)
        ->toProvider(PaymentProcessorProvider::class);
}

The provider can now use the information supplied in the qualifier annotation in order to instantiate the most appropriate class.

@Named

The most common use of a Qualifier annotation is tagging arguments in a function with a certain label, the label can be used in the bindings in order to select the right class to be instantiated. For those cases, Ray.Di comes with a built-in binding annotation @Named that takes a string.

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

/**
 * @Named("checkout")
 */
public function __construct(CreditCardProcessorInterface $processor)
{
...

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

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

You need to specify in case of multiple parameters.

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

/**
 * @Named("processor=checkout,subProcessor=backUp")
 */
public function __construct(CreditCardProcessorInterface $processor, CreditCardProcessorInterface $subProcessor)
{
...

Constructor Bindings

When @Inject annotation cannot be applied to the target constructor or setter method because it is a third party class, Or you simply don't like to use annotations. Constructor Binding provide the solution to this problem. By calling your target constructor explicitly, you don't need reflection and its associated pitfalls. But there are limitations of that approach: manually constructed instances do not participate in AOP.

To address this, Ray.Di has toConstructor bindings.

<?php
class WebApi implements WebApiInterface
{
    private $id;
    private $password;
    private $client;
    private $token;

    /**
     * @Named("id=user_id,password=user_password")
     */
    public function __construct(string $id, string $password)
    {
        $this->id = $id;
        $this->password = $password;
    }
    
    /**
     * @Inject
     */
    public function setGuzzle(ClientInterface $client)
    {
        $this->client = $client;
    }

    /**
     * @Inect(optional=true)
     * @Named("token")
     */
    public function setOptionalToken(string $token)
    {
        $this->token = $token;
    }

    /**
     * @PostConstruct
     */
    public function initialize()
    {
    }

All annotation in dependent above can be removed by following toConstructor binding.

<?php
protected function configure()
{
    $this
        ->bind(WebApiInterface::class)
        ->toConstructor(
            WebApi::class,                              // string $class_name
            [
                'id' => 'user_id',                    // array $name
                'passowrd' => 'user_password'
            ],
            (new InjectionPoints)                       // InjectionPoints $setter_injection
                ->addMethod('setGuzzle', 'token')
                ->addOptionalMethod('setOptionalToken'),
            'initialize'                                // string $postCostruct
        );
    $this->bind()->annotated('user_id')->toInstance($_ENV['user_id']);
    $this->bind()->annotated('user_password')->toInstance($_ENV['user_password']);
}

Parameter

class_name

Class Name

name

Parameter name binding.

array [[$parame_name => $binding_name],...] or string "param_name=binding_name&..."

setter_injection

Setter Injection

postCosntruct

Ray.Di will invoke that constructor and setter method to satisfy the binding and invoke in $postCosntruct method after all dependencies are injected.

PDO Example

Here is the example for the native PDO class.

public PDO::__construct ( string $dsn [, string $username [, string $password [, array $options ]]] )
protected function configure()
{       
    $this->bind(\PDO::class)->toConstructor(
        \PDO::class,
        [
            'dsn' => 'pdo_dsn',
            'username' => 'pdo_username',
            'password' => 'pdo_password'
        ]
    )->in(Scope::SINGLETON);
    $this->bind()->annotatedWith('pdo_dsn')->toInstance($dsn);
    $this->bind()->annotatedWith('pdo_username')->toInstance(getenv('db_user'));
    $this->bind()->annotatedWith('pdo_password')->toInstance(getenv('db_password'));
}

Since no argument of PDO has a type, it binds with the Name Binding of the second argument of the toConstructor() method.

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.

use Ray\Di\Scope;

protected function configure()
{
    $this->bind(TransactionLogInterface::class)->to(InMemoryTransactionLog::class)->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 init()
{
    //....
}

Aspect Oriented Programing

To compliment dependency injection, Ray.Di supports method interception. This feature enables you to write code that is executed each time a matching method is invoked. It's suited for cross cutting concerns ("aspects"), such as transactions, security and logging. Because interceptors divide a problem into aspects rather than objects, their use is called Aspect Oriented Programming (AOP).

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(),                           // any class
            $this->matcher->annotatedWith('NotOnWeekends'),  // @NotOnWeekends method
            [WeekendBlocker::class]                          // apply WeekendBlocker interceptor
        );
    }
}

$injector = new Injector(new WeekendModule);
$billing = $injector->getInstance(BillingServiceInterface::class);
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(),
            [TaxCharger::class]
        );
    }
}
use Ray\Di\AbstractModule;

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

Installation

A module can install other modules to configure more bindings.

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

Performance boost

Script injector

ScriptInjector generates raw factory code for better performance and to clarify how the instance is created.

use Ray\Di\ScriptInjector;
use Ray\Compiler\DiCompiler;
use Ray\Compiler\Exception\NotCompiled;

try {
    $injector = new ScriptInjector($tmpDir);
    $instance = $injector->getInstance(ListerInterface::class);
} catch (NotCompiled $e) {
    $compiler = new DiCompiler(new ListerModule, $tmpDir);
    $compiler->compile();
    $instance = $injector->getInstance(ListerInterface::class);
}

Once an instance has been created, You can view the generated factory files in $tmpDir

Cache injector

The injector is serializable. It also boosts the performance.

// save
$injector = new Injector(new ListerModule);
$cachedInjector = serialize($injector);

// load
$injector = unserialize($cachedInjector);
$lister = $injector->getInstance(ListerInterface::class);

CachedInjectorFactory

The CachedInejctorFactory can be used in a hybrid of the two injectors to achieve the best performance in both development and production.

The injector is able to inject singleton objects beyond the request, greatly increasing the speed of testing. Successive PDO connections also do not run out of connection resources in the test.

See CachedInjectorFactory for more information.

Grapher

In Grapher, constructor arguments are passed manually and subsequent injections are done automatically. It is useful to introduce Ray.Di into an existing system (where only root objects have an object generation mechanism).

// ...
$grapher = new Grapher(new Module, __DIR__ . '/tmp');
$instance = $grapher->newInstanceArgs(FooController::class, [$param1, $param2]);

Graphing Ray.Di Applications

When you've written a sophisticated application, Ray.Di rich introspection API can describe the object graph in detail. The object-visual-grapher exposes this data as an easily understandable visualization. It can show the bindings and dependencies from several classes in a complex application in a unified diagram.

fake

See more at https://github.com/koriym/Ray.ObjectGrapher

Frameworks integration

Other Modules

Various modules for Ray.Di are available at https://github.com/Ray-Di.

Installation

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

# Add Ray.Di as a dependency
$ composer require ray/di ^2.0

Testing Ray.Di

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

$ git clone https://github.com/ray-di/Ray.Di.git
$ cd Ray.Di
$ ./vendor/bin/phpunit
$ php demo/run.php

About

Guice style dependency injection framework for PHP

License:MIT License


Languages

Language:PHP 100.0%