PHPackages                             suhock/php-dependency-injection - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. suhock/php-dependency-injection

ActiveLibrary[Framework](/categories/framework)

suhock/php-dependency-injection
===============================

PHP Dependency Injection Library

010PHPCI passing

Since Jul 27Pushed 3w ago1 watchersCompare

[ Source](https://github.com/suhock/php-dependency-injection)[ Packagist](https://packagist.org/packages/suhock/php-dependency-injection)[ RSS](/packages/suhock-php-dependency-injection/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependencies (4)Versions (3)Used By (0)

Dependency Injection Library for PHP
====================================

[](#dependency-injection-library-for-php)

A compile-and-validate dependency injection container for PHP 8.4+, built for both long-running applications and per-request processes.

Highlights
----------

[](#highlights)

- **Validation** – Catch missing dependencies, cycles, captive scopes, and invalid factories when the container is built, with all detected defects reported together.
- **Service Lifetimes** – Use singleton, scoped, and transient lifetimes with deterministic disposal in reverse creation order.
- **Scoped Isolation** – Isolate request or job state in long-running workers with first-class scopes.
- **Keyed Services** – Register and inject multiple implementations of the same service type under distinct keys.
- **Native Lazy Objects** – Defer expensive services and break dependency cycles with native PHP lazy objects.
- **Caching and Diagnostics** – Cache compiled dependency plans for improved performance in per-request processes and export the dependency graph for tooling and analysis.
- **Flexible Integration** – Use the standalone dependency injector, optional PSR-11 adapter, and PHPStan extensions.

```
use Suhock\DependencyInjection\ContainerBuilder;

$container = ContainerBuilder::createDefault()
    ->addSingleton(MyApplication::class)
    ->addSingleton(Logger::class, fn () => new FileLogger('myapp.log'))
    ->addTransient(HttpClient::class, CurlHttpClient::class)
    ->addTransient(CurlHttpClient::class)
    ->build();

$container->get(MyApplication::class)->run();
```

Table of Contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Compatibility](#compatibility)
- [Basic usage](#basic-usage)
- [Building the container](#building-the-container)
    - [Build performance](#build-performance)
    - [Graph diagnostics](#graph-diagnostics)
- [Instance lifetime](#instance-lifetime)
    - [Singleton](#singleton)
    - [Scoped](#scoped)
    - [Transient](#transient)
- [Scopes](#scopes)
    - [Auto-binding](#auto-binding)
    - [Example: FrankenPHP worker mode](#example-frankenphp-worker-mode)
- [Disposing services](#disposing-services)
- [Adding services to the container](#adding-services-to-the-container)
    - [Specify the class name](#specify-the-class-name)
    - [Specify an implementing class name](#specify-an-implementing-class-name)
    - [Provide a factory callback](#provide-a-factory-callback)
    - [Provide a concrete instance](#provide-a-concrete-instance)
- [Keyed services](#keyed-services)
- [Dependency Injector](#dependency-injector)
- [Specifying dependencies](#specifying-dependencies)
    - [Named object types](#named-object-types)
    - [Nullable types](#nullable-types)
    - [Builtin types with default values](#builtin-types-with-default-values)
    - [Union types](#union-types)
    - [Intersection types](#intersection-types)
    - [Lazy dependencies](#lazy-dependencies)
- [Error handling](#error-handling)
- [Caching reflected metadata](#caching-reflected-metadata)
- [Appendix](#appendix)
    - [PSR-11 compatibility](#psr-11-compatibility)
    - [PHPStan extensions](#phpstan-extensions)

Installation
------------

[](#installation)

```
composer require "suhock/dependency-injection"
```

Compatibility
-------------

[](#compatibility)

The library requires PHP 8.4 or later and is tested on PHP 8.4 and 8.5.

The only required runtime dependency is the first-party `suhock/disposable`package; there are no third-party runtime dependencies. The optional `ext-apcu`extension enables persistent caching of reflected metadata; see [Caching reflected metadata](#caching-reflected-metadata).

Basic Usage
-----------

[](#basic-usage)

The `ContainerBuilder` class contains the methods for configuring the container: the `add*` methods, `remove()`, and `configure()`. Its `build()`method compiles that configuration into an immutable `Container`, which provides `get()`, `has()`, `createScope()`, and `dispose()`. Start by constructing a builder.

```
use Suhock\DependencyInjection\ContainerBuilder;

$builder = ContainerBuilder::createDefault();
```

Next, configure the builder: tell it how it should resolve specific services in your application.

```
$builder
    // Inject the constructor's dependencies
    ->addSingleton(MyApplication::class)

    // Manually construct an instance with factory
    ->addSingleton(MyLogger::class, fn () => new FileLogger('myapp.log'))

    // Provide a pre-constructed instance and promise to dispose it later ourselves
    ->addSingleton(RequestContext::class, $requestContext, shouldDispose: false)

    // Alias an interface to an implementing type
    ->addTransient(HttpClient::class, CurlHttpClient::class)

    // Take the autowired instance and configure it before handing it back
    ->addTransient(
        CurlHttpClient::class,
        function (CurlHttpClient $client, Logger $logger): CurlHttpClient {
            $client->addLogger($logger);

            return $client;
        }
    );
```

Finally, call `build()` to compile and validate the whole graph and obtain the container, then call `get()` on it to retrieve an instance of your application and run it. See [Building the container](#building-the-container)for what `build()` checks and how a misconfiguration is reported.

```
$container = $builder->build();

$container
    ->get(MyApplication::class)
    ->handleRequest();
```

The container will inject the constructor's dependencies and provide your application the instance.

```
class MyApplication
{
    // The constructor arguments will be provided by the container
    public function __construct(
        private readonly HttpClient $client,
        private readonly Logger $logger,
    ) {}
}
```

If your application has other entry points (e.g., controllers), it might be useful to inject the container into the part of your application that invokes those entry points (e.g., a router). There is nothing to add for this: `ContainerInterface` auto-binds to the current resolution root, so a router resolved from the container receives the container itself. See [Scopes](#scopes) for the full auto-binding rules, including what a router resolved from a scope receives instead.

```
class MyRouter
{
    public function __construct(
        private readonly ContainerInterface $container,
    ) {}

    public function routeRequest(string $method, string $path): void
    {
        $controllerClassName = $this->getControllerClassName($path);
        $controller = $this->container->get($controllerClassName);
        $controller->handleRequest($method);
    }
}
```

Warning

Reserve this for dispatchers that only know the required type at runtime. Everywhere else, inject the concrete dependency directly; pulling it from the container (the service locator pattern) makes code harder to test and refactor.

### Building the container

[](#building-the-container)

`ContainerBuilder::build(): Container` compiles the configured dependency graph, validates it, and returns an immutable `Container`. Every service you add must be resolvable. If the configuration has a defect, `build()` reports it as an error rather than waiting until you request the service.

```
$container = $builder->build();
```

If validation finds any guaranteed-failure defect, `build()` throws one `Suhock\DependencyInjection\Validation\ContainerValidationException` carrying every problem it found, not just the first:

```
use Suhock\DependencyInjection\Validation\ContainerValidationException;

try {
    $container = $builder->build();
} catch (ContainerValidationException $e) {
    foreach ($e->getIssues() as $issue) {
        // $issue->kind, $issue->className, $issue->key, $issue->message
        echo $issue->kind->name . ': ' . $issue->serviceId() . ' - ' . $issue->message . "\n";
    }
}
```

The builder is still usable after a failed build: fix the configuration and call `build()` again; each successful `build()` also produces a fully independent `Container`.

`build()` reports every one of the following as a build error:

- A required dependency that is not resolvable from the container, including a keyed dependency not added under that key.
- An interface mapped to an implementation that is not itself a resolvable service.
- A required parameter with a builtin type and no default value.
- A factory whose declared return type can never satisfy the service class it was added for.
- A service class that can never be instantiated: missing, abstract, or an interface.
- A dependency cycle in which every edge is required, so no member of the cycle can ever construct.
- A singleton that reaches a scoped service through required edges: a captive dependency (see [Scopes](#scopes)).
- A `#[Lazy]` parameter the container cannot construct as a lazy object (see [Lazy dependencies](#lazy-dependencies)).

#### Build performance

[](#build-performance)

Without a cache, `build()` recompiles and revalidates the whole graph every time it is called. That is inexpensive for most applications, but on a per-request lifecycle such as PHP-FPM you pay that cost on every request. Supplying a `CacheInterface` (e.g. `ApcuCache`) lets `build()` store the validated plans under a fingerprint of the configuration; rebuilding an unchanged configuration loads the stored plans and skips compilation and validation entirely:

```
use Suhock\DependencyInjection\Cache\ApcuCache;
use Suhock\DependencyInjection\ContainerBuilder;

$container = ContainerBuilder::createDefault(new ApcuCache())
    ->addSingleton(MyApplication::class)
    ->build();
```

With APCu, each later `build()` of an unchanged configuration costs little more than a hash and a cache lookup; the first build after a deploy or a configuration change still pays for full compilation and validation. A worker-mode runtime that builds once at boot (see [FrankenPHP worker mode](#example-frankenphp-worker-mode)) pays that cost once regardless of caching. The same cache also memoizes the reflected metadata used by the injector; see [Caching reflected metadata](#caching-reflected-metadata).

#### Graph diagnostics

[](#graph-diagnostics)

`exportDependencyGraph()` exports the dependency graph `build()` would produce as plain data for external tooling: every service (including the [auto-bound](#auto-binding) ones) and every satisfied dependency edge, with the injection point each edge flows through and whether it is required.

```
$graph = $builder->exportDependencyGraph();

// The graph roots (services nothing injects) are the ids no edge targets.
// They are typically the entry points your application resolves itself.
$targets = array_map(fn ($edge) => $edge->targetId, $graph->edges);
$roots = array_diff($graph->serviceIds, $targets);

// Or render it:
foreach ($graph->edges as $edge) {
    echo "\"$edge->sourceId\" -> \"$edge->targetId\";\n"; // Graphviz
}
```

The export mirrors what resolution would actually traverse. Unsatisfiable injection points produce no edge (validation reports those); an added-but-never-chosen union member gets no incoming edge; and dependencies hidden inside factory bodies do not appear. `exportDependencyGraph()` never throws, so a configuration that would fail `build()` still exports.

### Instance lifetime

[](#instance-lifetime)

The lifetime of an instance determines when the container should request a fresh instance of a class. There are three lifetime strategies for classes: singleton, scoped, and transient.

#### Singleton

[](#singleton)

Singleton instances are persisted for the lifetime of the container. When the container receives a request for a singleton instance for the first time, it will call the factory that you specified for that class, store the result, and then return it. Any time the container receives a subsequent request for that class, directly or through any [scope](#scopes), it will return that same instance. The default `ContainerBuilder` provides convenience methods for adding singleton factories, all starting with the prefix `addSingleton`.

#### Scoped

[](#scoped)

Scoped instances are persisted for the lifetime of a [scope](#scopes) created by `Container::createScope()`. Each scope receives its own instance the first time it requests the class, and that instance's dependencies are resolved from the scope, so scoped services can depend on other scoped services. Requesting a scoped instance with no scope active (directly from the root container, or from a singleton's dependency graph, which always resolves against the root) throws a `ScopeException`. The default `ContainerBuilder` provides convenience methods for adding scoped factories, all starting with the prefix `addScoped`.

#### Transient

[](#transient)

Transient instances are never persisted and the container provides a fresh value each time an instance is requested. Each time the container receives a request for a transient instance, it will call the factory you specified for that class. The default `ContainerBuilder` provides convenience methods for adding transient factories, all starting with the prefix `addTransient`.

### Scopes

[](#scopes)

A scope represents a bounded unit of work, such as an HTTP request in a long-running application server, a message pulled off a queue, or a job in a worker. Build the container once, then create a scope with `Container::createScope()`, resolve services from it as you would from the container, and dispose it when the unit of work ends:

```
use Suhock\DependencyInjection\ContainerBuilder;

$container = ContainerBuilder::createDefault()
    ->addSingleton(LoggerInterface::class, FileLogger::class)
    ->addSingleton(FileLogger::class)
    ->addScoped(RequestContext::class)
    ->addTransient(RequestHandler::class)
    ->build();

$scope = $container->createScope();

try {
    // Both handlers share one RequestContext; the logger is the container-wide
    // singleton.
    $scope->get(RequestHandler::class)->handle();
    $scope->get(RequestHandler::class)->handle();
} finally {
    $scope->dispose();
}
```

Within a scope, services added with `addScoped*` methods resolve to one instance per scope, and every dependency in their graph is resolved from the scope, so transient services requested from a scope also receive the scope's scoped instances. Singleton services resolve to the same instance no matter which scope requests them, and their dependencies always resolve against the root container. A singleton that depends on a scoped service therefore fails with a `ScopeException` instead of capturing one scope's instance. When the scoped service is required (reachable through required edges alone), [build validation](#building-the-container) catches this as a captive-dependency error before you ever call `get()`. A scoped service requested with no active scope (for example from inside a factory body) is something validation cannot predict, so it throws `ScopeException` at run time.

`dispose()` releases the scope's cached instances; any further request to the scope throws a `ScopeException`. Disposing a scope more than once has no effect.

#### Auto-binding

[](#auto-binding)

`ContainerInterface` and `ScopeFactoryInterface` are automatically added at `build()`, unless your own configuration already provides them, so most applications never add either explicitly:

- `ContainerInterface` resolves to the *current resolution root*: a service resolved from a scope receives that scope, and a service resolved from the root container receives the container. A scoped service can therefore depend on `ContainerInterface` to look up further services scoped to the same unit of work, and a service resolved from the root always receives the root. See the router example in [Basic usage](#basic-usage).
- `ScopeFactoryInterface` resolves to the root `Container` from any depth, even from inside a scope, since only the root can create new scopes.
- Neither auto-bound service is ever disposed by the container, and `has()`reports both as present. Adding your own descriptor for either id wins over the automatic binding.

A service that needs to open scopes of its own should depend on `ScopeFactoryInterface` rather than on the container:

```
final class QueueWorker
{
    public function __construct(private readonly ScopeFactoryInterface $scopes)
    {
    }

    public function process(Message $message): void
    {
        $scope = $this->scopes->createScope();

        try {
            $scope->get(MessageHandler::class)->handle($message);
        } finally {
            $scope->dispose();
        }
    }
}
```

#### Example: FrankenPHP worker mode

[](#example-frankenphp-worker-mode)

Application servers such as [FrankenPHP](https://frankenphp.dev/docs/worker/)keep the PHP process alive across many requests: the application (including the container and its singletons) boots once, and each incoming request is handled by a callback. Without the per-request teardown that PHP-FPM provided, any request-specific state held by a long-lived service silently leaks into subsequent requests. Creating a scope per request restores that isolation: scoped services live exactly as long as the request.

```
