Concepts

Container


The service collection is also available as its own independent package: elephox/di

The Problem

Imagine you want to create a simple application that can be used to manage todos.

You create several classes, each implementing a part of the application. Say you created the following classes:

Each class has its own responsibilities as well as their own dependencies. For example:

If we look at these constraints, we can see a dependency graph forming:

┌──────────────────────┬──────────────┐
│                      │              │
│     Todo App         │   external   │
│                      │              │
├──────────────────────┼──────────────┤
│                      │              │
│  TodoItemController  │              │
│         │            │              │
│         │            │              │
│         ▼            │              │
│  TodoItemRepository──┼──►ORM        │
│         │            │              │
│         │            │              │
│         ▼            │              │
│  TodoItemValidator───┼──►TimeLib    │
│                      │              │
└──────────────────────┴──────────────┘

Creating this kind of overview is helpful for understanding an applications structure and discover hidden dependencies. One such hidden dependency is the path from TodoItemController to TimeLib:

Now, if you want to create an instance of your controller class, you will need to create a TimeLib instance first and hand it down the graph through every class depending on it! This can become quite cumbersome in larger applications:

// imagine creating a controller instance like this:
$controller = new TodoItemController(
    new TodoItemRepository(
        new ORM(),
        new TodoItemValidator(
            new TimeLib()
        )
    )
);

Now imagine the dependency graph changing and new external dependencies being added. Or your external dependencies' dependencies being changed. You'd have to adjust every instantiation of each class having hidden/direct dependencies. Ludicrous!

The Solution

To solve this problem, let's introduce the concept of a Dependency Injection Container, or Container for short.

A Container is like a builder collecting manuals:

In this analogy, the manuals are classes and what is being built are objects.

This works by using Reflection. The code can basically look at itself and analyse things like parameter type hints, return types and object properties. When you ask the service collection to build a class instance, the service collection looks at the constructor arguments and tries to build an instance of each parameter.


Applying all this to our example app, we can use the service collection like this:

use Elephox\DI\ServiceCollection;

$services = new ServiceCollection();
$services->register(TimeLib::class);
$services->register(ORM::class);
$services->register(TodoItemValidator::class);
$services->register(TodoItemRepository::class);

$controller = $services->getOrInstantiate(TodoItemController::class);

Now you only have to have the service collection instance to care about and it will take care of the rest.

Registering a callback

To influence how the service collection builds an object, you can pass a callback to the register method, which gets invoked when an instance of the registered class is requested:

use Elephox\DI\ServiceCollection;

$services = new ServiceCollection();
$services->register(TimeLib::class, function (Container $c) {
    $timezoneProvider = $c->get(TimeZonesLib::class);
    $timezoneProvider->setDefault('Europe/Berlin');

    return new TimeLib($timezoneProvider);
});

Service Lifetime

The service collection keeps a reference to each object it created and returns it when the same class is requested another time.

You can of course influence this behaviour when registering a class:

use Elephox\DI\ServiceCollection;
use Elephox\DI\ServiceLifetime;

$services = new ServiceCollection();
$services->register(TimeLib::class, lifetime: ServiceLifetime::Transient);

Currently, you can only choose between ServiceLifetime::Singleton and ServiceLifetime::Transient. Singleton of course means there should only ever be one instance of the class within the service collection and that same instance is always returned when the class is requested. Transient means a new instance will be created every time a class is requested.

Aliases

While developing, you might want to change a concrete implementation of a class and haven't used an interface to request it from the service collection. Now you have to update every ->get() call to request the new implementation.

To prevent this, you can add an alias for classes. An alias doesn't need to be a valid class name. It can be any string you want (except the empty string):

use Elephox\DI\ServiceCollection;
use Elephox\DI\ServiceLifetime;

$services = new ServiceCollection();
$services->register(TimeLib::class, aliases: 'time-parser');

// then request it like you would normally:
$services->get('time-parser');

Attention

The alias takes precedence if the service collection has multiple options for injecting a parameter. Aliases are resolved by the parameter name.

Parameter Injection

The service collection implements functions allowing you to call any callback, method or constructor by analyzing the required parameters and trying to provide them.

Class Instantiation

You can use the service collection to instantiate objects for you. This can be helpful if you don't want to or can't provide constructor parameters for a given class:

use Elephox\DI\ServiceCollection;
use Elephox\DI\ServiceLifetime;

// somewhere in your code...
$services = new ServiceCollection();
$services->register(TimeLib::class);

// TestClass.php
class TestClass {
    public function __construct(private TimeLib $timeLib) {}
}

// somewhere else in your code...
$testClassInstance = $services->instantiate(TestClass::class);

Callbacks & Function Invocation

use Elephox\DI\ServiceCollection;
use Elephox\DI\ServiceLifetime;

// somewhere in your code...
$services = new ServiceCollection();
$services->register(TimeLib::class);

// somewhere else....
$callback = function (TimeLib $timeLib) {
    // do something with the TimeLib instance
}

$services->callback($callback);

// or use the service collection to call methods for you, injecting the required parameters

class TestClass {
    public function needsTimeLib(TimeLib $timeLib) {
        // do something with the TimeLib instance
    }
}

// use your own instance...
$testClass = new TestClass();
$services->call($testClass, 'needsTimeLib');

// ...or let the service collection create one for you and call the method
$services->call(TestClass::class, 'needsTimeLib');