A modern PHP 8.5 dependency injection container featuring native lazy objects (PHP 8.4+), zero-config autowiring, callable injection, contextual bindings, service tagging, scoped lifetimes, and full PSR-11 compliance.
composer require ordinary/containeruse Ordinary\Container\ContainerBuilder;
$container = (new ContainerBuilder())
->singleton(Database::class, fn($c) => new Database($c->get(Config::class)))
->singleton(Config::class, fn($c) => new Config('/etc/app.ini'))
->build();
$db = $container->get(Database::class);Any instantiable class with type-hinted constructor parameters can also be resolved without registration — see Zero-config autowiring.
$builder->singleton(Cache::class, fn($c) => new RedisCache($c->get(Redis::class)));$builder->transient(QueryBuilder::class, fn($c) => new QueryBuilder($c->get(Database::class)));$builder->scoped(RequestContext::class, fn($c) => new RequestContext());
// Create an isolated scope (e.g. per HTTP request in a long-running process)
$requestContainer = $container->scope('request-1');
$ctx = $requestContainer->get(RequestContext::class); // fresh instance for this scope
$ctx2 = $requestContainer->get(RequestContext::class); // same instance within the scopeScoped services throw ContainerException when resolved outside a scope. Always call scope() first.
$builder->lazySingleton(ExpensiveService::class, fn($c) => new ExpensiveService());
// Proxy returned immediately; real instance created only on first property or method access.
// Falls back to eager resolution for final classes and non-class-string ids.| Scope | Use when |
|---|---|
Singleton |
Stateless services, shared connections, caches — created once and reused for the lifetime of the container. |
Transient |
Stateful value objects, builders, or anything that must not share state between callers. |
Scoped |
Services that should be shared within a logical unit of work (e.g. one HTTP request in FrankenPHP or Swoole) but isolated between units. Requires scope(). |
$builder->singleton(RedisCache::class, fn($c) => new RedisCache());
$builder->alias(CacheInterface::class, RedisCache::class);
$cache = $container->get(CacheInterface::class); // resolves to RedisCacheRegister a class without writing a factory — the container resolves constructor parameters by type:
$builder->autowire(Mailer::class); // singleton by default
$builder->autowire(Logger::class, Scope::Transient); // explicit scope
$builder->autowire(Redis::class, overrides: ['host' => 'localhost', 'port' => 6379]);Autowiring is enabled by default. Any instantiable class can be resolved without explicit registration:
// No registration needed — resolved automatically if the constructor can be wired
$svc = $container->get(SimpleService::class);Auto-resolved classes are cached as singletons and shared across child scopes. To require explicit registration for every service:
$builder->useAutowiring(false);use Ordinary\Container\Attribute\Inject;
class ReportMailer
{
public function __construct(
#[Inject(S3Storage::class)] private StorageInterface $storage,
) {}
}
$builder->autowire(ReportMailer::class);use Ordinary\Container\Attribute\Singleton;
#[Singleton]
class SessionManager { ... }
$builder->autowireFromAttribute(SessionManager::class);
// Defaults to Scope::Singleton when neither attribute is present.- Union and intersection types — parameters typed
CacheInterface|NullCacheare skipped. Supply them via$overridesor#[Inject]. - Built-in type parameters —
string,int,array, etc. require either a default value, an explicit$overridesentry, or#[Inject]. The container cannot resolve scalar values by type. - Nullable built-in with no default — resolved to
null.
Invoke any PHP callable with parameters resolved from the container:
$result = $container->call(static function (Mailer $mailer, Logger $logger): string {
return $mailer->send('Hello');
});Pass named overrides for parameters that cannot be resolved by type:
$container->call([$handler, 'process'], parameters: ['timeout' => 30]);The #[Inject] attribute is honoured on closure and function parameters:
$container->call(
static function (#[Inject(S3Storage::class)] StorageInterface $storage): void {
$storage->store('payload');
},
);Any PHP callable is accepted: closures, named functions, [$object, 'method'] arrays, 'Class::method' strings, and invokable objects.
$builder->singleton(JsonSerializer::class, fn($c) => new JsonSerializer(), 'serializer');
$builder->singleton(MsgpackSerializer::class, fn($c) => new MsgpackSerializer(), 'serializer');
foreach ($container->tagged('serializer') as $serializer) {
// ...
}Or tag after the fact:
$builder->singleton(JsonSerializer::class, fn($c) => new JsonSerializer());
$builder->tag(JsonSerializer::class, 'serializer');Inject a different implementation depending on which class is requesting it:
$builder->singleton(S3Storage::class, fn($c) => new S3Storage());
$builder->singleton(LocalStorage::class, fn($c) => new LocalStorage());
$builder->alias(StorageInterface::class, LocalStorage::class); // default
$builder->when(ReportController::class)
->needs(StorageInterface::class)
->give(S3Storage::class);Works for both autowired and manually-factored services.
Group related bindings into a reusable provider:
use Ordinary\Container\ServiceProviderInterface;
use Ordinary\Container\ContainerBuilder;
class StorageServiceProvider implements ServiceProviderInterface
{
public function register(ContainerBuilder $builder): void
{
$builder->singleton(S3Storage::class, fn($c) => new S3Storage());
$builder->alias(StorageInterface::class, S3Storage::class);
}
}
$builder->provider(new StorageServiceProvider());The container throws CircularDependencyException when a dependency cycle is detected, including through auto-wired classes:
// A depends on B, B depends on A → throws CircularDependencyException with full chain
$container->get(ServiceA::class);The exception exposes the full chain via $e->chain() for diagnostics.
Container implements Psr\Container\ContainerInterface. It is compatible with any framework or library that accepts a PSR-11 container.
use Psr\Container\ContainerInterface;
function bootstrap(ContainerInterface $c): void
{
$app = $c->get(Application::class);
$app->run();
}
bootstrap($container);MIT