A lightweight, composable IoC/dependency injection container for PHP, usable on its own or as part of the Outboard Framework.
This is the outboardphp/di package from the Outboard monorepo. It can be used standalone or as part of the full framework.
- Respect SOLID principles, especially SRP, more than most other DI libraries
- Use a minimum of "magic" so that it is straightforward to understand
- Be powerful, flexible, and feature-rich, yet also fast and efficient
- Support a modular/layered architecture, allowing for simplified configuration by multiple packages
- Build on the work of Level-2/Dice, updating it for modern PHP and adding a few features from other libraries
Pre-alpha. Stable enough to experiment with, but APIs may still evolve. Feel free to try it and report bugs.
Implemented features:
- Service retrieval and factory-style creation (
get()/make()) - Shared and non-shared lifecycles (
shared, prototype-by-default) - Constructor parameter overrides (
withParams()by name or position) - Callable invocation with dependency resolution (
call()) - Optional autowiring via resolver strategy (
AutowiringResolver) - Definition inheritance across class/interface hierarchies (
strictto opt out) - Parent-container fallback for dependency resolution (
setParent())
Current missing features (planned):
- Service tagging retrieval (
getTagged()/ tag queries) - Type-based parameter matching in
withParams - Lazy services / lazy proxies
- Runtime circular dependency detection with cycle-path errors
- Parameter resolution for container-wide config values
- Parameterized
make()runtime constructor overrides
- PHP
>=8.4 - Composer
2.x
composer require outboardphp/diPreferred usage is via ContainerFactory with a DefinitionProvider.
You can implement DefinitionProvider in your own way to achieve
a variety of strategies of loading definitions.
<?php
declare(strict_types=1);
use Outboard\Di\ContainerFactory;
use Outboard\Di\Contract\DefinitionProvider;
use Outboard\Di\ValueObject\Definition;
$provider = new class implements DefinitionProvider {
public function getDefinitions(): array
{
return [
PDO::class => new Definition(
withParams: ['dsn' => 'sqlite::memory:'],
shared: true,
),
DateTimeZone::class => new Definition(
withParams: ['timezone' => 'UTC'],
shared: true,
),
'db.healthcheck' => new Definition(
substitute: static fn(PDO $db) => (int) $db->query('SELECT 1')->fetchColumn(),
),
'app.started_at' => new Definition(
substitute: static fn(DateTimeZone $tz) => new DateTimeImmutable('now', $tz),
shared: true,
),
];
}
};
$container = new ContainerFactory($provider)->build();
$db = $container->get(PDO::class);
var_dump($db instanceof PDO); // trueIf you need lower-level control (for tests or custom bootstrapping), you can still instantiate Container directly with Resolver instances.
Parameters are resolved in this precedence:
- Named keys (
'paramName' => value) always override any other resolution - Single-class-typed parameters (not union or intersection types) are resolved from the container by type
- Remaining parameters consume numeric-keyed
withParamsvalues in order (as a queue) - Default values or errors for anything still unresolved
This means you can supply scalar values without keys, and they'll apply to unresolved (non-class-typed) parameters in the order you provide them.
Example:
class Worker {
public function __construct(
public Database $db, // resolved by typehint
public string $name, // will get 'alice' from queue
public int $retries, // will get 3 from queue
) {}
}
$definitions = [
Worker::class => new Definition(
withParams: ['alice', 3], // numeric queue, no keys needed
),
];If a string value is supplied in withParams, it will try to match container definitions first, then fallback to the literal string if no match is found.
Create a Definition per service id and pass all definitions into Resolver.
$definitions = [
LoggerInterface::class => new Definition(
substitute: FileLogger::class,
shared: true,
),
];
$container = new Container([new Resolver($definitions)]);shared: false(default) -> new instance eachget()call (prototype behavior)shared: true-> same instance reused byget()(singleton behavior)make(string $id)-> always returns a fresh instance
$definitions = [
MyService::class => new Definition(
shared: true,
),
];
$container = new Container([new Resolver($definitions)]);
$a = $container->get(MyService::class); // fresh instance
$b = $container->get(MyService::class); // same instance as $a
$c = $container->make(MyService::class); // different instance than $a or $b
$d = $container->make(MyService::class); // different instance than $a, $b, or $cYou can pass scalar or object constructor parameters by name or queue them by numeric index.
$definitions = [
ReportWriter::class => new Definition(
withParams: [
'path' => '/tmp/report.log',
3, // will apply to the next unresolved parameter
],
),
];A definition can use a callable for custom creation logic. Class typehints on a callable's parameters will be resolved from the container.
$definitions = [
'mailer' => new Definition(
substitute: static fn() => new Mailer('smtp://localhost'),
),
];Run additional setup after construction, or return a replacement object (decoration). These closures will be passed two parameters: the constructed object, and the container.
$definitions = [
Logger::class => new Definition(
call: static fn(Logger $logger) => $logger->withChannel('api'),
),
];call() combines manually supplied args with container-resolved class dependencies.
$result = $container->call(
static fn(stdClass $obj, int $multiplier) => $obj->value * $multiplier,
['multiplier' => 2],
);If you want implicit class resolution, use the AutowiringResolver factory.
use Outboard\Di\AutowiringResolver;
use Outboard\Di\Container;
use Outboard\Di\ValueObject\Definition;
$definitions = [
App\Service\UserService::class => new Definition(
shared: true,
),
];
$container = new Container([AutowiringResolver::create($definitions)]);
$service = $container->get(App\Service\UserService::class);If you want to use autowiring class resolution only to cover gaps in your explicit definitions, you can combine resolvers. Each one will be queried sequentially.
use Outboard\Di\AutowiringResolver;
use Outboard\Di\Container;
use Outboard\Di\Resolver;
use Outboard\Di\ValueObject\Definition;
$container = new Container([
new Resolver($definitions),
AutowiringResolver::create($definitions),
]);This is essentially what ContainerFactory does by default.
Container::get(string $id): mixedContainer::make(string $id): mixedContainer::call(callable $callable, array $args = []): mixedDefinitionproperties:shared,strict,substitute,withParams,call,tags
- Explicit definitions with predictable behavior
- Optional autowiring strategy
- Regex/class/interface-based matching
- Definition inheritance down class/interface hierarchies (
strictto opt-out) - Modular definition composition via providers
- Tag retrieval APIs (for example,
getTagged()) - Lazy proxies
- Runtime cycle-path diagnostics for circular dependencies
- Parameterized runtime overrides in
make()
Issues and pull requests may be submitted to the main monorepo. Commits there will automatically be reflected here after automatic workflows run.
Previously I started to catalogue the details of many DIC libraries in order to lay out my opinions on each and synthesize my favorite parts of all of them into my ideal DIC library. See this repo's wiki.
But now with the advent of GenAI chatbots, I'm letting computers do that research for me so I can spend more time on decision-making and writing code.
The following libraries have aspects I really respect and plan to incorporate here:
- Dice
- Aura.Di
- Auryn / AmPHP Injector
- Caplet
- Capsule DI
- Laminas DI
- Laravel's container
- The PHP League's Container
- Symfony DI
- Unbox
- Yii 3 DI
- The container is a runtime repository; definitions are supplied externally rather than registered imperatively on the container.
- Definitions can be composed from multiple providers for modular package-level configuration.
- Resolver strategies are composable: explicit-definition resolution is the baseline, with optional autowiring resolution.
- Definition IDs can be arbitrary strings, class/interface names, or regex patterns for broad matching rules.
- Class/interface definitions inherit down the type hierarchy by default;
strictdisables inheritance for that definition. - A definition can substitute with a class name, another service ID, a callable factory, or a prebuilt instance.
- Callable substitutes let definitions act as factories for custom object creation flows.
withParamssupports constructor overrides for scalars and objects by parameter name or position.- Autowiring is optional and can be mixed with explicit parameter overrides.
- Services are non-shared by default (prototype-style); enable
sharedfor singleton behavior. get()respects configured sharing/caching, whilemake(string $id)always returns a fresh instance.- The container can invoke any callable via
call(), combining explicit args with container-based dependency resolution. - The container can inject itself when a dependency is type-hinted as the active container type.
- Post-construction
callhooks support initialization and decoration, including replacing the original instance. - Parent-container fallback is supported for delegated dependency lookup in composed container graphs.
Features that I don't intend to implement:
- Explicit setter injection
- Property injection
- A self-binding global container instance