A PSR-11 container which reads what a class needs from the class, and is built to do it quickly.
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.
- PHP 8.1 or newer
composer require quillstack/diNothing 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 therePublic typed properties are filled too, which is how a class asks for something it does not want in its constructor.
Say once which class answers to an interface:
$container = new Container([
Storage::class => FileStorage::class,
]);
$container->get(StorageController::class)->storage; // App\FileStorageWhere 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'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; // trueWhere 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\SalesReportcreate() is given the id that was asked for, which is what lets one factory serve a whole
family.
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.
$container->has(Database::class); // true
$container->has('nope'); // falseMeasured 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.
composer test
composer stanThis is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.
- quillstack/framework — what this wires together
- quillstack/cli — commands built the same way
- quillstack/unit-tests — assertions arrive through a constructor too
MIT — see LICENSE.