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.
Here is a basic example of dependency injection using Ray.Di.
namespace MovieApp;
use Ray\Di\AbstractModule;
use Ray\Di\Di\Inject;
use Ray\Di\Injector;
use MovieApp\FinderInterface;
use MovieApp\Finder;
interface FinderInterface
{
}
interface ListerInterface
{
}
class Finder implements FinderInterface
{
}
class Lister implements ListerInterface
{
public $finder;
/**
* @Inject
*/
public function setFinder(FinderInterface $finder)
{
$this->finder = $finder;
}
}
class ListerModule extends AbstractModule
{
public function configure()
{
$this->bind(FinderInterface::class)->to(Finder::class);
$this->bind(ListerInterface::class)->to(Lister::class);
}
}
$injector = new Injector(new ListerModule);
$lister = $injector->getInstance(ListerInterface::class);
$works = ($lister->finder instanceof Finder::class);
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, it can also be chained.
Provider bindings map a type 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 annotated with @Inject
.
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;
/**
* @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::class)->toProvider(DatabaseTransactionLogProvider::class);
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 __construct(CreditCardProcessorInterface $processor, TransactionLogInterface $transactionLog){
{
}
You can specify parameter name with qualifier. Qualifier applied all parameters without it.
/**
* @PayPal("processor")
*/
public __construct(CreditCardProcessorInterface $processor, TransactionLogInterface $transactionLog){
{
....
}
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
.
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.
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 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 __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 parameter.
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
/**
* @Inject
* @Named("processor=checkout,subProcessor=backUp")
*/
public __construct(CreditCardProcessorInterface $processor, CreditCardProcessorInterface $subProcessor)
{
...
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');
}
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 is not supported Untargeted Bindings
Occasionally it's necessary to bind a type to an arbitrary constructor. This comes up when the @Inject annotation cannot be applied to the target constructor: either because it is a third party class, or because multiple constructors that participate in dependency injection.
Provider 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 Car
{
public function __construct(EngineInterface $engine, $carName)
{
// ...
<?php
protected function configure()
{
$this->bind(EngineInterface::class)->annotatedWith('na')->to(NaturalAspirationEngine::class);
$this->bind()->annotatedWith('car_name')->toInstance('Eunos Roadster');
$this
->bind(CarInterface::class)
->toConstructor(
Car::class,
'engine=na,carName=car_name' // varName=BindName,...
);
}
In this example, the Car
have a constructor which name bound with engine=na,carName=car_name
. That constructor does not need an @Inject annotation. Ray.Di will invoke that constructor to satisfy the binding.
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);
}
@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()
{
//....
}
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;
}
}
Obtains the qualifiers
$annotations = $this->ip->getQualifiers();
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.
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]
);
}
}
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);
}
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 = $compiler->getInstance(ListerInterface::class);
}
Once an instance has been created, You can view the generated factory files in $tmpDir
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);
### method 1: Cache injector
- lorenzo/piping-bag for CakePHP3
- BEAR.Sunday
Various modules for Ray.Di
are available at https://github.com/Ray-Di.
- PHP 5.4+
- hhvm
The recommended way to install Ray.Di is through Composer.
# Add Ray.Di as a dependency
$ composer require ray/di ~2.0
Here's how to install Ray.Di from source and run the unit tests and demos.
$ composer create-project ray/di:~2.0 Ray.Di
$ cd Ray.Di
$ phpunit
$ php docs/demo/run.php