Skip to content

Container

composer require quillstack/di

A PSR-11 container which reads what a class needs from the class, and is built to do it quickly.

Why this exists

A container has two jobs and they pull against each other: work out what a class needs, and hand it over fast. Most containers answer that by making you choose — write the wiring out by hand and it is fast, let it work things out and it is slow. The ones that give you both compile a container to a PHP file, which is another build step, another cache to invalidate, and another thing that is stale in development.

This one reads a constructor and remembers what it read. Nothing is compiled and nothing is written to disk, and building a four-deep object graph from cold takes under a millisecond — see the benchmark.

It is also the container this framework runs on, which is the reason it exists at all: every other package here can be built by hand, without any container, and none of them needs this one. A container that packages depend on is a framework wearing a container's clothes.

Requirements

  • PHP 8.1 or newer

Installation

shell
composer require quillstack/di

Usage

Nothing is registered. A class is asked for, and what its constructor declares is worked out:

php
use Quillstack\DI\Container;

final class ExampleController
{
    public function __construct(private Database $database)
    {
    }
}
php
$controller = (new Container())->get(ExampleController::class);
// App\ExampleController, with its Database already there

Public typed properties are filled too, which is how a class asks for something it does not want in its constructor.

Interfaces

Say once which class answers to an interface:

php
$container = new Container([
    Storage::class => FileStorage::class,
]);

$container->get(StorageController::class)->storage;   // App\FileStorage

Parameters

Where a class needs a value rather than an object, name it:

php
$container = new Container([
    Database::class => ['hostname' => 'localhost'],
]);
php
final class Database
{
    public function __construct(private string $hostname)
    {
    }
}
php
$container->get(ExampleController::class)->database->hostname;   // 'localhost'

An object you already have

Where something is built once at boot and used everywhere, hand the object over:

php
use Psr\Log\LoggerInterface;
use Quillstack\Logger\Logger;

$logger = new Logger();

$container = new Container([
    LoggerInterface::class => $logger,
]);

$container->get(LoggingController::class)->logger === $logger;   // true

Your own factory

Where a family of objects is built the same way — requests, reports, messages — write the factory and let the container use it:

php
use Quillstack\DI\Container;
use Quillstack\DI\CustomFactoryInterface;

final class ReportFactory implements CustomFactoryInterface
{
    private Container $container;

    public function setContainer(Container $container): self
    {
        $this->container = $container;

        return $this;
    }

    public function create(string $id): object
    {
        return new SalesReport($id);
    }
}
php
$container = new Container([
    Report::class => ReportFactory::class,
]);

$container->get(ReportController::class)->report;   // App\SalesReport

create() is given the id that was asked for, which is what lets one factory serve a whole family.

Asking whether it can

php
$container->has(Database::class);   // true
$container->has('nope');            // false

Benchmark

Measured with quillstack/benchmark on one object graph four deep — a controller needing a service and a repository, the service needing the repository and a clock, the repository needing a connection. All four containers build the same graph. Runs are interleaved, each figure is the median of five, and PHP is 8.5.7.

Version
quillstack/di0.6.0
php-di/php-di7.1.1
league/container4.2.5
symfony/dependency-injectionv7.4.17

A container built and the graph resolved, in a fresh process — which is what a PHP request does:

TimeRelative
quillstack/di0.89 ms
symfony/dependency-injection, compiled and dumped1.38 ms1.6×
league/container2.07 ms2.3×
php-di/php-di2.39 ms2.7×
symfony/dependency-injection, compiling each time15.65 ms17.6×

The last row is the same container without its dump: Symfony compiles to a PHP file which is normally written once at deploy, so the row above it is the fair one. It is in the table because a container which has to be compiled is a build step this one does not have — in development, that 15 ms is what a changed class costs.

Asking again for something already built, a thousand times over:

Per call
symfony/dependency-injection, dumped32 ns
php-di/php-di40 ns
quillstack/di53 ns
league/container669 ns

This one is not the fastest here, and the gap is not worth having. All four are looking a key up in an array; 21 nanoseconds is nothing an application will feel, and the number that decides a request is the one above.

Tests

shell
composer test
composer stan

The rest of Quillstack

This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.

License

MIT — see LICENSE.

Released under the MIT License.