17.10.2026

PHP Dependency Injection Container: PSR-11, Autowiring and Zero Config

A small, PSR-11 compatible dependency injection container for PHP 8.1+. It offers autowiring, singletons, interface binding, tags and lazy service providers, and installs with one Composer command.

PHP Dependency Injection Container: PSR-11, Autowiring and Zero Config

Every PHP application grows dependencies: a service needs a logger, the logger needs a config value, the config needs a path. Wiring all of that by hand with new gets messy fast. A dependency injection (DI) container solves this by building your objects and their dependencies for you.

I built my own PSR-11 compatible PHP dependency injection container. It is small and strict, and it is focused on modern PHP 8.1+ code. It is open source and available on GitHub: Kamarbandi/di-container

What Is a Dependency Injection Container?

Dependency injection means that a class receives the objects it needs instead of creating them itself. A container is the tool that:

  1. knows how to create each service,

  2. resolves its dependencies recursively,

  3. decides whether to reuse an instance or create a new one.

The result is loosely coupled code that is easier to test, replace and maintain.

Why Another PHP DI Container?

Mature containers such as PHP-DI or Symfony's DependencyInjection component already exist. I wrote this one for three reasons:

  • Learning. Building a container teaches you reflection, autowiring and object lifecycles.

  • Minimal footprint. The only runtime dependency is psr/container.

  • A deliberate design. It supports constructor injection only. Setter and property injection are intentionally left out, and the library is built around readonly classes.

Installation

Requirements: PHP 8.1 or later.

composer require azad/container

Quick Start: Autowiring

For plain classes you don't need any configuration. The container reads constructor type hints and resolves them automatically:

$container = new Container();
$service = $container->get(UserService::class); // Logger is resolved automatically

Key Features

PSR-11 Compatible

The container implements Psr\Container, so it works anywhere a PSR-11 container is expected. All exceptions implement the PSR-11 exception interfaces.

Registering Services

Raw values and closure factories:

$container->set('project.root', '/opt/app');
$container->set(Logger::class, fn (Container $c) => new Logger($c->get('project.root')));

Shared Instances vs. Fresh Instances

Choose a scope explicitly:

$container->singleton(Clock::class);
$container->factory(RequestId::class, fn () => new RequestId(Uuid::v4()));

singleton() caches the instance. factory() builds a new one on every lookup.

Binding Interfaces to Implementations

$container->bind(LoggerInterface::class, FileLogger::class);

Interfaces can't be guessed, so they need an explicit bind(). You can also create a readable alias:

$container->alias('logger', LoggerInterface::class);

Overriding Constructor Parameters

Override a single named parameter:

$container->define(LoggerFactory::class)->parameter('targetDir', '/var/log/production');

Or use the #[Inject] attribute to inject scalars and config values, for example #[Inject('config.log_dir')] on a constructor parameter.

Decorating Services

Wrap an existing entry without touching the original class:

$container->extend(LoggerInterface::class, fn (LoggerInterface $logger) => new BufferedLogger($logger));

Building Fresh Objects with make()

$container->make(Report::class, ['year' => 2025]);

Tags

Group related services and loop over them:

$container->tag(ProcessInventories::class, ['processing.routine']);

foreach ($container->tagged('processing.routine') as $routine) {
    // ...
}

Lazy Service Providers

Providers implement ServiceProviderInterface and are passed to the constructor:

$container = new Container(providers: [new TwigServiceProvider()]);

A provider's register() method runs only the first time one of its entries is requested, so unused services cost nothing. A provider with an empty provides() array is registered eagerly, which suits providers with side effects such as tagging.

Delegate Lookup

If an entry isn't found locally, the container can fall back to another one:

$container->delegate($frameworkContainer);

This makes it easy to combine the container with a framework's own container.

Circular Dependency Detection

If class A needs B and B needs A, the container fails with a clear CircularDependencyException that shows the full resolution chain, so you can find the problem quickly.

Autowiring Rules at a Glance

  • Interfaces need an explicit bind().

  • Scalar parameters with defaults use the default value.

  • Nullable parameters that can't be resolved receive null.

  • Union types are tried in declaration order.

  • Variadic parameters default to an empty list.

  • Untyped parameters resolve only if they have defaults.

  • Intersection types are not autowired and raise UnresolvableParameterException.

Clear, Explicit Exceptions

Exception

When it happens

NotFoundException

The requested entry doesn't exist

CircularDependencyException

Dependencies form a loop

UnresolvableParameterException

A constructor parameter can't be resolved

FrozenServiceException

A service is changed after it was frozen

InvalidDefinitionException

A definition is invalid

ContainerException

General container error

Code Quality

The library ships with a PHPUnit test suite. The composer test script runs composer validate, PHP_CodeSniffer, PHPStan at level 8 and PHPUnit, so every change is checked for style, types and behavior.

Who Is It For?

  • Developers who want a small, strict PSR-11 container without a large dependency tree.

  • Projects that follow constructor injection and modern PHP 8.1+ practices.

  • Anyone who wants to learn how autowiring works by reading a compact codebase.

Frequently Asked Questions

What PHP version do I need?
PHP 8.1 or later.

Is it PSR-11 compatible?
Yes. It implements Psr\Container, and its exceptions implement the PSR-11 exception interfaces.

Does it support setter or property injection?
No. Only constructor injection is supported, by design.

How do I inject a config value?
Register the value with $container->set('config.log_dir', ...) and use the #[Inject('config.log_dir')] attribute on the constructor parameter.

What license does it use?
MIT.

Conclusion

A dependency injection container takes the boilerplate out of object creation and keeps your code decoupled and testable. This one is small and PSR-11 compatible, and it covers autowiring, scopes, tags, providers and delegate lookup.

👉 Get the source code on GitHub: github.com/Kamarbandi/di-container

If it's useful to you, leave a star and open an issue with your ideas.

Mehr aus der Praxis?

Schauen Sie sich auch meine Leistungen an — oder schreiben Sie mir direkt.

Kostenloses Erstgespräch Preise ansehen