This page collects all manual pages in one place.
Installation
The recommended way to install Ray.Di is through Composer.
composer require ray/di ^2.0
The GitHub repository is at ray-di/Ray.Di
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-php8/run.php
Overview
Ray.Di is a dependency injection (DI) framework for PHP. It automatically resolves object dependencies and enables flexible object graph construction according to the context.
Core Features
Dependency Resolution at Compile Time
- Resolves dependencies by describing overall rules rather than individual object assembly (autowiring)
- Detects dependency issues before execution
- Minimizes runtime overhead through code generation
Flexible Object Graph Construction
- Enables various contexts through the combination of independent modules
- Allows dependency resolution according to the injected object; for example, changing dependencies based on the target method’s attributes or the object’s state (CDI: Contexts and Dependency Injection)
- Injects different implementations of the same interface using
Qualifier
- Supports injection of lazily instantiated objects
Explicit Dependency Description
- Describes dependency generation using raw PHP code
- Utilizes attributes for self-documented dependency definitions
- Separates cross-cutting concerns through integration with AOP
Stability and Reliability
Since the release of version 2.0 in 2015, Ray.Di has expanded its features along with the evolution of PHP while maintaining backward compatibility by following semantic versioning.
Google Guice and Ray.Di
Ray.Di is a PHP DI framework inspired by Google Guice. Based on the proven API design of Google Guice, it aims for PHP-like evolution. Most of the documents on this site are also quoted from Google Guice.
Using dependency injection offers many benefits, but doing it manually requires writing a lot of boilerplate code. Ray.Di is a framework that allows you to use dependency injection without writing such cumbersome code. For more details, please see the Motivation page.
In short, Ray.Di eliminates the need to use factories or new
in your PHP code. While you may still need to write factories, your code does not directly depend on them. Your code becomes easier to modify, unit test, and reuse in other contexts.
Motivation
Wiring everything together is a tedious part of application development. There are several approaches to connect data, service, and presentation classes to one another. To contrast these approaches, we’ll write the billing code for a pizza ordering website:
interface BillingServiceInterface
{
/**
* Attempts to charge the order to the credit card. Both successful and
* failed transactions will be recorded.
*
* @return Receipt a receipt of the transaction. If the charge was successful,
* the receipt will be successful. Otherwise, the receipt will contain a
* decline note describing why the charge failed.
*/
public function chargeOrder(PizzaOrder order, CreditCard creditCard): Receipt;
}
Along with the implementation, we’ll write unit tests for our code. In the tests
we need a FakeCreditCardProcessor
to avoid charging a real credit card!
Direct constructor calls
Here’s what the code looks like when we just new
up the credit card processor
and transaction logger:
class RealBillingService implements BillingServiceInterface
{
public function chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
{
$processor = new PaypalCreditCardProcessor();
$transactionLog = new DatabaseTransactionLog();
try {
$result = $processor->charge($creditCard, $order->getAmount());
$transactionLog->logChargeResult($result);
return $result->wasSuccessful()
? Receipt::forSuccessfulCharge($order->getAmount())
: Receipt::forDeclinedCharge($result->getDeclineMessage());
} catch (UnreachableException $e) {
$transactionLog->logConnectException($e);
return ReceiptforSystemFailure($e->getMessage());
}
}
}
This code poses problems for modularity and testability. The direct, compile-time dependency on the real credit card processor means that testing the code will charge a credit card! It’s also awkward to test what happens when the charge is declined or when the service is unavailable.
Factories
A factory class decouples the client and implementing class. A simple factory uses static methods to get and set mock implementations for interfaces. A factory is implemented with some boilerplate code:
class CreditCardProcessorFactory
{
private static CreditCardProcessor $instance;
public static setInstance(CreditCardProcessor $processor): void
{
self::$instance = $processor;
}
public static function getInstance(): CreditCardProcessor
{
if (self::$instance == null) {
return new SquareCreditCardProcessor();
}
return self::$instance;
}
}
In our client code, we just replace the new
calls with factory lookups:
class RealBillingService implements BillingServiceInterface
{
public function chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
{
$processor = CreditCardProcessorFactory::getInstance();
$transactionLog = TransactionLogFactory::getInstance();
try {
$result = $processor->charge($creditCard, $order->getAmount());
$transactionLog->logChargeResult($result);
return $result->wasSuccessful()
? Receipt::forSuccessfulCharge($order->getAmount())
: Receipt::forDeclinedCharge($result->getDeclineMessage());
} catch (UnreachableException $e) {
$transactionLog->logConnectException($e);
return Receipt::forSystemFailure($e->getMessage());
}
}
}
The factory makes it possible to write a proper unit test:
class RealBillingServiceTest extends TestCase
{
private PizzaOrder $order;
private CreditCard $creditCard;
private InMemoryTransactionLog $transactionLog;
private FakeCreditCardProcessor $processor;
public function setUp(): void
{
$this->order = new PizzaOrder(100);
$this->creditCard = new CreditCard('1234', 11, 2010);
$this->processor = new FakeCreditCardProcessor();
TransactionLogFactory::setInstance($transactionLog);
CreditCardProcessorFactory::setInstance($this->processor);
}
public function tearDown(): void
{
TransactionLogFactory::setInstance(null);
CreditCardProcessorFactory::setInstance(null);
}
public function testSuccessfulCharge()
{
$billingService = new RealBillingService();
$receipt = $billingService->chargeOrder($this->order, $this->creditCard);
$this->assertTrue($receipt->hasSuccessfulCharge());
$this->assertEquals(100, $receipt->getAmountOfCharge());
$this->assertEquals($creditCard, $processor->getCardOfOnlyCharge());
$this->assertEquals(100, $processor->getAmountOfOnlyCharge());
$this->assertTrue($this->transactionLog->wasSuccessLogged());
}
}
This code is clumsy. A global variable holds the mock implementation, so we need
to be careful about setting it up and tearing it down. Should the tearDown
fail, the global variable continues to point at our test instance. This could
cause problems for other tests. It also prevents us from running multiple tests
in parallel.
But the biggest problem is that the dependencies are hidden in the code. If we
add a dependency on a CreditCardFraudTracker
, we have to re-run the tests to
find out which ones will break. Should we forget to initialize a factory for a
production service, we don’t find out until a charge is attempted. As the
application grows, babysitting factories becomes a growing drain on
productivity.
Quality problems will be caught by QA or acceptance tests. That may be sufficient, but we can certainly do better.
Dependency Injection
Like the factory, dependency injection is just a design pattern. The core
principle is to separate behaviour from dependency resolution. In our example,
the RealBillingService
is not responsible for looking up the TransactionLog
and CreditCardProcessor
. Instead, they’re passed in as constructor parameters:
class RealBillingService implements BillingServiceInterface
{
public function __construct(
private readonly CreditCardProcessor $processor,
private readonly TransactionLog $transactionLog
) {}
public chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
{
try {
$result = $this->processor->charge($creditCard, $order->getAmount());
$this->transactionLog->logChargeResult(result);
return $result->wasSuccessful()
? Receipt::forSuccessfulCharge($order->getAmount())
: Receipt::forDeclinedCharge($result->getDeclineMessage());
} catch (UnreachableException $e) {
$this->transactionLog->logConnectException($e);
return Receipt::forSystemFailure($e->getMessage());
}
}
}
We don’t need any factories, and we can simplify the testcase by removing the
setUp
and tearDown
boilerplate:
class RealBillingServiceTest extends TestCase
{
private PizzaOrder $order;
private CreditCard $creditCard;
private InMemoryTransactionLog $transactionLog;
private FakeCreditCardProcessor $processor;
public function setUp(): void
{
$this->order = new PizzaOrder(100);
$this->$creditCard = new CreditCard("1234", 11, 2010);
$this->$transactionLog = new InMemoryTransactionLog();
$this->$processor = new FakeCreditCardProcessor();
}
public function testSuccessfulCharge()
{
$billingService= new RealBillingService($this->processor, $this->transactionLog);
$receipt = $billingService->chargeOrder($this->order, $this->creditCard);
$this->assertTrue($receipt.hasSuccessfulCharge());
$this->assertSame(100, $receipt->getAmountOfCharge());
$this->assertSame($this->creditCard, $this->processor->getCardOfOnlyCharge());
$this->assertSame(100, $this->processor->getAmountOfOnlyCharge());
$this->assertTrue($this->transactionLog->wasSuccessLogged());
}
}
Now, whenever we add or remove dependencies, the compiler will remind us what tests need to be fixed. The dependency is exposed in the API signature.
Unfortunately, now the clients of BillingService
need to lookup its
dependencies. We can fix some of these by applying the pattern again! Classes
that depend on it can accept a BillingService
in their constructor. For
top-level classes, it’s useful to have a framework. Otherwise you’ll need to
construct dependencies recursively when you need to use a service:
<?php
$processor = new PaypalCreditCardProcessor();
$transactionLog = new DatabaseTransactionLog();
$billingService = new RealBillingService($processor, $transactionLog);
// ...
Dependency Injection with Ray.Di
The dependency injection pattern leads to code that’s modular and testable, and
Ray.Di makes it easy to write. To use Ray.Di in our billing example, we first need
to tell it how to map our interfaces to their implementations. This
configuration is done in a Ray.Di module, which is any PHP class that implements
the Module
interface:
class BillingModule extends AbstractModule
{
protected function configure(): void
{
$this->bind(TransactionLog::class)->to(DatabaseTransactionLog::class);
$this->bind(CreditCardProcessor::class)->to(PaypalCreditCardProcessor::class);
$this->bind(BillingServiceInterface::class)->to(RealBillingService::class);
}
}
Ray.Di will inspect the constructor, and lookup values for each parameter.
class RealBillingService implements BillingServiceInterface
{
public function __construct(
private readonly CreditCardProcessor $processor,
private readonly TransactionLog $transactionLog
) {}
public function chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
{
try {
$result = $this->processor->charge($creditCard, $order->getAmount());
$this->transactionLog->logChargeResult($result);
return $result->wasSuccessful()
? Receipt::forSuccessfulCharge($order->getAmount())
: Receipt::forDeclinedCharge($result->getDeclineMessage());
} catch (UnreachableException $e) {
$this->transactionLog->logConnectException($e);
return Receipt::forSystemFailure($e->getMessage());
}
}
}
Finally, we can put it all together. The Injector
can be used to get an
instance of any of the bound classes.
<?php
$injector = new Injector(new BillingModule());
$billingService = $injector->getInstance(BillingServiceInterface::class);
//...
Getting started explains how this all works.
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:
$this->bind(MyConcreteClass::class);
$this->bind(AnotherConcreteClass::class)->in(Scope::SINGLETON);
Note: Untargeted binding does not currently support the annotatedWith()
clause.
Constructor Bindings
When #[Inject]
attribute 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.
$this->bind($interfaceName)
->toConstructor(
$className, // Class name
$name, // Qualifier
$injectionPoint, // Setter injection
$postConstruct // Initialize method
);
(new InjectionPoints)
->addMethod('setGuzzle') // Setter injection method name
->addOptionalMethod('setOptionalToken'); // Optional setter injection method name
Parameter
class_name
Class name
name
Parameter name binding.
If you want to add an identifier to the argument, specify an array with the variable name as the key and the value as the name of the identifier.
[
[$param_name1 => $binding_name1],
...
]
The following string formats are also supported
'param_name1=binding_name1&...'
setter_injection
Specify the method name ($methodName) and qualifier ($named) of the setter injector in the InjectionPoints
object.
(new InjectionPoints)
->addMethod($methodName1)
->addMethod($methodName2, $named)
->addOptionalMethod($methodName, $named);
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 ]]] )
$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.
In the above example, the variable username
is given the identifier pdo_username
, and toInstance
binds the value of the environment variable.
Built-in Bindings
More bindings that you can use
NOTE: It’s very rare that you’d need to use those built-in bindings.
The Injector
In framework code, sometimes you don’t know the type you need until runtime. In this rare case you should inject the injector. Code that injects the injector does not self-document its dependencies, so this approach should be done sparingly.
Providers
For every type Ray.Di knows about, it can also inject a Provider of that type. Injecting Providers describes this in detail.
Multi-bundling
Multi bindinga allows multiple implementations to be injected for a type. It is explained in detail in MultiBindings.
Multibindings
Overview of Multibinder, MapBinder
Multibinder is intended for plugin-type architectures.
Multibinding
Using Multibinder
to host plugins.
Multibinder
Multibindings make it easy to support plugins in your application. Made popular by IDEs and browsers, this pattern exposes APIs for extending the behaviour of an application.
Neither the plugin consumer nor the plugin author need write much setup code for
extensible applications with Ray.Di. Simply define an interface, bind
implementations, and inject sets of implementations! Any module can create a new
Multibinder to contribute bindings to a set of implementations. To illustrate,
we’ll use plugins to summarize ugly URIs like http://bit.ly/1mzgW1
into
something readable on Twitter.
First, we define an interface that plugin authors can implement. This is usually an interface that lends itself to several implementations. For this example, we would write a different implementation for each website that we could summarize.
interface UriSummarizerInterface
{
/**
* Returns a short summary of the URI, or null if this summarizer doesn't
* know how to summarize the URI.
*/
public function summarize(Uri $uri): string;
}
Next, we’ll get our plugin authors to implement the interface. Here’s an implementation that shortens Flickr photo URLs:
class FlickrPhotoSummarizer implements UriSummarizer
{
public function __construct(
private readonly PhotoPaternMatcherInterface $matcher
) {}
public function summarize(Uri $uri): ?string
{
$match = $this->matcher->match($uri);
if (! $match) {
return null;
}
$id = $this->matcher->group(1);
$photo = Photo::loockup($id);
return $photo->getTitle();
}
}
}
The plugin author registers their implementation using a multibinder. Some plugins may bind multiple implementations, or implementations of several extension-point interfaces.
class FlickrPluginModule extends AbstractModule
{
public function configure(): void
{
$uriBinder = Multibinder::newInstance($this, UriSummarizerInterface::class);
$uriBinder->addBinding()->to(FlickrPhotoSummarizer::class);
// ...bind plugin dependencies, such as our Flickr API key
}
}
Now we can consume the services exposed by our plugins. In this case, we’re summarizing tweets:
class TweetPrettifier
{
/**
* @param Map<UriSummarizerInterface> $summarizers
*/
public function __construct(
#[Set(UriSummarizer::class)] private readonyl Map $summarizers;
private readonly EmoticonImagifier $emoticonImagifier;
) {}
public function prettifyTweet(String tweetMessage): Html
{
// split out the URIs and call prettifyUri() for each
}
public function prettifyUri(Uri $uri): string
{
// loop through the implementations, looking for one that supports this URI
foreach ($this->summarizer as summarizer) {
$summary = $summarizer->summarize($uri);
if ($summary != null) {
return $summary;
}
}
// no summarizer found, just return the URI itself
return $uri->toString();
}
}
Note: The method Multibinder::newInstance($module, $type)
can be confusing.
This operation creates a new binder, but doesn’t override any existing bindings.
A binder created this way contributes to the existing Set of implementations for
that type. It would create a new set only if one is not already bound.
Finally we must register the plugins themselves. The simplest mechanism to do so is to list them programatically:
class PrettyTweets
{
public function __invoke(): void
{
$injector = new Injector(
new GoogleMapsPluginModule(),
new BitlyPluginModule(),
new FlickrPluginModule()
// ...
);
$injector->getInstance(Frontend::class)->start();
}
}
(new PrettyTweets)();
MapBinder
You can name the classes you add in the multibinder.
class FlickrPluginModule extends AbstractModule
{
public function configure(): void
{
$uriBinder = Multibinder::newInstance($this, UriSummarizerInterface::class);
$uriBinder->addBinding('flickr')->to(FlickrPhotoSummarizer::class);
// ...bind plugin dependencies, such as our Flickr API key
}
}
In the application, you can retrieve a Map
injected by specifying attributes such as #[Set(UriSummarizer::class)]
with the name as it was when specified by the binding.
class TweetPrettifier
{
/**
* @param Map<UriSummarizerInterface> $summarizers
*/
public function __construct(
#[Set(UriSummarizer::class)] private readonly Map $summarizers;
) {}
public doSomething(): void
{
$filickerSummarizer = $this->summarizers['flicker'];
assert($filickerSummarizer instanceof FlickrPhotoSummarizer);
}
}
Set binding
The setBinding()
method overrides any previous binding.
$UriBinder = Multibinder::newInstance($this, UriSummarizerInterface::class);
$UriBinder->setBinding('flickr')->(FlickrPhotoSummarizer::class);
Map
Map
objects are treated as generics in static analysis. If the injected interface is T, it is written as Map<T>
.
/** @param Map<UriSummarizerInterface> $summarizers **/
Annotation
Since it is not possible to annotate the argument, annotate the property to be assigned with the same name and annotate the property with @Set
.
class TweetPrettifier
{
/** @Set(UriSummarizer::class) */
private $summarizers;
/**
* @param Map<UriSummarizerInterface> $summarizers
*/
public function __construct(Map $summarizers) {
$this->summarizers = $summarizers;
}
}
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.
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
class DbalProvider implements ProviderInterface, SetContextInterface
{
private $dbConfigs;
public function setContext($context)
{
$this->context = $context;
}
public function __construct(#[Named('db_config') 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
.
public function __construct(
#[Named('user')] private readonly Connection $userDb,
#[Named('job')] private readonly Connection $jobDb,
#[Named('log') private readonly Connection $logDb)
) {}
Null Object Binding
A Null Object is an object that implements an interface but whose methods do nothing.
When bound with toNull()
, the code of the Null Object is generated from the interface and bound to the generated instance.
This is useful for testing and AOP.
$this->bind(CreditCardProcessorInterface::class)->toNull();
Injections
How Ray.Di initializes your objects
The dependency injection pattern separates behaviour from dependency resolution. Rather than looking up dependencies directly or from factories, the pattern recommends that dependencies are passed in. The process of setting dependencies into an object is called injection.
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]
attribute in constructor.
public function __construct(DbInterface $db)
{
$this->db = $db;
}
Setter Injection
Ray.Di can inject by methods that have the #[Inject]
attribute. 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
Also called method-call injection action injection, or Invocation 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 add #[Assisted]
to the parameter(s). You need null
default for that parameter.
Note that this Assisted Injection is different from the one in Google Guice.
use Ray\Di\Di\Assisted;
public function doSomething(string $id, #[Assisted] 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
{
public function __construct(
private readonly MethodInvocationProvider $invocationProvider
){}
public function get()
{
$methodInvocation = $this->invocationProvider->get();
[$id] = $methodInvocation->getArguments()->getArrayCopy();
return UserDb::withId($id); // $id for database choice.
}
}
This injection done by AOP is powerful and useful for injecting objects that are only determined at method execution time, as described above. However, this injection is outside the scope of the original IOC and should only be used when really necessary.
Optional Injections
Occasionally it’s convenient to use a dependency when it exists and to fall back
to a default when it doesn’t. Method and field injections may be optional, which
causes Ray.Di to silently ignore them when the dependencies aren’t available. To
use optional injection, apply the #[Inject(optional: true)
attribute:
class PayPalCreditCardProcessor implements CreditCardProcessorInterface
{
private const SANDBOX_API_KEY = "development-use-only";
private string $apiKey = self::SANDBOX_API_KEY;
#[Inject(optional: true)]
public setApiKey(#[Named('paypal-apikey') string $apiKey): void
{
$this->apiKey = $apiKey;
}
}
Injecting Providers
With normal dependency injection, each type gets exactly one instance of each
of its dependent types. The RealBillingService
gets one CreditCardProcessor
and one TransactionLog
. Sometimes you want more than one instance of your
dependent types. When this flexibility is necessary, Ray.Di binds a provider.
Providers produce a value when the get()
method is invoked:
/**
* @template T
*/
interface ProviderInterface
{
/**
* @return T
*/
public function get();
}
The type provided by the provider is specified by the #[Set]
attribute.
class RealBillingService implements BillingServiceInterface
{
/**
* @param ProviderInterface<TransactionLogInterface> $processorProvider
* @param ProviderInterface<CreditCardProcessorInterface> $transactionLogProvider
*/
public __construct(
#[Set(TransactionLogInterface::class)] private ProviderInterface $processorProvider,
#[Set(CreditCardProcessorInterface::class)] private ProviderInterface $transactionLogProvider
) {}
public chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
{
$transactionLog = $this->transactionLogProvider->get();
$processor = $this->processorProvider->get();
/* use the processor and transaction log here */
}
}
To support generics in static analysis, you need to set @param
in phpdoc to ProviderInterface<TransactionLogInterface>
or ProviderInterface<Cre ditCardProcessorInterface>
and so on. The type of the instance obtained by the get()
method is specified and checked by static analysis.
Providers for multiple instances
Use providers when you need multiple instances of the same type. Suppose your application saves a summary entry and a details when a pizza charge fails. With providers, you can get a new entry whenever you need one:
class LogFileTransactionLog implements TransactionLogInterface
{
public function __construct(
#[Set(TransactionLogInterface::class)] private readonly ProviderInterface $logFileProvider
) {}
public logChargeResult(ChargeResult $result): void {
$summaryEntry = $this->logFileProvider->get();
$summaryEntry->setText("Charge " + (result.wasSuccessful() ? "success" : "failure"));
$summaryEntry->save();
if (! $result->wasSuccessful()) {
$detailEntry = $this->logFileProvider->get();
$detailEntry->setText("Failure result: " + result);
$detailEntry->save();
}
}
}
Providers for lazy loading
If you’ve got a dependency on a type that is particularly expensive to produce, you can use providers to defer that work. This is especially useful when you don’t always need the dependency:
class LogFileTransactionLog implements TransactionLogInterface
{
public function __construct(
(#[Set(Connection::class)] private ProviderInterface $connectionProvider
) {}
public function logChargeResult(ChargeResult $result) {
/* only write failed charges to the database */
if (! $result->wasSuccessful()) {
$connection = $connectionProvider->get();
}
}
Providers for Mixing Scopes
Directly injecting an object with a narrower scope usually causes unintended
behavior in your application. In the example below, suppose you have a singleton
ConsoleTransactionLog
that depends on the request-scoped current user. If you
were to inject the user directly into the ConsoleTransactionLog
constructor,
the user would only be evaluated once for the lifetime of the application. This
behavior isn’t correct because the user changes from request to request.
Instead, you should use a Provider. Since Providers produce values on-demand,
they enable you to mix scopes safely:
class ConsoleTransactionLog implements TransactionLogInterface
{
public function __construct(
#[Set(User::class)] private readonly ProviderInterface $userProvider
) {}
public function logConnectException(UnreachableException $e): void
{
$user = $this->userProvider->get();
echo "Connection failed for " . $user . ": " . $e->getMessage();
}
}
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
Intercepting methods with Ray.Di
To complement 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).
Matcher is a simple interface that either accepts or rejects a value. For Ray.Di AOP, you need two matchers: one that defines which classes participate, and another for the methods of those classes.
MethodInterceptors are executed whenever a matching method is invoked. They have the opportunity to inspect the call: the method, its arguments, and the receiving instance. They can perform their cross-cutting logic and then delegate to the underlying method. Finally, they may inspect the return value or exception and return. Since interceptors may be applied to many methods and will receive many calls, their implementation should be efficient and unintrusive.
Example: Forbidding method calls on weekends
To illustrate how method interceptors work with Ray.Di, we’ll forbid calls to our pizza billing system on weekends. The delivery guys only work Monday thru Friday so we’ll prevent pizza from being ordered when it can’t be delivered! This example is structurally similar to use of AOP for authorization.
To mark select methods as weekdays-only, we define an attribute:
#[Attribute(Attribute::TARGET_METHOD)]
final class NotOnWeekends
{
}
…and apply it to the methods that need to be intercepted:
class BillingService implements BillingServiceInterface
{
#[NotOnWeekends]
public function chargeOrder(PizzaOrder $order, CreditCard $creditCard)
{
Next, we define the interceptor by implementing the 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]
attribute:
use Ray\Di\AbstractModule;
class WeekendModule extends AbstractModule
{
protected function configure()
{
$this->bind(BillingServiceInterface::class)->to(BillingService::class);
$this->bindInterceptor(
$this->matcher->any(), // any class
$this->matcher->annotatedWith('NotOnWeekends'), // #[NotOnWeekends] attributed 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
Disable interceptors
To disable the interceptor, bind NullInterceptor.
use Ray\Aop\NullInterceptor;
protected function configure()
{
// ...
$this->bind(LoggerInterface::class)->to(NullInterceptor::class);
}
Limitations
Behind the scenes, method interception is implemented by generating bytecode at runtime. Ray.Di dynamically creates a subclass that applies interceptors by overriding methods.
This approach imposes limits on what classes and methods can be intercepted:
- Classes must be non-final
- Methods must be public
- Methods must be non-final
- Instances must be created by Ray.Di.
AOP Alliance
The method interceptor API implemented by Ray.Di is mostly same as a public specification called AOP Alliance in Java.
This page collects all manual pages in one place.
Installation
The recommended way to install Ray.Di is through Composer.
composer require ray/di ^2.0
The GitHub repository is at ray-di/Ray.Di
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-php8/run.php
Overview
Ray.Di is a dependency injection (DI) framework for PHP. It automatically resolves object dependencies and enables flexible object graph construction according to the context.
Core Features
Dependency Resolution at Compile Time
- Resolves dependencies by describing overall rules rather than individual object assembly (autowiring)
- Detects dependency issues before execution
- Minimizes runtime overhead through code generation
Flexible Object Graph Construction
- Enables various contexts through the combination of independent modules
- Allows dependency resolution according to the injected object; for example, changing dependencies based on the target method’s attributes or the object’s state (CDI: Contexts and Dependency Injection)
- Injects different implementations of the same interface using
Qualifier
- Supports injection of lazily instantiated objects
Explicit Dependency Description
- Describes dependency generation using raw PHP code
- Utilizes attributes for self-documented dependency definitions
- Separates cross-cutting concerns through integration with AOP
Stability and Reliability
Since the release of version 2.0 in 2015, Ray.Di has expanded its features along with the evolution of PHP while maintaining backward compatibility by following semantic versioning.
Google Guice and Ray.Di
Ray.Di is a PHP DI framework inspired by Google Guice. Based on the proven API design of Google Guice, it aims for PHP-like evolution. Most of the documents on this site are also quoted from Google Guice.
Using dependency injection offers many benefits, but doing it manually requires writing a lot of boilerplate code. Ray.Di is a framework that allows you to use dependency injection without writing such cumbersome code. For more details, please see the Motivation page.
In short, Ray.Di eliminates the need to use factories or new
in your PHP code. While you may still need to write factories, your code does not directly depend on them. Your code becomes easier to modify, unit test, and reuse in other contexts.
Motivation
Wiring everything together is a tedious part of application development. There are several approaches to connect data, service, and presentation classes to one another. To contrast these approaches, we’ll write the billing code for a pizza ordering website:
interface BillingServiceInterface
{
/**
* Attempts to charge the order to the credit card. Both successful and
* failed transactions will be recorded.
*
* @return Receipt a receipt of the transaction. If the charge was successful,
* the receipt will be successful. Otherwise, the receipt will contain a
* decline note describing why the charge failed.
*/
public function chargeOrder(PizzaOrder order, CreditCard creditCard): Receipt;
}
Along with the implementation, we’ll write unit tests for our code. In the tests
we need a FakeCreditCardProcessor
to avoid charging a real credit card!
Direct constructor calls
Here’s what the code looks like when we just new
up the credit card processor
and transaction logger:
class RealBillingService implements BillingServiceInterface
{
public function chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
{
$processor = new PaypalCreditCardProcessor();
$transactionLog = new DatabaseTransactionLog();
try {
$result = $processor->charge($creditCard, $order->getAmount());
$transactionLog->logChargeResult($result);
return $result->wasSuccessful()
? Receipt::forSuccessfulCharge($order->getAmount())
: Receipt::forDeclinedCharge($result->getDeclineMessage());
} catch (UnreachableException $e) {
$transactionLog->logConnectException($e);
return ReceiptforSystemFailure($e->getMessage());
}
}
}
This code poses problems for modularity and testability. The direct, compile-time dependency on the real credit card processor means that testing the code will charge a credit card! It’s also awkward to test what happens when the charge is declined or when the service is unavailable.
Factories
A factory class decouples the client and implementing class. A simple factory uses static methods to get and set mock implementations for interfaces. A factory is implemented with some boilerplate code:
class CreditCardProcessorFactory
{
private static CreditCardProcessor $instance;
public static setInstance(CreditCardProcessor $processor): void
{
self::$instance = $processor;
}
public static function getInstance(): CreditCardProcessor
{
if (self::$instance == null) {
return new SquareCreditCardProcessor();
}
return self::$instance;
}
}
In our client code, we just replace the new
calls with factory lookups:
class RealBillingService implements BillingServiceInterface
{
public function chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
{
$processor = CreditCardProcessorFactory::getInstance();
$transactionLog = TransactionLogFactory::getInstance();
try {
$result = $processor->charge($creditCard, $order->getAmount());
$transactionLog->logChargeResult($result);
return $result->wasSuccessful()
? Receipt::forSuccessfulCharge($order->getAmount())
: Receipt::forDeclinedCharge($result->getDeclineMessage());
} catch (UnreachableException $e) {
$transactionLog->logConnectException($e);
return Receipt::forSystemFailure($e->getMessage());
}
}
}
The factory makes it possible to write a proper unit test:
class RealBillingServiceTest extends TestCase
{
private PizzaOrder $order;
private CreditCard $creditCard;
private InMemoryTransactionLog $transactionLog;
private FakeCreditCardProcessor $processor;
public function setUp(): void
{
$this->order = new PizzaOrder(100);
$this->creditCard = new CreditCard('1234', 11, 2010);
$this->processor = new FakeCreditCardProcessor();
TransactionLogFactory::setInstance($transactionLog);
CreditCardProcessorFactory::setInstance($this->processor);
}
public function tearDown(): void
{
TransactionLogFactory::setInstance(null);
CreditCardProcessorFactory::setInstance(null);
}
public function testSuccessfulCharge()
{
$billingService = new RealBillingService();
$receipt = $billingService->chargeOrder($this->order, $this->creditCard);
$this->assertTrue($receipt->hasSuccessfulCharge());
$this->assertEquals(100, $receipt->getAmountOfCharge());
$this->assertEquals($creditCard, $processor->getCardOfOnlyCharge());
$this->assertEquals(100, $processor->getAmountOfOnlyCharge());
$this->assertTrue($this->transactionLog->wasSuccessLogged());
}
}
This code is clumsy. A global variable holds the mock implementation, so we need
to be careful about setting it up and tearing it down. Should the tearDown
fail, the global variable continues to point at our test instance. This could
cause problems for other tests. It also prevents us from running multiple tests
in parallel.
But the biggest problem is that the dependencies are hidden in the code. If we
add a dependency on a CreditCardFraudTracker
, we have to re-run the tests to
find out which ones will break. Should we forget to initialize a factory for a
production service, we don’t find out until a charge is attempted. As the
application grows, babysitting factories becomes a growing drain on
productivity.
Quality problems will be caught by QA or acceptance tests. That may be sufficient, but we can certainly do better.
Dependency Injection
Like the factory, dependency injection is just a design pattern. The core
principle is to separate behaviour from dependency resolution. In our example,
the RealBillingService
is not responsible for looking up the TransactionLog
and CreditCardProcessor
. Instead, they’re passed in as constructor parameters:
class RealBillingService implements BillingServiceInterface
{
public function __construct(
private readonly CreditCardProcessor $processor,
private readonly TransactionLog $transactionLog
) {}
public chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
{
try {
$result = $this->processor->charge($creditCard, $order->getAmount());
$this->transactionLog->logChargeResult(result);
return $result->wasSuccessful()
? Receipt::forSuccessfulCharge($order->getAmount())
: Receipt::forDeclinedCharge($result->getDeclineMessage());
} catch (UnreachableException $e) {
$this->transactionLog->logConnectException($e);
return Receipt::forSystemFailure($e->getMessage());
}
}
}
We don’t need any factories, and we can simplify the testcase by removing the
setUp
and tearDown
boilerplate:
class RealBillingServiceTest extends TestCase
{
private PizzaOrder $order;
private CreditCard $creditCard;
private InMemoryTransactionLog $transactionLog;
private FakeCreditCardProcessor $processor;
public function setUp(): void
{
$this->order = new PizzaOrder(100);
$this->$creditCard = new CreditCard("1234", 11, 2010);
$this->$transactionLog = new InMemoryTransactionLog();
$this->$processor = new FakeCreditCardProcessor();
}
public function testSuccessfulCharge()
{
$billingService= new RealBillingService($this->processor, $this->transactionLog);
$receipt = $billingService->chargeOrder($this->order, $this->creditCard);
$this->assertTrue($receipt.hasSuccessfulCharge());
$this->assertSame(100, $receipt->getAmountOfCharge());
$this->assertSame($this->creditCard, $this->processor->getCardOfOnlyCharge());
$this->assertSame(100, $this->processor->getAmountOfOnlyCharge());
$this->assertTrue($this->transactionLog->wasSuccessLogged());
}
}
Now, whenever we add or remove dependencies, the compiler will remind us what tests need to be fixed. The dependency is exposed in the API signature.
Unfortunately, now the clients of BillingService
need to lookup its
dependencies. We can fix some of these by applying the pattern again! Classes
that depend on it can accept a BillingService
in their constructor. For
top-level classes, it’s useful to have a framework. Otherwise you’ll need to
construct dependencies recursively when you need to use a service:
<?php
$processor = new PaypalCreditCardProcessor();
$transactionLog = new DatabaseTransactionLog();
$billingService = new RealBillingService($processor, $transactionLog);
// ...
Dependency Injection with Ray.Di
The dependency injection pattern leads to code that’s modular and testable, and
Ray.Di makes it easy to write. To use Ray.Di in our billing example, we first need
to tell it how to map our interfaces to their implementations. This
configuration is done in a Ray.Di module, which is any PHP class that implements
the Module
interface:
class BillingModule extends AbstractModule
{
protected function configure(): void
{
$this->bind(TransactionLog::class)->to(DatabaseTransactionLog::class);
$this->bind(CreditCardProcessor::class)->to(PaypalCreditCardProcessor::class);
$this->bind(BillingServiceInterface::class)->to(RealBillingService::class);
}
}
Ray.Di will inspect the constructor, and lookup values for each parameter.
class RealBillingService implements BillingServiceInterface
{
public function __construct(
private readonly CreditCardProcessor $processor,
private readonly TransactionLog $transactionLog
) {}
public function chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
{
try {
$result = $this->processor->charge($creditCard, $order->getAmount());
$this->transactionLog->logChargeResult($result);
return $result->wasSuccessful()
? Receipt::forSuccessfulCharge($order->getAmount())
: Receipt::forDeclinedCharge($result->getDeclineMessage());
} catch (UnreachableException $e) {
$this->transactionLog->logConnectException($e);
return Receipt::forSystemFailure($e->getMessage());
}
}
}
Finally, we can put it all together. The Injector
can be used to get an
instance of any of the bound classes.
<?php
$injector = new Injector(new BillingModule());
$billingService = $injector->getInstance(BillingServiceInterface::class);
//...
Getting started explains how this all works.
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:
$this->bind(MyConcreteClass::class);
$this->bind(AnotherConcreteClass::class)->in(Scope::SINGLETON);
Note: Untargeted binding does not currently support the annotatedWith()
clause.
Constructor Bindings
When #[Inject]
attribute 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.
$this->bind($interfaceName)
->toConstructor(
$className, // Class name
$name, // Qualifier
$injectionPoint, // Setter injection
$postConstruct // Initialize method
);
(new InjectionPoints)
->addMethod('setGuzzle') // Setter injection method name
->addOptionalMethod('setOptionalToken'); // Optional setter injection method name
Parameter
class_name
Class name
name
Parameter name binding.
If you want to add an identifier to the argument, specify an array with the variable name as the key and the value as the name of the identifier.
[
[$param_name1 => $binding_name1],
...
]
The following string formats are also supported
'param_name1=binding_name1&...'
setter_injection
Specify the method name ($methodName) and qualifier ($named) of the setter injector in the InjectionPoints
object.
(new InjectionPoints)
->addMethod($methodName1)
->addMethod($methodName2, $named)
->addOptionalMethod($methodName, $named);
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 ]]] )
$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.
In the above example, the variable username
is given the identifier pdo_username
, and toInstance
binds the value of the environment variable.
Built-in Bindings
More bindings that you can use
NOTE: It’s very rare that you’d need to use those built-in bindings.
The Injector
In framework code, sometimes you don’t know the type you need until runtime. In this rare case you should inject the injector. Code that injects the injector does not self-document its dependencies, so this approach should be done sparingly.
Providers
For every type Ray.Di knows about, it can also inject a Provider of that type. Injecting Providers describes this in detail.
Multi-bundling
Multi bindinga allows multiple implementations to be injected for a type. It is explained in detail in MultiBindings.
Multibindings
Overview of Multibinder, MapBinder
Multibinder is intended for plugin-type architectures.
Multibinding
Using Multibinder
to host plugins.
Multibinder
Multibindings make it easy to support plugins in your application. Made popular by IDEs and browsers, this pattern exposes APIs for extending the behaviour of an application.
Neither the plugin consumer nor the plugin author need write much setup code for
extensible applications with Ray.Di. Simply define an interface, bind
implementations, and inject sets of implementations! Any module can create a new
Multibinder to contribute bindings to a set of implementations. To illustrate,
we’ll use plugins to summarize ugly URIs like http://bit.ly/1mzgW1
into
something readable on Twitter.
First, we define an interface that plugin authors can implement. This is usually an interface that lends itself to several implementations. For this example, we would write a different implementation for each website that we could summarize.
interface UriSummarizerInterface
{
/**
* Returns a short summary of the URI, or null if this summarizer doesn't
* know how to summarize the URI.
*/
public function summarize(Uri $uri): string;
}
Next, we’ll get our plugin authors to implement the interface. Here’s an implementation that shortens Flickr photo URLs:
class FlickrPhotoSummarizer implements UriSummarizer
{
public function __construct(
private readonly PhotoPaternMatcherInterface $matcher
) {}
public function summarize(Uri $uri): ?string
{
$match = $this->matcher->match($uri);
if (! $match) {
return null;
}
$id = $this->matcher->group(1);
$photo = Photo::loockup($id);
return $photo->getTitle();
}
}
}
The plugin author registers their implementation using a multibinder. Some plugins may bind multiple implementations, or implementations of several extension-point interfaces.
class FlickrPluginModule extends AbstractModule
{
public function configure(): void
{
$uriBinder = Multibinder::newInstance($this, UriSummarizerInterface::class);
$uriBinder->addBinding()->to(FlickrPhotoSummarizer::class);
// ...bind plugin dependencies, such as our Flickr API key
}
}
Now we can consume the services exposed by our plugins. In this case, we’re summarizing tweets:
class TweetPrettifier
{
/**
* @param Map<UriSummarizerInterface> $summarizers
*/
public function __construct(
#[Set(UriSummarizer::class)] private readonyl Map $summarizers;
private readonly EmoticonImagifier $emoticonImagifier;
) {}
public function prettifyTweet(String tweetMessage): Html
{
// split out the URIs and call prettifyUri() for each
}
public function prettifyUri(Uri $uri): string
{
// loop through the implementations, looking for one that supports this URI
foreach ($this->summarizer as summarizer) {
$summary = $summarizer->summarize($uri);
if ($summary != null) {
return $summary;
}
}
// no summarizer found, just return the URI itself
return $uri->toString();
}
}
Note: The method Multibinder::newInstance($module, $type)
can be confusing.
This operation creates a new binder, but doesn’t override any existing bindings.
A binder created this way contributes to the existing Set of implementations for
that type. It would create a new set only if one is not already bound.
Finally we must register the plugins themselves. The simplest mechanism to do so is to list them programatically:
class PrettyTweets
{
public function __invoke(): void
{
$injector = new Injector(
new GoogleMapsPluginModule(),
new BitlyPluginModule(),
new FlickrPluginModule()
// ...
);
$injector->getInstance(Frontend::class)->start();
}
}
(new PrettyTweets)();
MapBinder
You can name the classes you add in the multibinder.
class FlickrPluginModule extends AbstractModule
{
public function configure(): void
{
$uriBinder = Multibinder::newInstance($this, UriSummarizerInterface::class);
$uriBinder->addBinding('flickr')->to(FlickrPhotoSummarizer::class);
// ...bind plugin dependencies, such as our Flickr API key
}
}
In the application, you can retrieve a Map
injected by specifying attributes such as #[Set(UriSummarizer::class)]
with the name as it was when specified by the binding.
class TweetPrettifier
{
/**
* @param Map<UriSummarizerInterface> $summarizers
*/
public function __construct(
#[Set(UriSummarizer::class)] private readonly Map $summarizers;
) {}
public doSomething(): void
{
$filickerSummarizer = $this->summarizers['flicker'];
assert($filickerSummarizer instanceof FlickrPhotoSummarizer);
}
}
Set binding
The setBinding()
method overrides any previous binding.
$UriBinder = Multibinder::newInstance($this, UriSummarizerInterface::class);
$UriBinder->setBinding('flickr')->(FlickrPhotoSummarizer::class);
Map
Map
objects are treated as generics in static analysis. If the injected interface is T, it is written as Map<T>
.
/** @param Map<UriSummarizerInterface> $summarizers **/
Annotation
Since it is not possible to annotate the argument, annotate the property to be assigned with the same name and annotate the property with @Set
.
class TweetPrettifier
{
/** @Set(UriSummarizer::class) */
private $summarizers;
/**
* @param Map<UriSummarizerInterface> $summarizers
*/
public function __construct(Map $summarizers) {
$this->summarizers = $summarizers;
}
}
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.
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
class DbalProvider implements ProviderInterface, SetContextInterface
{
private $dbConfigs;
public function setContext($context)
{
$this->context = $context;
}
public function __construct(#[Named('db_config') 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
.
public function __construct(
#[Named('user')] private readonly Connection $userDb,
#[Named('job')] private readonly Connection $jobDb,
#[Named('log') private readonly Connection $logDb)
) {}
Null Object Binding
A Null Object is an object that implements an interface but whose methods do nothing.
When bound with toNull()
, the code of the Null Object is generated from the interface and bound to the generated instance.
This is useful for testing and AOP.
$this->bind(CreditCardProcessorInterface::class)->toNull();
Injections
How Ray.Di initializes your objects
The dependency injection pattern separates behaviour from dependency resolution. Rather than looking up dependencies directly or from factories, the pattern recommends that dependencies are passed in. The process of setting dependencies into an object is called injection.
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]
attribute in constructor.
public function __construct(DbInterface $db)
{
$this->db = $db;
}
Setter Injection
Ray.Di can inject by methods that have the #[Inject]
attribute. 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
Also called method-call injection action injection, or Invocation 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 add #[Assisted]
to the parameter(s). You need null
default for that parameter.
Note that this Assisted Injection is different from the one in Google Guice.
use Ray\Di\Di\Assisted;
public function doSomething(string $id, #[Assisted] 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
{
public function __construct(
private readonly MethodInvocationProvider $invocationProvider
){}
public function get()
{
$methodInvocation = $this->invocationProvider->get();
[$id] = $methodInvocation->getArguments()->getArrayCopy();
return UserDb::withId($id); // $id for database choice.
}
}
This injection done by AOP is powerful and useful for injecting objects that are only determined at method execution time, as described above. However, this injection is outside the scope of the original IOC and should only be used when really necessary.
Optional Injections
Occasionally it’s convenient to use a dependency when it exists and to fall back
to a default when it doesn’t. Method and field injections may be optional, which
causes Ray.Di to silently ignore them when the dependencies aren’t available. To
use optional injection, apply the #[Inject(optional: true)
attribute:
class PayPalCreditCardProcessor implements CreditCardProcessorInterface
{
private const SANDBOX_API_KEY = "development-use-only";
private string $apiKey = self::SANDBOX_API_KEY;
#[Inject(optional: true)]
public setApiKey(#[Named('paypal-apikey') string $apiKey): void
{
$this->apiKey = $apiKey;
}
}
Injecting Providers
With normal dependency injection, each type gets exactly one instance of each
of its dependent types. The RealBillingService
gets one CreditCardProcessor
and one TransactionLog
. Sometimes you want more than one instance of your
dependent types. When this flexibility is necessary, Ray.Di binds a provider.
Providers produce a value when the get()
method is invoked:
/**
* @template T
*/
interface ProviderInterface
{
/**
* @return T
*/
public function get();
}
The type provided by the provider is specified by the #[Set]
attribute.
class RealBillingService implements BillingServiceInterface
{
/**
* @param ProviderInterface<TransactionLogInterface> $processorProvider
* @param ProviderInterface<CreditCardProcessorInterface> $transactionLogProvider
*/
public __construct(
#[Set(TransactionLogInterface::class)] private ProviderInterface $processorProvider,
#[Set(CreditCardProcessorInterface::class)] private ProviderInterface $transactionLogProvider
) {}
public chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
{
$transactionLog = $this->transactionLogProvider->get();
$processor = $this->processorProvider->get();
/* use the processor and transaction log here */
}
}
To support generics in static analysis, you need to set @param
in phpdoc to ProviderInterface<TransactionLogInterface>
or ProviderInterface<Cre ditCardProcessorInterface>
and so on. The type of the instance obtained by the get()
method is specified and checked by static analysis.
Providers for multiple instances
Use providers when you need multiple instances of the same type. Suppose your application saves a summary entry and a details when a pizza charge fails. With providers, you can get a new entry whenever you need one:
class LogFileTransactionLog implements TransactionLogInterface
{
public function __construct(
#[Set(TransactionLogInterface::class)] private readonly ProviderInterface $logFileProvider
) {}
public logChargeResult(ChargeResult $result): void {
$summaryEntry = $this->logFileProvider->get();
$summaryEntry->setText("Charge " + (result.wasSuccessful() ? "success" : "failure"));
$summaryEntry->save();
if (! $result->wasSuccessful()) {
$detailEntry = $this->logFileProvider->get();
$detailEntry->setText("Failure result: " + result);
$detailEntry->save();
}
}
}
Providers for lazy loading
If you’ve got a dependency on a type that is particularly expensive to produce, you can use providers to defer that work. This is especially useful when you don’t always need the dependency:
class LogFileTransactionLog implements TransactionLogInterface
{
public function __construct(
(#[Set(Connection::class)] private ProviderInterface $connectionProvider
) {}
public function logChargeResult(ChargeResult $result) {
/* only write failed charges to the database */
if (! $result->wasSuccessful()) {
$connection = $connectionProvider->get();
}
}
Providers for Mixing Scopes
Directly injecting an object with a narrower scope usually causes unintended
behavior in your application. In the example below, suppose you have a singleton
ConsoleTransactionLog
that depends on the request-scoped current user. If you
were to inject the user directly into the ConsoleTransactionLog
constructor,
the user would only be evaluated once for the lifetime of the application. This
behavior isn’t correct because the user changes from request to request.
Instead, you should use a Provider. Since Providers produce values on-demand,
they enable you to mix scopes safely:
class ConsoleTransactionLog implements TransactionLogInterface
{
public function __construct(
#[Set(User::class)] private readonly ProviderInterface $userProvider
) {}
public function logConnectException(UnreachableException $e): void
{
$user = $this->userProvider->get();
echo "Connection failed for " . $user . ": " . $e->getMessage();
}
}
GettingStarted
How to start doing dependency injection with Ray.Di.
Getting Started
Ray.Di is a framework that makes it easier for your application to use the dependency injection (DI) pattern. This getting started guide will walk you through a simple example of how you can use Ray.Di to incorporate dependency injection into your application.
What is dependency injection?
Dependency injection is a design pattern wherein classes declare their dependencies as arguments instead of creating those dependencies directly. For example, a client that wishes to call a service should not have to know how to construct the service, rather, some external code is responsible for providing the service to the client.
Here’s a simple example of code that does not use dependency injection:
class Foo
{
private Database $database; // We need a Database to do some work
public function __construct()
{
// Ugh. How could I test this? What if I ever want to use a different
// database in another application?
$this->database = new Database('/path/to/my/data');
}
}
The Foo
class above creates a fixed Database
object directly. This prevents this class from being used with other Database
objects and does not allow the real database to be swapped out for a testing database in tests. Instead of writing untestable or inflexible code, you can use dependency injection pattern
to address all these issues.
Here’s the same example, this time using dependency injection:
class Foo {
private Database $database; // We need a Database to do some work
public function __construct(Database $database)
{
// The database comes from somewhere else. Where? That's not my job, that's
// the job of whoever constructs me: they can choose which database to use.
$this->database = $database;
}
}
The Foo
class above can be used with any Database
objects since Foo
has no knowledge of how the Database
is created. For example, you can create a test version of Database
implementation that uses an in-memory database in tests to make the test hermetic and fast.
The Motivation page explains why applications should use the dependency injection pattern in more detail.
Core Ray.Di concepts
constructor
PHP class constructors can be called by Ray.Di through a process called constructor injection, during which the constructors’ arguments will be created and provided by Ray.Di. (Unlike Guice, Ray.Di does not require the “Inject” annotation in its constructor.)
Here is an example of a class that uses constructor injection:
class Greeter
{
// Greeter declares that it needs a string message and an integer
// representing the number of time the message to be printed.
public function __construct(
#[Message] readonly string $message,
#[Count] readonly int $count
) {}
public function sayHello(): void
{
for ($i=0; $i < $this->count; $i++) {
echo $message;
}
}
}
In the example above, the Greeter
class has a constructor that is called whenapplication asks Ray.Di to create an instance of Greeter
. Ray.Di will create the two arguments required, then invoke the constructor. The Greeter
class’s constructor arguments are its dependencies and applications use Module
to tell Ray.Di how to satisfy those dependencies.
Ray.Di modules
Applications contain objects that declare dependencies on other objects, and those dependencies form graphs. For example, the above Greeter
class has two dependencies (declared in its constructor):
- A
string
value for the message to be printed - An
int
value for the number of times to print the message
Ray.Di modules allow applications to specify how to satisfy those dependencies. For example, the following DemoModule
configures all the necessary dependencies for Greeter
class:
class CountProvider implements ProviderInterface
{
public function get(): int
{
return 3;
}
}
class MessageProvider implements ProviderInterface
{
public function get(): string
{
return 'hello world';
}
}
/**
* Ray.Di module that provides bindings for message and count used in
* {@link Greeter}.
*/
class DemoModule extends AbstractModule
{
protected function configure(): void
{
$this->bind()->annotatedWith(Count::class)->toProvider(CountProvider::class);
$this->bind()->annotatedWith(Message::class)->toProvider(MessageProvider::class);
}
}
In a real application, the dependency graph for objects will be much more complicated and Ray.Di makes creating complex object easy by creating all the transitive dependencies automatically.
Ray.Di injectors
To bootstrap your application, you’ll need to create a Ray.Di Injector
withone or more modules in it. For example, a web server script might that looks like this:
final class MyWebServer {
public function __construct(
private readonyly RequestLoggingInterface $requestLogging,
private readonyly RequestHandlerInterface $requestHandler,
private readonyly AuthenticationInterface $authentication,
private readonyly Database $database
) {}
public function start(): void
{
// ...
}
public function __invoke(): void
{
// Creates an injector that has all the necessary dependencies needed to
// build a functional server.
$injector = new Injector([
new RequestLoggingModule(),
new RequestHandlerModule(),
new AuthenticationModule(),
new DatabaseModule()
]);
// Bootstrap the application by creating an instance of the server then
// start the server to handle incoming requests.
$injector->getInstance(MyWebServer::class)->start();
}
}
(new MyWebServer)();
The injector internally holds the dependency graphs described in your application. When you request an instance of a given type, the injector figures out what objects to construct, resolves their dependencies, and wires everything together. To specify how dependencies are resolved, configure your injector with bindings.
A simple Ray.Di application
The following is a simple Ray.Di application with all the necessary pieces put together:
<?php
require __DIR__ . '/vendor/autoload.php';
use Ray\Di\AbstractModule;
use Ray\Di\Di\Qualifier;
use Ray\Di\Injector;
#[Attribute, Qualifier]
class Message
{
}
#[Attribute, Qualifier]
class Count
{
}
class CountProvider implements ProviderInterface
{
public function get(): int
{
return 3;
}
}
class MessageProvider implements ProviderInterface
{
public function get(): string
{
return 'hello world';
}
}
class DemoModule extends AbstractModule
{
protected function configure()
{
$this->bind()->annotatedWith(Count::class)->toProvider(CountProvider::class);
$this->bind()->annotatedWith(Message::class)->toProvider(MessageProvider::class);
}
}
class Greeter
{
public function __construct(
#[Message] private string $messag,
#[Count] private int $count
) {}
public function sayHello(): void
{
for ($i = 0; $i < $this->count ; $i++) {
echo $this->messag . PHP_EOL;
}
}
}
/*
* Injector's constructor takes one or more modules.
* Most applications will call this method exactly once in bootstrap.
*/
$injector = new Injector([new DemoModule]);
/*
* Now that we've got the injector, we can build objects.
*/
$greeter = $injector->getInstance(Greeter::class);
// Prints "hello world" 3 times to the console.
$greeter->sayHello();
The greeter application constructed a small dependency graph using Ray.Di
that is capable of building instances of Greeter
class. Large applications
usually have many Module
s that can build complex objects.
What’s next?
Read more on how to conceptualize Ray.Di with a simple mental model.
Ray.Di Best Practices
- Minimize mutability
- Inject only direct dependencies
- Use the Injector as little as possible (preferably only once)
- Avoid cyclic dependencies
- Avoid static state
- Modules should be fast and side-effect free
- Avoid conditional logic in modules
- Don’t reuse binding attributes (aka
#[Qualifiers]
) - Organize modules by feature, not by class type
- Document the public bindings provided by modules
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.
Generating a .dot file
Ray.Di’s grapher leans heavily on GraphViz, an open source graph visualization package. It cleanly separates graph specification from visualization and layout. To produce a graph .dot
file for an Injector
, you can use the following code:
use Ray\ObjectGrapher\ObjectGrapher;
$dot = (new ObjectGrapher)(new FooModule);
file_put_contents('path/to/graph.dot', $dot);
The .dot file
Executing the code above produces a .dot
file that specifies a graph. Each entry in the file represents either a node or an edge in the graph. Here’s a sample .dot
file:
digraph injector {
graph [rankdir=TB];
dependency_BEAR_Resource_ResourceInterface_ [style=dashed, margin=0.02, label=<<table cellspacing="0" cellpadding="5" cellborder="0" border="0"><tr><td align="left" port="header" bgcolor="#ffffff"><font color="#000000">BEAR\\Resource\\ResourceInterface<br align="left"/></font></td></tr></table>>, shape=box]
dependency_BEAR_Resource_FactoryInterface_ [style=dashed, margin=0.02, label=<<table cellspacing="0" cellpadding="5" cellborder="0" border="0"><tr><td align="left" port="header" bgcolor="#ffffff"><font color="#000000">BEAR\\Resource\\FactoryInterface<br align="left"/></font></td></tr></table>>, shape=box]
dependency_BEAR_Resource_ResourceInterface_ -> class_BEAR_Resource_Resource [style=dashed, arrowtail=none, arrowhead=onormal]
dependency_BEAR_Resource_FactoryInterface_ -> class_BEAR_Resource_Factory [style=dashed, arrowtail=none, arrowhead=onormal]
}
Rendering the .dot file
You can then paste that code into GraphvizOnlineto render it.
On Linux, you can use the command-line dot
tool to convert .dot
files into images.
dot -T png graph.dot > graph.png
Graph display
Edges:
- Solid edges represent dependencies from implementations to the types they depend on.
- Dashed edges represent bindings from types to their implementations.
- Double arrows indicate that the binding or dependency is to a
Provider
.
Nodes:
- Implementation types are given black backgrounds.
- Implementation instances have gray backgrounds.
Frameworks integration
Performance boost
Injectors that know all dependency bindings can compile simple PHP factory code from those bindings and provide the best performance. Injectors that don’t use anonymous functions for bindings can be serialized, which can improve performance.
In any case, there is no need to initialize the container for every request in production.
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.
Attribute Reader
When not using Doctrine annotations, you can improve performance during development by using only PHP8 attribute readers.
Register it as an autoloader in the composer.json
"autoload": {
"files": [
"vendor/ray/aop/attribute_reader.php"
]
Or set in bootstrap script.
declare(strict_types=1);
use Koriym\Attributes\AttributeReader;
use Ray\ServiceLocator\ServiceLocator;
ServiceLocator::setReader(new AttributeReader());
Backward Compatibility
We will not break backward compatibility.
Ray.Di 2.0 was first released in 2015 and since then we’ve been supporting the latest PHP and adding features; we may no longer support PHP that has become deprecated, but we have never broken backwards compatibility, and we plan to continue to do so.
Ray.Di Tutorial 1
In this tutorial, you will learn the basics of the DI pattern and how to start a Ray.Di project. We will change from a non-di code to a manual DI code, then to a code using Ray.Di to add functionality.
Preparation
Create a project for the tutorial.
mkdir ray-tutorial
cd ray-tutorial
composer self-update
composer init --name=ray/tutorial --require=ray/di:^2 --autoload=src -n
composer update
Create src/Greeter.php
.
A program that greets $users
one after another.
<?php
namespace Ray\Tutorial;
class Greeter
{
public function sayHello(): void
{
$users = ['DI', 'AOP', 'REST'];
foreach ($users as $user) {
echo 'Hello ' . $user . '!' . PHP_EOL;
}
}
}
Prepare a script in bin/run.php
to run it.
<?php
use Ray\Tutorial\Greeter;
require dirname(__DIR__) . '/vendor/autoload.php';
(new Greeter)->sayHello();
Let’s run it.
php bin/run.php
Hello DI!
Hello AOP!
Hello REST!
Dependency pull
Consider making $users
variable.
For example, a global variable?
- $users = ['DI', 'AOP', 'REST'];
+ $users = $GLOBALS['users'];
Too wild. Let’s consider other ways.
define("USERS", ['DI', 'AOP', 'REST']);
$users = USERS;
class User
{
public const NAMES = ['DI', 'AOP', 'REST'];
};
$users = User::NAMES;
$users = Config::get('users')
It is getting the necessary dependencies from the outside, It’s “dependency pull” and in the end it is the same global as the $GLOBALS
variable. It makes the coupling between objects tight and difficult to test.
Dependency Injection
The DI pattern is one in which dependencies are injected from other sources, rather than being obtained from the code itself.
class Greeter
{
public function __construct(
private readonly Users $users
) {}
public function sayHello(): void
{
foreach ($this->users as $user) {
echo 'Hello ' . $user . '!' . PHP_EOL;
}
}
}
Inject not only the data you need, but also the output as a separate service.
public function __construct(
- private readonly Users $users
+ private readonly Users $users,
+ private readonly PrinterInterface $printer
) {}
public function sayHello()
{
foreach ($this->users as $user) {
- echo 'Hello ' . $user . '!' . PHP_EOL;
+ ($this->printer)($user);
}
}
Create the following classes
src/Users.php
<?php
namespace Ray\Tutorial;
use ArrayObject;
final class Users extends ArrayObject
{
}
src/PrinterInterface.php
<?php
namespace Ray\Tutorial;
interface PrinterInterface
{
public function __invoke(string $user): void;
}
src/Printer.php
<?php
namespace Ray\Tutorial;
class Printer implements PrinterInterface
{
public function __invoke(string $user): void
{
echo 'Hello ' . $user . '!' . PHP_EOL;
}
}
src/GreeterInterface.php
<?php
namespace Ray\Tutorial;
interface GreeterInterface
{
public function sayHello(): void;
}
src/CleanGreeter.php
<?php
namespace Ray\Tutorial;
class CleanGreeter implements GreeterInterface
{
public function __construct(
private readonly Users $users,
private readonly PrinterInterface $printer
) {}
public function sayHello(): void
{
foreach ($this->users as $user) {
($this->printer)($user);
}
}
}
Manual DI
Create and run a script bin/run_di.php
to do this.
<?php
use Ray\Tutorial\CleanGreeter;
use Ray\Tutorial\Printer;
use Ray\Tutorial\Users;
require dirname(__DIR__) . '/vendor/autoload.php';
$greeter = new CleanGreeter(
new Users(['DI', 'AOP', 'REST']),
new Printer
);
$greeter->sayHello();
While the number of files may seem to increase in number and overall complexity, the individual scripts are so simple that it is difficult to make them any simpler. Each class has only one responsibility 1, relies on abstractions rather than implementations 2, and is easy to test, extend, and reuse.
Compile Time and Runtime
Code under bin/
constitutes a dependency at compile time, while code under src/
is executed at run time. PHP is a scripting language, but this distinction between compile time and run time can be considered.
Constructor Injection
DI code passes dependencies externally and receives them in the constructor.
$instance = new A(
new B,
new C(
new D(
new E, new F, new G
)
)
);
B and C needed to generate A are passed to the constructor from outside A (without being obtained from inside A); D to generate C, E,F,G to generate D… and dependencies require other dependencies, and objects generate an object graph 3 containing dependent objects.
As the size of the project grows, manual DI using such factory code becomes a reality with problems such as deep nested dependency resolution, instance management such as singletons, reusability, and maintainability. Ray.Di solves that dependency problem.
Module
A module is a set of bindings. There are several types of binding, but here we will use the most basic, link binding, which binds an interface to a class, and instance binding, which binds to an actual instance, such as a value object.
Create src/AppModule.php
.
<?php
namespace Ray\Tutorial;
use Ray\Di\AbstractModule;
class AppModule extends AbstractModule
{
protected function configure(): void
{
$this->bind(Users::class)->toInstance(new Users(['DI', 'AOP', 'REST']));
$this->bind(PrinterInterface::class)->to(Printer::class);
$this->bind(GreeterInterface::class)->to(CleanGreeter::class);
}
}
Create and run bin/run_di.php
to run.
<?php
use Ray\Di\Injector;
use Ray\Tutorial\AppModule;
use Ray\Tutorial\GreeterInterface;
require dirname(__DIR__) . '/vendor/autoload.php';
$module = new AppModule();
$injector = new Injector($module);
$greeter = $injector->getInstance(GreeterInterface::class);
$greeter->sayHello();
Did it work? If something is wrong, please compare it with tutorial1.
Dependency Replacement
Sometimes you want to change the bindings depending on the context of execution, such as only for unit testing, only for development, and so on.
For example, suppose you have a test-only binding src/TestModule.php
.
<?php
namespace Ray\Tutorial;
use Ray\Di\AbstractModule;
final class TestModule extends AbstractModule
{
protected function configure(): void
{
$this->bind(Users::class)->toInstance(new Users(['TEST1', 'TEST2']));
}
}
Modify the bin/run_di.php
script to override this binding.
use Ray\Tutorial\AppModule;
+use Ray\Tutorial\TestModule;
use Ray\Tutorial\GreeterInterface;
require dirname(__DIR__) . '/vendor/autoload.php';
$module = new AppModule();
+$module->override(new TestModule());
Let’s run it.
Hello TEST1!
Hello TEST2!
Dependency on Dependency
Next, the greeting message, which is now fixed and retained in the Printer, is also changed to be injected to support multiple languages.
Create src/IntlPrinter.php
.
<?php
namespace Ray\Tutorial;
use Ray\Di\Di\Named;
class IntlPrinter implements PrinterInterface
{
public function __construct(
#[Message] private string $message
){}
public function __invoke(string $user): void
{
printf($this->message, $user);
}
}
The constructor takes a message string for the greeting, but to identify this bundle attribute bundle for the #[Message]
attribute, src/Message.php
.
<?php
namespace Ray\Tutorial;
use Attribute;
use Ray\Di\Di\Qualifier;
#[Attribute, Qualifier]
class Message
{
}
Change the binding.
class AppModule extends AbstractModule
{
protected function configure(): void
{
$this->bind(Users::class)->toInstance(new Users(['DI', 'AOP', 'REST']));
- $this->bind(PrinterInterface::class)->to(Printer::class);
+ $this->bind(PrinterInterface::class)->to(IntlPrinter::class);
+ $this->bind()->annotatedWith(Message::class)->toInstance('Hello %s!' . PHP_EOL);
$this->bind(GreeterInterface::class)->to(CleanGreeter::class);
}
}
Run it to make sure it does not change.
Then try the error. Comment out the Message::class
binding in the configure()
method.
- $this->bind()->annotatedWith(Message::class)->toInstance('Hello %s!' . PHP_EOL);
+ // $this->bind()->annotatedWith(Message::class)->toInstance('Hello %s!' . PHP_EOL);
This means that Ray.Di does not know what to inject into the dependencies attributed as #[Message]
.
When I run it, I get the following error
PHP Fatal error: Uncaught exception 'Ray\Di\Exception\Unbound' with message '-Ray\Tutorial\Message'
- dependency '' with name 'Ray\Tutorial\Message' used in /tmp/tutorial/src/IntlPrinter.php:8 ($message)
- dependency 'Ray\Tutorial\PrinterInterface' with name '' /tmp/tutorial/src/CleanGreeter.php:6 ($printer)
This is an error that $message
in IntlPrinter.php:8
cannot resolve its dependency, so $printer
in CleanGreeter.php:6
which depends on it also cannot resolve its dependency and the injection failed. Thus, when a dependency of a dependency cannot be resolved, the nesting of that dependency is also displayed.
Finally, let’s create the following bundle as src/SpanishModule.php
and overwrite it in the same way as TestModule.
<?php
namespace Ray\Tutorial;
use Ray\Di\AbstractModule;
class SpanishModule extends AbstractModule
{
protected function configure(): void
{
$this->bind()->annotatedWith(Message::class)->toInstance('¡Hola %s!' . PHP_EOL);
}
}
use Ray\Tutorial\AppModule;
-use Ray\Tutorial\TestModule;
+use Ray\Tutorial\SpanishModule;
use Ray\Tutorial\GreeterInterface;
require dirname(__DIR__) . '/vendor/autoload.php';
$module = new AppModule();
-$module->override(new TestModule());
+$module->override(new SpanishModule());
Have you changed to the Spanish greeting as follows?
¡Hola DI!
¡Hola AOP!
¡Hola REST!
Summary
We have seen the basics of the DI pattern and Ray.Di. Dependencies are injected recursively from the outside rather than obtained by user code from within, and an object graph is generated.
At compile time, the relationship building between objects through dependency binding is complete, and at the runtime, the running code depends only on the interface. By following the DI pattern, the SRP principle 1 and the DIP principle 2 can be followed by nature.
The responsibility of securing dependencies has been removed from the code, making it loosely coupled and simple. The code is stable yet flexible, open to extensions but closed to modifications. 4
¥ —
Ray.Di Mental Model
Learn about Key
, Provider
and how Ray.Di is just a map
When you are reading about “Dependency Injection”, you often see many buzzwords (“Inversion of control”, “Hollywood principle”) that make it sound confusing. But underneath the jargon of dependency injection, the concepts aren’t very complicated. In fact, you might have written something very similar already! This page walks through a simplified model of Ray.Di implementation, which should make it easier to think about how it works.
Ray.Di is a map
Fundamentally, Ray.Di helps you create and retrieve objects for your application to use. These objects that your application needs are called dependencies.
You can think of Ray.Di as being a map[^Ray.Di-map]. Your application code declares the dependencies it needs, and Ray.Di fetches them for you from its map. Each entry in the “Ray.Di map” has two parts:
- Ray.Di key: a key in the map which is used to fetch a particular value from the map.
- Provider: a value in the map which is used to create objects for your application.
Ray.Di keys and Providers are explained below.
map is a reasonable approximation for how Ray.Di behaves.
Ray.Di keys
Ray.Di uses Key
to identify a dependency that can be resolved using the
“Ray.Di map”.
The Greeter
class used in the Getting Started declares two
dependencies in its constructor and those dependencies are represented as Key
in Ray.Di:
#[Message] string
–>$map[$messageKey]
#[Count] int
–>$map[$countKey]
The simplest form of a Key
represents a type in php:
// Identifies a dependency that is an instance of string.
/** @var string $databaseKey */
$databaseKey = $map[$key];
However, applications often have dependencies that are of the same type:
class Message
{
public function __construct(
public readonly string $text
){}
}
class MultilingualGreeter
{
public function __construct(
private readonly Message $englishGreeting,
private readonly Message $spanishGreeting
) {}
}
Ray.Di uses binding attributes to distinguish dependencies that are of the same type, that is to make the type more specific:
class MultilingualGreeter
{
public function __construct(
#[English] private readonly Message $englishGreeting,
#[Spanish] private readonly Message $spanishGreeting
) {}
}
Key
with binding attribute can be created as:
$englishGreetingKey = $map[Message::class . English::class];
$spanishGreetingKey = $map[Message::class . Spanish::class];
When an application calls $injector->getInstance(MultilingualGreeter::class)
to
create an instance of MultilingualGreeter
. This is the equivalent of doing:
// Ray.Di internally does this for you so you don't have to wire up those
// dependencies manually.
$english = $injector->getInstance(Message::class, English::class));
$spanish = $injector->getInstance(Message::class, Spanish::class));
$greeter = new MultilingualGreeter($english, $spanish);
To summarize: Ray.Di Key
is a type combined with an optional binding
attribute used to identify dependencies.
Ray.Di Provider
s
Ray.Di uses
Provider
to represent factories in the “Ray.Di map” that are capable of creating objects
to satisfy dependencies.
Provider
is an interface with a single method:
interface Provider
{
/** Provides an instance/
public function get();
}
Each class that implements Provider
is a bit of code that knows how to give
you an instance of T
. It could call new T()
, it could construct T
in some
other way, or it could return you a precomputed instance from a cache.
Most applications do not implement Provider
interface directly, they use
Module
to configure Ray.Di injector and Ray.Di injector internally creates
Provider
s for all the object it knows how to create.
For example, the following Ray.Di module creates two Provider
s:
class countProvicer implements ProviderInterface
{
public function get(): int
{
return 3;
}
}
class messageProvider implements ProviderInterface
{
public function get(): Message
{
return new Message('hello world');
}
}
class DemoModule extends AbstractModule
{
protected function configure(): void
{
$this->bind()->annotatedWith(Count::class)->toProvider(CountProvicer::class);
$this->bind()->annotatedWith(Message::class)->toProvider(MessageProvicer::class);
}
}
MessageProvicer
that calls theget()
method and returns “hello world”CountProvicer
that calls theget()
method and returns3
Using Ray.Di
There are two parts to using Ray.Di:
- Configuration: your application adds things into the “Ray.Di map”.
- Injection: your application asks Ray.Di to create and retrieve objects from the map.
Configuration and injection are explained below.
Configuration
Ray.Di maps are configured using Ray.Di modules. A Ray.Di module is a unit of configuration logic that adds things into the Ray.Di map. There are two ways to do this:
- Using the Ray.Di Domain Specific Language (DSL).
Conceptually, these APIs simply provide ways to manipulate the Ray.Di map. The manipulations they do are pretty straightforward. Here are some example translations, shown using PHP syntax for brevity and clarity:
Ray.Di DSL syntax | Mental model |
---|---|
bind($key)->toInstance($value) |
$map[$key] = $value; (instance binding) |
bind($key)->toProvider($provider) |
$map[$key] = fn => $value; (provider binding) |
bind(key)->to(anotherKey) |
$map[$key] = $map[$anotherKey]; (linked binding) |
DemoModule
adds two entries into the Ray.Di map:
#[Message] string
–>fn() => (new MessageProvicer)->get()
#[Count] int
–>fn() => (new CountProvicer)->get()
Injection
You don’t pull things out of a map, you declare that you need them. This is the essence of dependency injection. If you need something, you don’t go out and get it from somewhere, or even ask a class to return you something. Instead, you simply declare that you can’t do your work without it, and rely on Ray.Di to give you what you need.
This model is backwards from how most people think about code: it’s a more declarative model rather than an imperative one. This is why dependency injection is often described as a kind of inversion of control (IoC).
Some ways of declaring that you need something:
-
An argument to a constructor:
class Foo { // We need a database, from somewhere public function __construct( private Database $database ) {} }
-
An argument to a
DatabaseProvider::get()
method:class DatabaseProvider implements ProviderInterface { public function __construct( #[Dsn] private string $dsn ){} public function get(): Database { return new Database($this->dsn); } }
This example is intentionally the same as the example Foo
class from
Getting Started Guide.
Unlike Guice, Ray.Di does not require the Inject
attribute to be added to the constructor.
Dependencies form a graph
When injecting a thing that has dependencies of its own, Ray.Di recursively
injects the dependencies. You can imagine that in order to inject an instance of
Foo
as shown above, Ray.Di creates Provider
implementations that look like
these:
class FooProvider implements Provider
{
public function get(): Foo
{
global $map;
$databaseProvider = $map[Database::class]);
$database = $databaseProvider->get();
return new Foo($database);
}
}
class DatabaseProvider implements Provider
{
public function get(): Database
{
global $map;
$dsnProvider = $map[Dsn::class];
$dsn = $dsnProvider->get();
return new Database($dsn);
}
}
class DsnProvider implements Provider
{
public function get(): string
{
return getenv(DB_DSN);
}
}
Dependencies form a directed graph, and injection works by doing a depth-first traversal of the graph from the object you want up through all its dependencies.
A Ray.Di Injector
object represents the entire dependency graph. To create an
Injector
, Ray.Di needs to validate that the entire graph works. There can’t be
any “dangling” nodes where a dependency is needed but not provided.5
If the bound is incomplete somewhere in the graph, Ray.Di will throw an Unbound
exception.
nothing ever uses it—it’s just dead code in that case. That said, just like any dead code, it’s best to delete providers if nobody uses them anymore.
What’s next?
Learn how to use Scopes
to manage the lifecycle of objects created
by Ray.Di and the many different ways to
add entries into the Ray.Di map.
Scopes
By default, Ray returns a new instance each time it supplies a value. This behaviour is configurable via scopes.
use Ray\Di\Scope;
$this->bind(TransactionLogInterface::class)->to(InMemoryTransactionLog::class)->in(Scope::SINGLETON);
Bindings
Overview of bindings in Ray.Di
A binding is an object that corresponds to an entry in Ray.Di map. You add new entries into the Ray.Di map by creating 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 untargeted 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");
}
}
More Bindings
In addition to the bindings you specify the injector includes built-in bindings. When a dependency is requested but not found it attempts to create a just-in-time binding. The injector also includes bindings for the providers of its other bindings.
Module Install
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);
}
Linked Bindings
Linked bindings map a type to its implementation. This example maps the interface TransactionLogInterface to the implementation DatabaseTransactionLog:
$this->bind(TransactionLogInterface::class)->to(DatabaseTransactionLog::class);
Binding Attributes
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 attribute. The attribute and type together uniquely identify a binding. This pair is called a key.
Defining binding attributes
Define qualifier attribute first. It needs to be annotated with Qualifier
attribute.
use Ray\Di\Di\Qualifier;
#[Attribute, Qualifier]
final class PayPal
{
}
To depend on the annotated binding, apply the attribute to the injected parameter:
public function __construct(
#[Paypal] private readonly CreditCardProcessorInterface $processor
){}
You can specify parameter name with qualifier. Qualifier applied all parameters without it.
public function __construct(
#[Paypal('processor')] private readonly CreditCardProcessorInterface $processor
){}
Lastly we create a binding that uses the attribute. This uses the optional annotatedWith
clause in the bind() statement:
$this->bind(CreditCardProcessorInterface::class)
->annotatedWith(PayPal::class)
->to(PayPalCreditCardProcessor::class);
Binding Attributes in Setters
In order to make your custom Qualifier
attribute inject dependencies by default in any method the
attribute is added, you need to implement the Ray\Di\Di\InjectInterface
:
use Ray\Di\Di\InjectInterface;
use Ray\Di\Di\Qualifier;
#[Attribute, Qualifier]
final class PaymentProcessorInject implements InjectInterface
{
public function isOptional()
{
return $this->optional;
}
public function __construct(
public readonly bool $optional = true
public readonly string $type;
){}
}
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 attribute, 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:
$this->bind(CreditCardProcessorInterface::class)
->annotatedWith(PaymentProcessorInject::class)
->toProvider(PaymentProcessorProvider::class);
The provider can now use the information supplied in the qualifier attribute in order to instantiate the most appropriate class.
#[Named]
The most common use of a Qualifier attribute 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 attribute #[Named]
that takes a string.
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
public function __construct(
#[Named('checkout')] private CreditCardProcessorInterface $processor
){}
To bind a specific name, pass that string using the annotatedWith()
method.
$this->bind(CreditCardProcessorInterface::class)
->annotatedWith('checkout')
->to(CheckoutCreditCardProcessor::class);
You need to put the #[Named]
attribuet in order to specify the parameter.
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
public function __construct(
#[Named('checkout')] private CreditCardProcessorInterface $processor,
#[Named('backup')] private CreditCardProcessorInterface $subProcessor
){}
Binding Annotation
Ray.Di can be used with doctrine/annotation for PHP 7.x. See the old README(v2.10) for annotation code examples. To create forward-compatible annotations for attributes, see custom annotation classes.
Since annotations cannot be applied to arguments, the first argument of a custom annotation should be the name of the variable. This is not necessary if the method has only one argument.
/**
* @Paypal('processor')
*/
public function setCreditCardProcessor(
CreditCardProcessorInterface $processor
OtherDepedeciyInterface $depedency
){
Instance Bindings
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:
$this->bind(UserInterface::class)->toInstance(new User);
$this->bind()->annotatedWith('login_id')->toInstance('bear');
Avoid using toInstance()
with objects that are complicated to create, since it can slow down application startup.
Provider Bindings
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. 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
{
public function __construct(
private readonly ConnectionInterface $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);
Injection Point
An InjectionPoint is a class that has information about an injection point.
It provides access to metadata via \ReflectionParameter
or an attribute 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
{
public function __construct(
private InjectionPointInterface $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
Avoid conditional logic in modules
It’s tempting to create modules that have moving parts and can be configured to operate differently for different environments:
class FooModule extends AbstractModule
{
public function __construct(
private readonly ?string $fooServer
}{}
protected function configure(): void
{
if ($this->fooServer != null) {
$this->bind(String::class)->annotatedWith(ServerName::class)->toInstance($this->fooServer);
$this->bind(FooService::class)->to(RemoteFooService::class);
} else {
$this->bind(FooService::class)->to(InMemoryFooService::class);
}
}
}
Conditional logic in itself isn’t too bad. But problems arise when
configurations are untested. In this example, theInMemoryFooService
is used
for development and RemoteFooService
is used in production. But without
testing this specific case, it’s impossible to be sure that RemoteFooService
works in the integrated application.
To overcome this, minimize the number of distinct configurations in your
applications. If you split production and development into distinct modules, it
is easier to be sure that the entire production codepath is tested. In this
case, we split FooModule
into RemoteFooModule
and InMemoryFooModule
. This
also prevents production classes from having a compile-time dependency on test
code.
Another, related, issue with the example above: sometimes there’s a binding for
#[ServerName]
, and sometimes that binding is not there. You should avoid
sometimes binding a key, and other times not.
Avoid static state
Static state and testability are enemies. Your tests should be fast and free of side-effects. But non-constant values held by static fields are a pain to manage. It’s tricky to reliably tear down static singletons that are mocked by tests, and this interferes with other tests.
Although static state is bad, there’s nothing wrong with the static keyword. Static classes are okay (preferred even!) and for pure functions (sorting, math, etc.), static is just fine.
Avoid Circular Dependencies
What are circular dependencies?
Say that your application has a few classes including a Store
, a Boss
, and a
Clerk
.
public class Store {
private final Boss boss;
//...
@Inject public Store(Boss boss) {
this.boss = boss;
//...
}
public void incomingCustomer(Customer customer) {...}
public Customer getNextCustomer() {...}
}
public class Boss {
private final Clerk clerk;
@Inject public Boss(Clerk clerk) {
this.clerk = clerk;
}
}
public class Clerk {
// Nothing interesting here
}
Right now, the dependency chain is all good: constructing a Store
results in
constructing a Boss
, which results in constructing a Clerk
. However, to get
the Clerk
to get a Customer
to do the selling, it will need a reference to
the Store
to get those customer:
public class Store {
private final Boss boss;
//...
@Inject public Store(Boss boss) {
this.boss = boss;
//...
}
public void incomingCustomer(Customer customer) {...}
public Customer getNextCustomer() {...}
}
public class Boss {
private final Clerk clerk;
@Inject public Boss(Clerk clerk) {
this.clerk = clerk;
}
}
public class Clerk {
private final Store shop;
@Inject Clerk(Store shop) {
this.shop = shop;
}
void doSale() {
Customer sucker = shop.getNextCustomer();
//...
}
}
which leads to a cycle: Clerk
-> Store
-> Boss
-> Clerk
. In trying to
construct a Clerk
, an Store
will be constructed, which needs a Boss
, which
needs a Clerk
again!
Ways to avoid circular dependencies
Eliminate the cycle (Recommended)
Cycles often reflect insufficiently granular decomposition. To eliminate such cycles, extract the Dependency Case into a separate class.
Take the above Store
example, the work of managing the incoming customers can
be extracted into another class, say CustomerLine
, and that can be injected
into the Clerk
and Store
.
public class Store {
private final Boss boss;
private final CustomerLine line;
//...
@Inject public Store(Boss boss, CustomerLine line) {
this.boss = boss;
this.line = line;
//...
}
public void incomingCustomer(Customer customer) { line.add(customer); }
}
public class Clerk {
private final CustomerLine line;
@Inject Clerk(CustomerLine line) {
this.line = line;
}
void doSale() {
Customer sucker = line.getNextCustomer();
//...
}
}
While both Store
and Clerk
depend on the CustomerLine
, there’s no cycle in
the dependency graph (although you may want to make sure that the Store
and
Clerk
both use the same CustomerLine
instance). This also means that your
Clerk
will be able to sell cars when your shop has a big tent sale: just
inject a different CustomerLine
.
Break the cycle with a Provider
Injecting a Guice provider
will allow you to add a seam in the dependency graph. The Clerk
will still
depend on the Store
, but the Clerk
doesn’t look at the Store
until it
needs a Store
.
public class Clerk {
private final Provider<Store> shopProvider;
@Inject Clerk(Provider<Store> shopProvider) {
this.shopProvider = shopProvider;
}
void doSale() {
Customer sucker = shopProvider.get().getNextCustomer();
//...
}
}
Note here, that unless Store
is bound as a
Singleton
or in some
other scope to be reused, the shopProvider.get()
call will end up constructing
a new Store
, which will construct a new Boss
, which will construct a new
Clerk
again!
Use factory methods to tie two objects together
When your dependencies are tied together a bit closer, untangling them with the above methods won’t work. Situations like this come up when using something like a View/Presenter paradigm:
public class FooPresenter {
@Inject public FooPresenter(FooView view) {
//...
}
public void doSomething() {
view.doSomethingCool();
}
}
public class FooView {
@Inject public FooView(FooPresenter presenter) {
//...
}
public void userDidSomething() {
presenter.theyDidSomething();
}
//...
}
Each of those objects needs the other object. Here, you can use AssistedInject to get around it:
public class FooPresenter {
private final FooView view;
@Inject public FooPresenter(FooView.Factory viewMaker) {
view = viewMaker.create(this);
}
public void doSomething() {
//...
view.doSomethingCool();
}
}
public class FooView {
@Inject public FooView(@Assisted FooPresenter presenter) {...}
public void userDidSomething() {
presenter.theyDidSomething();
}
public static interface Factory {
FooView create(FooPresenter presenter)
}
}
Such situations also come up when attempting to use Guice to manifest business object models, which may have cycles that reflect different types of relationships. AssistedInject is also quite good for such cases.
Circular proxy feature
In cases where one of the dependencies in the circular chain is an interface type, Guice can work around the circular dependency chain by generating a proxy at runtime to break the cycle. However, this support is really limited and can break unexpectedly if the type is changed to a non-interface type.
To prevent unexpected circular dependency chains in your code, we recommend that
you disable Guice’s circular proxy feature. To do so, install a module that
calls binder().disableCircularProxies()
:
```java {.good} final class ApplicationModule extends AbstractModule { @Override protected void configure() { …
binder().disableCircularProxies(); } } ```
TIP: You can also install Modules.disableCircularProxiesModule()
to disable
circular proxy in Guice.
Document the public bindings provided by modules
To document a Ray.Di module, a good strategy is to describe the public bindings that that module installs, for example:
/**
* Provides FooServiceClient and derived bindings
*
* [...]
*
* The following bindings are provided:
*
* FooServiceClient
* FooServiceClientAuthenticator
*/
final class FooServiceClientModule extends AbstractModule
{
// ...
}
Don’t reuse binding attributes (aka #[Qualifier]
)
Sometimes, of course, it makes sense to bind some highly-related bindings with the same attributes. E.g. #[ServerName]
That said, most binding attributes should only qualify one binding. And you should definitely not reuse a binding attributes for unrelated bindings.
When in doubt, don’t reuse attributes: creating one is straightfoward!
To avoid some boilerplate, sometimes it makes sense to use attribute parameters to create distinct annotation instances from a single declaration. For example:
enum Thing
{
case FOO;
case BAR;
case BAZ;
}
#[Attribute, \Ray\Di\Di\Qualifier]
final class MyThing
{
public function __construct(
public readonly Thing $value
) {}
}
You can then use #[MyThing(Thing::FOO)]
, #[MyThing(Thing::BAR)]
, and #[MyThing(Thing::BAZ)]
rather than defining each of them as separate attribute types.
Inject only direct dependencies
Avoid injecting an object only as a means to get at another object. For example, don’t inject a Customer
as a means to get at an Account
:
class ShowBudgets
{
private readonly Account $account;
public function __construct(Customer $customer)
{
$this->account = $customer->getPurchasingAccount();
}
Instead, inject the dependency directly. This makes testing easier; the test case doesn’t need to concern itself with the customer. Use an Provider
class to create the binding for Account
that uses the binding for Customer
:
class CustomersModule extends AbstractModule
{
protected function configure()
{
$this->bind(Account::class)->toProvider(PurchasingAccountProvider::class);
}
}
class PurchasingAccountProvider implements ProviderInterface
{
public function __construct(
private readonly Customer $customer
) {}
public function get(): Account
{
return $this->customer->getPurchasingAccount();
}
}
By injecting the dependency directly, our code is simpler.
class ShowBudgets
{
public function __construct(
private readonly Account $account
) {}
Use the Injector as little as possible (preferably only once)
Ray.Di has a built-in binding for the Injector
but it should be used sparsely.
Don’t pass injectors into other injected objects through the constructor (which is also called “injecting the injector”). You should declare your dependencies statically.
By injecting the injector, Ray.Di will not know in advance if the dependency can be resolved.
This is because you can get instances directly from the injector.
If the dependencies are not set up correctly and the injector is not injected, the dependency resolution failure can be detected in the compilation of Ray.Di.
However, if you are injecting an injector, Ray.Di may raise an Unbound
exception at runtime (when the code executes getInstance()
lazily) and the dependency resolution may fail.
Minimize mutability
Wherever possible, use constructor injection to create immutable objects. Immutable objects are simple, shareable, and can be composed. Follow this pattern to define your injectable types:
class RealPaymentService implements PaymentServiceInterface
{
public function __construct(
private readnonly PaymentQueue $paymentQueue,
private readnonly Notifier $notifier;
){}
All fields of this class are readonly and initialized by a constructor.
Injecting methods
Constructor injection has some limitations:
- Injected constructors may not be optional.
- It cannot be used unless objects are created by Ray.Di.
- Subclasses must call
parent()
with all dependencies. This makes constructor injection cumbersome, especially as the injected base class changes.
Setter injection is most useful when you need to initialize an instance that is not constructed by Ray.Di.
Modules should be fast and side-effect free
Rather than using an external XML file for configuration, Ray.Di modules are written using regular PHP code. PHP is familiar, works with your IDE, and survives refactoring.
But the full power of the PHP language comes at a cost: it’s easy to do too much in a module. It’s tempting to connect to a database connection or to start an HTTP server in your Ray.Di module. Don’t do this! Doing heavy-lifting in a module poses problems:
- Modules start up, but they don’t shut down. Should you open a database connection in your module, you won’t have any hook to close it.
- Modules should be tested. If a module opens a database as a course of execution, it becomes difficult to write unit tests for it.
- Modules can be overridden. Ray.Di modules support
overrides
, allowing a production service to be substituted with a lightweight or test one. When the production service is created as a part of module execution, such overrides are ineffective.
Rather than doing work in the module itself, define an interface that can do the work at the proper level of abstraction. In our applications we use this interface:
interface ServiceInterface
{
/**
* Starts the service. This method blocks until the service has completely started.
*/
public function start(): void;
/**
* Stops the service. This method blocks until the service has completely shut down.
*/
public function stop(): void;
}
After creating the Injector, we finish bootstrapping our application by starting its services. We also add shutdown hooks to cleanly release resources when the application is stopped.
class Main
{
public function __invoke()
$injector = new Injector([
new DatabaseModule(),
new WebserverModule(),
// ..
]);
$databaseConnectionPool = $injector->getInstance(ServiceInterface::class, DatabaseService::class);
$databaseConnectionPool->start();
$this->addShutdownHook($databaseConnectionPool);
$webserver = $injector->getInstance(ServiceInterface::class, WebserverService::class);
$webserver->start();
$this->addShutdownHook($webserver);
);
}
Organize modules by feature, not by class type
Group bindings into features. Ideally it should be possible to enable/disable an entire working feature by simply installing or not installing a single module in the injector.
For example, don’t just make a FiltersModule
that has bindings for all the
classes that implement Filter
in it, and a GraphsModule
that has all the
classes that implement Graph
, etc. Instead, try to organize modules by
feature, for example an AuthenticationModule
that authenticates requests made
to your server, or a FooBackendModule
that lets your server make requests to
the Foo backend.
This principle is also known as “organize modules vertically, not horizontally”.
-
“In computer science, object-oriented applications have a complex network of interrelated objects. Objects are connected to each other either by being owned by one object or by containing other objects (or their references). This object net is called an object graph.” Object Graph ↩
-
The reverse case is not an error: it’s fine to provide something even if ↩