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

There are many advantages to using dependency injection, but doing so manually often leads to a large amount of boilerplate code to be written. Ray.Di is a framework that makes it possible to write code that uses dependency injection without the hassle of writing much of that boilerplate code,as further detailed in this page on Motivation.

Put simply, Ray.Di alleviates the need for factories and the use of new in your PHP code. You will still need to write factories in some cases, but your code will not depend directly on them. Your code will be easier to change, unit test and reuse in other contexts.

Google Guice and Ray.Di

Ray.Di is a PHP DI framework inspired by Google Guice. Most of the documentation on this site is taken from Google Guice.

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

There are many advantages to using dependency injection, but doing so manually often leads to a large amount of boilerplate code to be written. Ray.Di is a framework that makes it possible to write code that uses dependency injection without the hassle of writing much of that boilerplate code,as further detailed in this page on Motivation.

Put simply, Ray.Di alleviates the need for factories and the use of new in your PHP code. You will still need to write factories in some cases, but your code will not depend directly on them. Your code will be easier to change, unit test and reuse in other contexts.

Google Guice and Ray.Di

Ray.Di is a PHP DI framework inspired by Google Guice. Most of the documentation on this site is taken from Google Guice.

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(): int
    {
        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 Modules 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

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

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 Providers

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 Providers for all the object it knows how to create.

For example, the following Ray.Di module creates two Providers:

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 the get() method and returns “hello world”
  • CountProvicer that calls the get() method and returns 3

Using Ray.Di

There are two parts to using Ray.Di:

  1. Configuration: your application adds things into the “Ray.Di map”.
  2. 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:

  1. An argument to a constructor:

     class Foo
     {
       // We need a database, from somewhere
       public function __construct(
             private Database $database
        ) {}
     }
    
  2. 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

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”.

  1. Single Responsibility Principle (SRP)  2

  2. Dependency Inversion Principle (DIP)  2

  3. “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 

  4. OCP 

  5. The reverse case is not an error: it’s fine to provide something even if