Skip to content

Repository files navigation

Quillstack DI Container

Tests Latest Version Downloads PHP Version StyleCI CodeFactor Quality Gate Coverage Maintainability Reliability Security License

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

composer require quillstack/di

Usage

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

use Quillstack\DI\Container;

final class ExampleController
{
    public function __construct(private Database $database)
    {
    }
}
$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:

$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:

$container = new Container([
    Database::class => ['hostname' => 'localhost'],
]);
final class Database
{
    public function __construct(private string $hostname)
    {
    }
}
$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:

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:

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);
    }
}
$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.

Collaborators a class can do without

A constructor saying a dependency is optional is taken at its word:

final class FileQueue
{
    public function __construct(
        private readonly StorageInterface $storage,
        private readonly string $directory,
        private readonly ?ClockInterface $clock = null   // works without one
    ) {
    }
}

Register a clock and it is used. Register none and the class is built with null, rather than refused for wanting something it said it could manage without.

The line is between nothing registered and registered but broken. Only the first becomes the default; a binding which exists and throws on the way up is a mistake you want to hear about, and handing back null instead would move the failure somewhere further away.

Asking whether it can

$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/di 0.6.0
php-di/php-di 7.1.1
league/container 4.2.5
symfony/dependency-injection v7.4.17

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

Time Relative
quillstack/di 0.89 ms
symfony/dependency-injection, compiled and dumped 1.38 ms 1.6×
league/container 2.07 ms 2.3×
php-di/php-di 2.39 ms 2.7×
symfony/dependency-injection, compiling each time 15.65 ms 17.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, dumped 32 ns
php-di/php-di 40 ns
quillstack/di 53 ns
league/container 669 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

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.

About

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

Topics

Resources

Stars

2 stars

Watchers

2 watching

Forks

Releases

Contributors

Languages