Version 4 supported
This version of Silverstripe CMS is still supported though will not receive any additional features. Go to documentation for the most recent stable version.

Dependency injection

...dependency injection is a design pattern in which an object or function receives other objects or functions that it depends on Wikipedia

In Silverstripe a combination of the Injector API and the Configuration API provide a comprehensive dependency injection pattern. Some of the goals of dependency injection are:

  • Simplified instantiation of objects
  • Providing a uniform way of declaring and managing inter-object dependencies
  • Promoting abstraction of logic

In practical terms it allows developers to:

  • Make class dependencies configurable rather than hard-coded
  • Override or replace core behaviour without needing to alter core code
  • Write more testable code

Injector

The Injector class is the central manager of inter-class dependencies in Silverstripe CMS. It offers developers the ability to declare the dependencies a class type has, or to change the nature of the dependencies defined by other developers.

Basic usage

The following snippet shows Injector creating a new object of type App\MyClass through its create method:

use App\MyClass;
use SilverStripe\Core\Injector\Injector;

$object = Injector::inst()->create(MyClass::class);

Repeated calls to create() create a new object each time.

use App\MyClass;
use SilverStripe\Core\Injector\Injector;

$object = Injector::inst()->create(MyClass::class);
$object2 = Injector::inst()->create(MyClass::class);

echo $object !== $object2;

// returns true;

Singleton pattern

The Injector API can be used for the singleton pattern through get(). Unlike create() subsequent calls to get return the same object instance as the first call.

use App\MyClass;
use SilverStripe\Core\Injector\Injector;

// sets up MyClass as a singleton
$object = Injector::inst()->get(MyClass::class);
$object2 = Injector::inst()->get(MyClass::class);

echo ($object === $object2);

// returns true;

Basic dependency injection

The benefit of constructing objects this way is that the object that the injector returns for My ClassName can be changed by subsequent code or configuration, for example:

use App\MyClient;
use SilverStripe\Core\Injector\Injector;

// default client created - could be in core code
Injector::inst()->registerService(new ReadClient(), MyClient::class);
$client = Injector::inst()->get(MyClient::class);
// $client is an instance of ReadClient

// somewhere later, perhaps in some application code
Injector::inst()->registerService(new WriteClient(), MyClient::class);
$client = Injector::inst()->get(MyClient::class);
// $client is now an instance of WriteClient

Note that 'MyClient' does not have to be an existing class - you could use an abitrary string to identify it. That said using existing classes can be easier to reason about and can be refactored by automatic tools/IDEs.

Using Injector imperatively like this is most common in testing.

Injector API 🤝 configuration API

The Injector API combined with the Configuration API is a powerful way to declare and manage dependencies in your code. For example, MyClass can be swapped out using the following config:

# app/_config/class-overrides.yml
SilverStripe\Core\Injector\Injector:
  App\MyClass:
    class: MyBetterClass

then used in PHP:

use App\MyClass;
use SilverStripe\Core\Injector\Injector;

// sets up MyClass as a singleton
$object = Injector::inst()->get(MyClass::class);
// $object is an instance of MyBetterClass

This allows you to concisely override classes in Silverstripe core or other third-party Silverstripe code.

When overriding other configuration beware the order that configuration is applied. You may have to use the Before/After syntax to apply your override.

Special YAML syntax

You can use the special %$ prefix in the configuration YAML to fetch items via the Injector. For example:

App\Services\MediumQueuedJobService:
    properties:
      queueRunner: '%$App\Tasks\Engines\MediumQueueAsyncRunner'

It is equivalent of calling Injector::get()->instance(MediumQueueAsyncRunner::class) and assigning the result to the MediumQueuedJobService::queueRunner property. This can be useful as these properties can easily updated if provided in a module or be changed for unit testing. It can also be used to provide constructor arguments such as this example from the assets module:

SilverStripe\Core\Injector\Injector:
  # Define the secondary adapter for protected assets
  SilverStripe\Assets\Flysystem\ProtectedAdapter:
    class: SilverStripe\Assets\Flysystem\ProtectedAssetAdapter
  # Define the secondary filesystem for protected assets
  League\Flysystem\Filesystem.protected:
    class: League\Flysystem\Filesystem
    constructor:
      FilesystemAdapter: '%$SilverStripe\Assets\Flysystem\ProtectedAdapter'

Using constants and environment variables

The Injector configuration has the special ability to include core constants or environment variables. They can be used by quoting with back ticks "`". Please ensure you also quote the entire value (see below).

