The PHP Dependency Injection library provides a customizable dependency injection framework for projects running on PHP 8.1 or later.
$container = Suhock\DependencyInjection\ContainerBuilder::createDefault()
->addSingleton(MyApplication::class)
->addSingleton(MyLogger::class, fn () => new FileLogger('myapp.log'))
->addScopedInstance(RequestContext::class, $requestContext, shouldDispose: false)
->addTransient(HttpClient::class, CurlHttpClient::class)
->addTransient(CurlHttpClient::class)
->build()
->get(MyApplication::class)
->run();This library provides singleton, scoped, and transient lifetime strategies and a variety of ways of adding services to the container. You can also add more than one implementation of the same type as keyed services.
Once configured, build() compiles and validates
the whole dependency graph and hands back an immutable Container.
The library also provides an Injector class for
injecting dependencies and explicit parameters into a specific function or
constructor.
- Installation
- Compatibility
- Basic usage
- Building the container
- Instance lifetime
- Scopes
- Disposing services
- Adding services to the container
- Keyed services
- Dependency Injector
- Specifying dependencies
- Error handling
- Caching reflected metadata
- Appendix
Add suhock/dependency-injection to the require section of your project's
composer.json file.
{
"require": {
"suhock/dependency-injection": "^1.0"
}
}Alternatively, use the command line from your project's root directory.
composer require "suhock/dependency-injection"The library requires PHP 8.1 or later and is tested on PHP 8.1, 8.2, 8.3, 8.4, and 8.5.
There are no required runtime dependencies. The optional ext-apcu extension
enables persistent caching of reflected metadata; see
Caching reflected metadata.
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
->addScopedInstance(RequestContext::class, $requestContext, shouldDispose: false)
// Alias an interface to an implementing type
->addTransient(HttpClient::class, CurlHttpClient::class)
// Add optional values with a mutator after injecting the constructor's dependencies
->addTransientClass(
CurlHttpClient::class,
function (CurlHttpClient $client, Logger $logger): void {
$client->addLogger($logger);
}
);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
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 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.
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.
- An invalid
#[Inject]or#[Key]member. - 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).
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())
->addSingletonClass(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) pays that cost once
regardless of caching. The same cache also memoizes the reflected metadata used
by the injector; see Caching reflected metadata.
exportDependencyGraph() exports the dependency graph build() would produce
as plain data for external tooling: every service (including the
auto-bound 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.
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 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, it will return that same
instance. The default ContainerBuilder provides convenience methods for
adding singleton factories, all starting with the prefix addSingleton.
Scoped instances are persisted for the lifetime of a scope 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 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.
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)
->addSingletonClass(FileLogger::class)
->addScopedClass(RequestContext::class)
->addTransientClass(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 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.
ContainerInterface and ScopeFactoryInterface are automatically added at
build(), unless your own configuration already provides them, so most
applications never add either explicitly:
ContainerInterfaceresolves 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 onContainerInterfaceto 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.ScopeFactoryInterfaceresolves to the rootContainerfrom 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();
}
}
}Application servers such as FrankenPHP 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.
<?php
// public/worker.php
use Suhock\DependencyInjection\ContainerBuilder;
require dirname(__DIR__) . '/vendor/autoload.php';
// Built once, reused for every request this worker handles: build()'s
// compilation and validation cost is paid a single time, before the loop
// starts, not per request.
$container = ContainerBuilder::createDefault()
->addSingletonClass(FileLogger::class)
->addSingletonImplementation(Logger::class, FileLogger::class)
// FrankenPHP refreshes the superglobals before each request.
->addScopedFactory(RequestContext::class, fn () => RequestContext::fromGlobals())
->addTransientClass(RequestHandler::class)
->build();
$handler = static function () use ($container): void {
$scope = $container->createScope();
try {
$scope->get(RequestHandler::class)->handle();
} finally {
$scope->dispose();
}
};
while (frankenphp_handle_request($handler)) {
gc_collect_cycles();
}Run it with:
frankenphp php-server --worker public/worker.phpEvery RequestHandler and any service in its dependency graph receives the
current request's RequestContext; when dispose() runs, the scope's cached
instances are released (and any that implement
DisposableInterface have their dispose() method
called), so nothing carries over into the next iteration of the loop. The same
pattern applies to any long-running runtime (a RoadRunner or Swoole worker, a
queue consumer, or a daemon), with the runtime's own receive loop in place of
frankenphp_handle_request().
A service that holds a resource (a database transaction, an open file, a
socket) often needs to release it deterministically when its lifetime ends,
rather than waiting for garbage collection. A service can implement
DisposableInterface to be notified:
use Suhock\DependencyInjection\DisposableInterface;
final class UnitOfWork implements DisposableInterface
{
public function __construct(private readonly Connection $connection)
{
}
public function dispose(): void
{
$this->connection->rollBackIfActive();
}
}When a resolution root (the container or a scope) is disposed, it calls
dispose() on the disposable services it created, in reverse creation
order so that dependents are disposed before their dependencies. build()
itself constructs nothing, so only the services your application actually
resolves are ever disposed:
use Suhock\DependencyInjection\ContainerBuilder;
$container = ContainerBuilder::createDefault()
->addScopedClass(Connection::class)
->addScopedClass(UnitOfWork::class)
->build();
$scope = $container->createScope();
try {
$scope->get(UnitOfWork::class)->commit();
} finally {
// Disposes UnitOfWork, then Connection.
$scope->dispose();
}The container disposes its own singletons (and any surviving transients it created) when the container itself is disposed:
$container = ContainerBuilder::createDefault()
->addSingletonClass(ConnectionPool::class) // implements DisposableInterface
->build();
// ... run the application ...
$container->dispose();After a container is disposed, any further get(), has(), or createScope()
call throws a ContainerException. Disposing a container or scope more than
once has no effect.
Caution
By default the built container disposes every disposable instance it holds,
including concrete instances you explicitly supply. When an instance's
disposal is managed by something outside the container or if you intend to
dispose it yourself, you should use add*Instance() and pass
shouldDispose: false:
// The pool is closed elsewhere; the container must not dispose it.
$builder->addSingletonInstance(ConnectionPool::class, $pool, shouldDispose: false);- Scoped and singleton disposables are always disposed when their scope or container is disposed.
- Transient disposables are disposed only if they are still referenced
when their resolution root is disposed; a transient the application has
already discarded is left to normal garbage collection (implement
__destruct()if it must always clean up). This tracking uses aWeakMap, so discarded transients never accumulate. - Disposal proceeds in reverse creation order. This relies on
dependencies being constructed before their dependents, which holds for
constructor,
Inject-attribute, and mutator injection. A service that resolves further dependencies lazily (for example by holding the container or aScopeFactoryInterfaceand callingget()after construction) can invert that order for the pair involved. - If a
dispose()call throws, the remaining instances are still disposed and the first exception is rethrown once the sweep completes.
Scopes created from a container are managed by their own caller; dispose them
before disposing the container so that their scoped instances are swept.
Disposal via a custom callback for classes that cannot implement
DisposableInterface is not yet supported.
There are a number of built-in ways to specify how services should be resolved:
- Specify the class name
- Specify an implementing class name
- Provide a factory callback
- Provide a concrete instance
The container will construct the named class by calling the class's constructor, automatically resolving any dependencies in the constructor's parameter list.
If the class has any methods with an Inject attribute, the container will
call those methods, resolving and injecting any dependencies listed in the
parameter list. Any property with the Inject attribute will be resolved
automatically from the container.
The optional $mutator callback allows additional configuration of the object
after the container has initialized it. The callback must take an instance of
the class as its first parameter. Additional parameters will be injected.
class ContainerBuilder
{
/**
* @template TClass of object
* @param class-string<TClass> $className
* @param (callable(TClass, mixed...): void)|null $mutator
* @return $this
*/
public function addSingletonClass(string $className, ?callable $mutator = null): self;
public function addScopedClass(string $className, ?callable $mutator = null): self;
public function addTransientClass(string $className, ?callable $mutator = null): self;
}Tip
If a mutator callback is not needed, the shorthand forms can be used instead:
$builder->addSingleton(MyService::class); // equivalent to $builder->addSingletonClass(MyService::class);
$builder->addScoped(MyService::class); // equivalent to $builder->addScopedClass(MyService::class);
$builder->addTransient(MyService::class); // equivalent to $builder->addTransientClass(MyService::class);In the following example, when the container provides an instance of MyService
it will automatically inject all dependencies into its constructor to create an
instance.
$builder->addSingletonClass(MyService::class);When the container provides instances of CurlHttpClient, after injecting the
constructor dependencies, it will also set its logger property.
$builder->addTransientClass(
CurlHttpClient::class,
function (CurlHttpClient $obj, Logger $logger): void {
$obj->setLogger($logger);
}
);When the container provides an instance of CurlHttpClient, it will see that
setLogger() has an Inject attribute and call it passing in a Logger
instance resolved from the container.
use Suhock\DependencyInjection\Inject;
class CurlHttpClient
{
#[Inject]
public function setLogger(Logger $logger): void {
$this->logger = $logger;
}
// ...
}The container will provide the named service by resolving the named implementing subclass in its place.
Important
You must also add the implementing class to the container as its own
resolvable service, or build() will reject the configuration.
class ContainerBuilder
{
/**
* @template TClass of object
* @template TImplementation of TClass
* @param class-string<TClass> $className
* @param class-string<TImplementation> $implementationClassName
* @return $this
*/
public function addSingletonImplementation(string $className, string $implementationClassName): self;
public function addScopedImplementation(string $className, string $implementationClassName): self;
public function addTransientImplementation(string $className, string $implementationClassName): self;
}$builder
->addSingletonImplementation(HttpClient::class, CurlHttpClient::class)
->addSingletonClass(CurlHttpClient::class);When your application requests an instance of HttpClient, the container will
see that it should actually provide an instance of CurlHttpClient. It will
then inject the CurlHttpClient constructor's dependencies to provide an instance.
$builder
->addTransientImplementation(Throwable::class, Exception::class)
->addTransientImplementation(Exception::class, LogicException::class)
->addTransientClass(LogicException::class);When your application requests an instance of Throwable, the container will
see that it should actually provide an instance of Exception. Next it will
see that instances of Exception should be created using LogicException.
Finally, it will provide an instance of LogicException for Throwable by
injecting its constructor's dependencies. If your application instead requests an instance of
Exception then the container will also provide an instance of
LogicException.
The container must know how to provide the implementation, or build() will
reject the configuration:
$builder->addSingletonImplementation(HttpClient::class, CurlHttpClient::class);
/*
* build() throws a ContainerValidationException because CurlHttpClient is
* not itself added as a resolvable service. See "Building the container".
*/
$container = $builder->build();The container will resolve the named service by requesting it from the provided factory callback method. Any parameters in the factory method will be resolved automatically.
class ContainerBuilder
{
/**
* @template TClass of object
* @param class-string<TClass> $className
* @param callable(mixed...): TClass $factory
* @return $this
*/
public function addSingletonFactory(string $className, callable $factory): static;
public function addScopedFactory(string $className, callable $factory): static;
public function addTransientFactory(string $className, callable $factory): static;
}Tip
If providing a Closure the shorthand forms can be also used:
// equivalent to $builder->addSingletonFactory(MyService::class, fn () => new MyService('foo'));
$builder->addSingleton(MyService::class, fn () => new MyService('foo'));Caution
If you need to provide a named function, you must use the add{Lifetime}Factory
form, since the shorthand methods interpret string sources as class names.
$builder->addSingletonFactory(
Mailer::class,
fn (AppConfig $config) => new Mailer($config->mailerTransport)
);When your application requests an instance of Mailer from the container, it
will call the specified factory, injecting the AppConfig dependency. The
factory then manually constructs an instance, specifying the mailer transport
from that config.
$builder->addTransientFactory(
Logger::class,
fn (FileWriter $writer) => new class($writer) implements Logger {
public function __construct(private readonly FileWriter $writer)
{
}
public function log(string $message): void
{
$this->writer->writeLine($message);
}
}
);The container will resolve the specified service to the provided class instance.
class ContainerBuilder
{
/**
* @template TClass of object
* @param class-string<TClass> $className
* @param TClass $instance
* @return $this
*/
public function addSingletonInstance(string $className, object $instance, bool $shouldDispose = true): static;
public function addScopedInstance(string $className, object $instance, bool $shouldDispose = true): static;
}Note
There is no addTransientInstance since, by definition, instances must be unique
per recipient.
Tip
If the default value of $shouldDispose: true does not need to be changed
the shorthand forms can be used.
// equivalent to $builder->addSingletonInstance(MyService::class, $obj);
$builder->addSingleton(MyService::class, $obj);$request = new Request($_SERVER, $_GET, $_POST, $_COOKIE);
$builder->addSingletonInstance(Request::class, $request);Anytime your application requires a Request object, the container will provide
the exact same instance that was passed in with the $request variable.
An application sometimes needs to provide the same type in more than one
configuration. For example, you might want a separate Settings object for
different areas of your application. Keyed services let you add multiple
factories for a class under distinct keys and then retrieve or inject a specific
one. Keys can be strings or enum values. To help ease analysis and future
refactorings, enums or string-typed constants are recommended.
A keyed service is resolved only by its exact key. If the container has no
service under the requested key, it will throw a ClassNotFoundException
rather than falling back to the unkeyed service. A class may have both an
unkeyed service and any number of keyed services; they are independent
of one another.
class ContainerBuilder
{
/**
* @template TClass of object
* @template TImplementation of TClass
* @param class-string<TClass> $className
* @param class-string<TImplementation>|(Closure(mixed...): TClass)|TClass|null $source
* @return $this
*/
public function addKeyedSingleton(
string $className,
string|UnitEnum $key,
string|object|null $source = null
): static;
public function addKeyedScoped(
string $className,
string|UnitEnum $key,
string|Closure|null $source = null
): static;
public function addKeyedTransient(
string $className,
string|UnitEnum $key,
string|Closure|null $source = null
): static;
}
class Container
{
/**
* @template TClass of object
* @param class-string<TClass> $className
* @return TClass
*/
public function get(string $className, string|UnitEnum|null $key = null): object;
public function has(string $className, string|UnitEnum|null $key = null): bool;
}The $source parameter determines how the container provides the instance:
- If
null, the container injects the class's constructor dependencies. - If a class name, the container maps the class to that implementation, which must also be added to the container.
- If a closure, the container calls it as a factory, injecting its parameters.
- If any other object, the container provides that object directly.
Each explicit add* variant also has a keyed counterpart:
addKeyedSingletonFactory(), addKeyedTransientClass(),
addKeyedScopedImplementation(), and so on.
$container = $builder
->addSingletonFactory(
Settings::class,
fn () => JsonSettings::fromFile('default.json')
)
->addKeyedSingleton(
Settings::class,
'admin',
fn () => JsonSettings::fromFile('admin.json')
)
->build();
// Resolves the unkeyed Settings service.
$settings = $container->get(Settings::class);
// Resolves the Settings service under the 'admin' key.
$adminSettings = $container->get(Settings::class, 'admin');Apply the Key attribute to a constructor parameter to inject the service added
under a specific key. As with get(), the lookup is absolute: if the container
has no service under the key, it will throw a ParameterResolutionException.
use Suhock\DependencyInjection\Key;
class AdminController
{
public function __construct(
/*
* Resolved from the unkeyed Settings service.
*/
private readonly Settings $settings,
/*
* Resolved from the Settings service under the 'admin' key.
*/
#[Key('admin')]
private readonly Settings $adminSettings
) {
}
}The library also provides a dependency injector, Injector that can be used for
directly calling constructors and functions, injecting any dependencies from a
container. The injector also lets you directly inject specific values for named
or indexed parameters.
class Injector
{
/**
* @template TResult
* @param callable(mixed...): TResult $function
* @param array<int|string, mixed> $params
* @return TResult
*/
public function call(callable $function, array $params = []): mixed;
/**
* @template TClass of object
* @param class-string<TClass> $className
* @param array<int|string, mixed> $params
* @return TClass
*/
public function instantiate(string $className, array $params = []): object;
}The following is an example where dependencies need to be injected into a function in a controller instead of the constructor.
use Suhock\DependencyInjection\ContainerBuilder;
use Suhock\DependencyInjection\Injector;
// Configure and build the container
$container = ContainerBuilder::createDefault()
// ... add services ...
->build();
// Create an injector backed by the built container
$injector = Injector::createDefault($container);
// Fetch the application router from the container
$router = $container->get(Router::class);
// Get the appropriate controller from the request path
$controller = $router->getControllerFromRequest($_SERVER);
// Call the controller's handleGet() method, injecting the indicated parameter
// values in addition to any additional dependencies in the parameter list.
$page = $injector
->call(
$controller->handleGet(...),
map_query_to_assoc_param_array($_GET)
)
// Then, call the render() function on the return value.
$page->render();
class ProjectListController
{
public function handleGet(
// Parameter below will be injected from the container.
ProjectRepository $projectRepository,
// Parameter below will be populated from the value provided in the
// $injector->call() parameter array. The default value will be used if
// the key 'filter' is not present in the array.
string $filter = ''
): PageInterface {
$projects = $projectRepository->query($filter);
return new ProjectListPage($projects);
}
}
interface PageInterface {
public function render(): void;
}A function specifies its dependencies by listing them in its parameter list. A class specifies its dependencies by listing them in the parameter list of its constructor. Dependencies must be specified as either named object types, union types, or intersection types.
If a dependency is specified as a named object type, the container will only provide a value if it can resolve a factory for that type.
class MyApplication
{
public function __construct(
private readonly HttpClient $httpClient
) {
}
}In the example above, the container will attempt to resolve an instance of
HttpClient. If it cannot resolve HttpClient it will throw an
ParameterResolutionException.
If the container cannot resolve a dependency, but the dependency is nullable, then the container will provide a null value.
class MyApplication
{
public function __construct(
private readonly ?HttpClient $httpClient
) {
}
}In the example above, the container will attempt to resolve an instance of
HttpClient. If it cannot resolve HttpClient it will inject a null value
instead.
The container is not able to resolve builtin types. However, if the function or class takes a builtin type and that parameter specifies a default value, the default value will be used.
class MyApplication
{
public function __construct(
private readonly HttpClient $httpClient,
private readonly string $homeUrl = '',
private readonly int $timeout = 0,
private readonly array $otherOptions = []
) {
}
}In the example above, although the container cannot resolve string, int, or
array types, it will construct the class using the specified default
values. If you need to inject non-default values for builtin types, use a
factory callback.
If a dependency is specified as a union type, the container will search sequentially through all named object types in the union list. It will provide a value using the first type it is able to resolve. Builtin types are ignored.
class MyApplication
{
public function __construct(
private readonly HttpClient|GopherClient|string $client
) {
}
}In the example above, the container will attempt to resolve an instance of
HttpClient first. If it cannot resolve HttpClient, it will attempt to
resolve an instance of GopherClient. If it cannot resolve GopherClient, it
will ignore string and then throw an ParameterResolutionException.
If a dependency is specified as an intersection type, the container will attempt to fetch an instance of each type in the list until it finds one that satisfies all the types in the list. Since an instance must be retrieved in order to test whether it is a match, the use of intersection types may be slow and could have unintended consequences if the construction of any non-matching instances have side effects.
class MyApplication
{
public function __construct(
private readonly HttpClient&Serializable $httpClient
) {
}
}In the example above, the container will first attempt to resolve an instance of
HttpClient. If it succeeds, it will check whether that instance is also
Serializable and, if so, provide it. Otherwise, it will attempt to resolve
Serializable and check whether that instance is also an HttpClient. If
neither candidate satisfies both types, it will throw a
ParameterResolutionException.
Every exception the library throws implements
Suhock\DependencyInjection\DependencyInjectionExceptionInterface, so a single
catch block can handle any failure originating from the container or injector.
use Suhock\DependencyInjection\DependencyInjectionExceptionInterface;
try {
$app = $container->get(MyApplication::class);
} catch (DependencyInjectionExceptionInterface $e) {
// Handle any dependency injection failure.
}These exceptions all extend RuntimeException: they signal a container that
was misconfigured or misused, whether the problem shows up at build time or at
run time, not a bug inside the library itself.
The base class is DependencyInjectionException. Notable subclasses include:
Validation\ContainerValidationException: thrown byContainerBuilder::build(), aggregating every guaranteed-failure configuration defect (see Building the container). Unlike the rest of this list, it is a build-time error: fix the configuration and callbuild()again.ClassNotFoundException: no service is registered for the requested class.CircularDependencyException: a dependency cycle was detected. Cycles through ordinary descriptors are caught at build time as aContainerValidationException, so at run time this means the cycle passed through a factory body.ScopeException: a scoped service was requested with no active scope, or a disposed scope was used.ImplementationException: a mapped implementation is not a subtype of the class it is mapped to.ParameterResolutionException/PropertyResolutionException: the injector could not resolve a parameter or property.
When dependency-injection exceptions are chained through a resolution graph,
they are consolidated into a single message; the original exception remains
available via getConsolidatedException(). ContainerValidationException
does not chain a previous exception; its issue list carries every problem
found instead.
To resolve dependencies, the container and injector reflect over constructor and method signatures. This reflected metadata can be memoized so it survives between requests instead of being recomputed each time.
ContainerBuilder::createDefault() and Injector::createDefault() each accept
an optional cache:
ContainerBuilder::createDefault(?CacheInterface $cache = null): self
Injector::createDefault(ContainerInterface $container, ?CacheInterface $cache = null): selfThe same CacheInterface instance backs two independent things: the reflected
metadata memoized here, and build()'s compiled-graph reuse described in
Build performance; supplying one cache to
ContainerBuilder::createDefault() gets you both.
Suhock\DependencyInjection\Cache\CacheInterface is a minimal key/value store
with two methods:
interface CacheInterface
{
public function tryGet(string $id, mixed &$value): bool;
public function set(string $id, mixed $value): void;
}tryGet() returns whether the id was present and populates $value by
reference. Reporting presence through the return value means a stored null or
false is not mistaken for a miss.
Suhock\DependencyInjection\Cache\ApcuCache implements CacheInterface using
the APCu extension. Its entries live in shared memory and persist across
requests served by the same worker pool. The constructor throws a
RuntimeException if the apcu extension is not loaded and enabled (on the
CLI, apc.enable_cli must be set), and it requires the optional ext-apcu
extension.
use Suhock\DependencyInjection\Cache\ApcuCache;
use Suhock\DependencyInjection\ContainerBuilder;
$builder = ContainerBuilder::createDefault(new ApcuCache());Consumers can also implement CacheInterface themselves to back the cache with
another store.
This library's get() takes a class name and optional key rather than PSR-11's
opaque string id, so Container does not implement Psr\Container\ContainerInterface
directly. For frameworks that expect a PSR-11 container, the
suhock/dependency-injection-psr11
package provides a thin adapter that wraps the container and translates its
exceptions into their PSR-11 counterparts.
{
"require": {
"suhock/dependency-injection-psr11": "^1.0"
}
}The suhock/dependency-injection-phpstan package provides
PHPStan extensions that provide analyses relvant to this
library. Add it to require-dev in projects that use these attributes.
{
"require-dev": {
"suhock/dependency-injection-phpstan": "^1.0"
}
}