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:
knows how to create each service,
resolves its dependencies recursively,
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
readonlyclasses.
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 |
|---|---|
| The requested entry doesn't exist |
| Dependencies form a loop |
| A constructor parameter can't be resolved |
| A service is changed after it was frozen |
| A definition is invalid |
| 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.