SilverStripe\Core\Injector\Injector:
  CachingService:
    class: SilverStripe\Cache\CacheProvider
    properties:
      CacheDir: '`TEMP_DIR`'

Environment variables are used in the same way:

SilverStripe\Core\Injector\Injector:
  App\Services\MyService:
    class: App\Services\MyService
    constructor:
      baseURL: '`SS_API_URI`'
    credentials:
      id: '`SS_API_CLIENT_ID`'
      secret: '`SS_API_CLIENT_SECRET`'

Note: undefined variables will be replaced with null.

Dependencies

Silverstripe classes can declare a special $dependencies array which can quickly configure dependencies when used with Injector. Injector will instantiate an object for every array value and assign it to a property that matches the array key. For example:

namespace App;

use SilverStripe\Control\Controller;
use ThirdParty\PermissionService;

class MyController extends Controller
{
    private string $permissions;

    // we declare the types for each of the properties on the object. Anything we pass in via the Injector API must
    // match these data types.
    private static $dependencies = [
        'permissions' => '%$' . PermissionService::class,
    ];

    // Setter methods matching the array keys in $dependencies will be automatically
    // used by the injector to pass those dependencies in on instantiation.
    public function setPermissions(PermissionService $service): static
    {
        $this->permissions = $service;
        return $this;
    }
}

Note that using public properties instead of setter methods is also supported, though setter methods are generally preferred for code quality reasons.

When creating a new instance of App\Control\MyController via Injector the permissions property will contain an instance of the ThirdParty\PermissionService that was resolved by Injector.

use App\Control\MyController;
use SilverStripe\Core\Injector\Injector;
use ThirdParty\PermissionService;

$object = Injector::inst()->get(MyController::class);

echo ($object->permissions instanceof PermissionService);
// returns true;

We can then change or override any of those dependencies via the Configuration YAML and Injector does the hard work of wiring it up.

# app/_config/services.yml
SilverStripe\Core\Injector\Injector:
  ThirdParty\PermissionService:
    class: App\MyCustomPermissionService

Now the dependencies will be replaced with our configuration.

use App\Control\MyController;
use App\MyCustomPermissionService;
use SilverStripe\Core\Injector\Injector;

$object = Injector::inst()->get(MyController::class);

// prints true
echo ($object->permissions instanceof MyCustomPermissionService);

Properties

Injector's configuration can also be used to define properties, for example:

SilverStripe\Core\Injector\Injector:
  App\Control\MyController:
    properties:
      textProperty: 'My Text Value'
use App\Control\MyController;

$object = Injector::inst()->get(MyController::class);

echo (is_string($object->textProperty));
// returns true;

Dependent calls

As well as properties, method calls the class depends on can also be specified via the calls property in YAML:

SilverStripe\Core\Injector\Injector:
  App\Logger:
    class: Monolog\Logger
    calls:
      - [pushHandler, ['%$App\Log\DefaultHandler']]

This configuration will mean that every time App\Logger is instantiated by injector the pushHandler method will be called with the arguments [ %$App\Log\DefaultHandler ] (%$App\Log\DefaultHandler will be resolved by injector first). Note that configuration is merged so there may be multiple calls to pushHandler from other configuration files.

Managed objects

Simple dependencies can be specified by the $dependencies, but more complex configurations are possible by specifying constructor arguments, or by specifying more complex properties such as lists.

These more complex configurations are defined in Injector configuration blocks and are read by the Injector at runtime.

Assuming a class structure such as

namespace App\Control;

class MyController
{
    private $permissions;

    private static $dependencies = [];

    public function setPermissions($permissions): static
    {
        $this->permissions = $permissions;
        return $this;
    }
}
namespace App\Control;

class RestrictivePermissionService
{
    private $database;

    public function setDatabase($db): static
    {
        $this->database = $db;
    }
}
namespace App\ORM;

class MySQLDatabase
{
    private $username;

    private $password;

    public function __construct($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
    }
}

And the following configuration..

---
name: MyController
---
App\Control\MyController:
  dependencies:
    permissions: '%$PermissionService'
SilverStripe\Core\Injector\Injector:
  PermissionService:
    class: App\Control\RestrictivePermissionService
    properties:
      database: '%$App\ORM\MySQLDatabase'
  App\ORM\MySQLDatabase:
    constructor:
      0: 'dbusername'
      1: 'dbpassword'

Calling..

use App\Control\MyController;
use SilverStripe\Core\Injector\Injector;

// sets up MyController as a singleton
$controller = Injector::inst()->get(MyController::class);

Would setup the following

  • Create an object of type App\Control\MyController
  • Look through the dependencies and call get('PermissionService')
  • Load the configuration for PermissionService, and create an object of type App\Control\RestrictivePermissionService
  • Look at the properties to be injected and look for the config for App\ORM\MySQLDatabase
  • Create a App\ORM\MySQLDatabase class, passing dbusername and dbpassword as the parameters to the constructor.

Factories

Some services require non-trivial construction which means they must be created by a factory.

Factory interface

Create a factory class which implements the Factory interface. You can then specify the factory key in the service definition, and the factory service will be used.

An example using the App\MyFactory service to create instances of the App\MyService service is shown below:

# app/_config/services.yml
SilverStripe\Core\Injector\Injector:
  App\MyService:
    factory: App\MyFactory
// app/src/MyFactory.php
namespace App;

class MyFactory implements SilverStripe\Core\Injector\Factory
{
    public function create($service, array $params = [])
    {
        return new MyServiceImplementation();
    }
}
use App\MyService;
use SilverStripe\Core\Injector\Injector;

// Will use App\MyFactoryImplementation::create() to create the service instance.
$instance = Injector::inst()->get(MyService::class);

Factory method

To use any class that does not implement the Factory interface as a service factory specify factory and factory_method keys.

An example of HTTP Client service with extra logging middleware:

# app/_config/services.yml
SilverStripe\Core\Injector\Injector:
  App\LogMiddleware:
    factory: 'GuzzleHttp\Middleware'
    factory_method: 'log'
    constructor: ['%$Psr\Log\LoggerInterface', '%$GuzzleHttp\MessageFormatter', 'info']
  GuzzleHttp\HandlerStack:
    factory: 'GuzzleHttp\HandlerStack'
    factory_method: 'create'
    calls:
      - [push, ['%$App\LogMiddleware']]
  GuzzleHttp\Client:
    constructor:
      -
        handler: '%$GuzzleHttp\HandlerStack'

Service inheritance

By default, services registered with Injector do not inherit from one another; This is because it registers named services, which may not be actual classes, and thus should not behave as though they were.

Thus if you want an object to have the injected dependencies of a service of another name, you must assign a reference to that service. References are denoted by using a percent and dollar sign, like in the YAML configuration example below.

SilverStripe\Core\Injector\Injector:
  App\JSONServiceDefinition:
    class: App\JSONServiceImplementor
    properties:
      Serialiser: App\JSONSerialiser
  App\GZIPJSONProvider: '%$App\JSONServiceDefinition'

Injector::inst()->get(GZIPJSONProvider::class) will then be an instance of App\JSONServiceImplementor with the injected properties.

It is important here to note that the 'class' property of the parent service will be inherited directly as well. If class is not specified, then the class will be inherited from the outer service name, not the inner service name.

For example with this config:

SilverStripe\Core\Injector\Injector:
  App\Connector:
    properties:
      AsString: true
  App\ServiceConnector: '%$Connector'

Both App\Connector and App\ServiceConnector will have the AsString property set to true, but the resulting instances will be classes which match their respective service names, due to the lack of a class specification.

Testing with injector

In situations where injector states must be temporarily overridden, it is possible to create nested Injector instances which may be later discarded, reverting the application to the original state. This is done through nest and unnest.

This is useful when writing test cases, as certain services may be necessary to override for a single method call.

use App\LiveService;
use App\MyService;
use App\TestingService;
use SilverStripe\Core\Injector\Injector;

// Setup default service
Injector::inst()->registerService(new LiveService(), MyService::class);

// Test substitute service temporarily
Injector::nest();

Injector::inst()->registerService(new TestingService(), MyService::class);
$service = Injector::inst()->get(MyService::class);
// ... do something with $service

// revert changes
Injector::unnest();

Injectable trait

The SilverStripe\Core\Injector\Injectable trait can be used to indicate your class is able to be used with Injector (though it is not required). It also provides the create and singleton methods to shortcut creating objects through Injector.

For example with the following class:

namespace App;

use SilverStripe\Core\Injector\Injectable;

class MyClass
{
    use Injectable;
}

you can instantiate it with:

use App\MyClass;

$object = MyClass::create();
// or for a singleton
$singletonObject = MyClass::singleton();

this is much shorter than the full Injector syntax:

use App\MyClass;
use SilverStripe\Core\Injector\Injector;

$object = Injector::inst()->create(MyClass::class);
// or for a singleton
$singletonObject = Injector::inst()->get(MyClass::class);

this might look familar as it is the standard way to instantiate a dataobject eg Page::create(). Using this syntax rather than new Page() allows the object to be overridden by dependency injection.

API documentation