Skip to content

Latest commit

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

ordinary/container

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.


Installation

composer require ordinary/container

Quick Start

useOrdinary\Container\ContainerBuilder;
$container = (newContainerBuilder())
->singleton(Database::class, fn($c) => newDatabase($c->get(Config::class)))
->singleton(Config::class, fn($c) => newConfig('/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.


Binding Services

Singleton — one shared instance

$builder->singleton(Cache::class, fn($c) => newRedisCache($c->get(Redis::class)));

Transient — new instance on every call

$builder->transient(QueryBuilder::class, fn($c) => newQueryBuilder($c->get(Database::class)));

Scoped — one instance per named scope

$builder->scoped(RequestContext::class, fn($c) => newRequestContext());
// 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 scope

Scoped services throw ContainerException when resolved outside a scope. Always call scope() first.

Lazy Singleton — deferred via PHP 8.4 native lazy proxies

$builder->lazySingleton(ExpensiveService::class, fn($c) => newExpensiveService());
// 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.

Choosing a scope

ScopeUse when
SingletonStateless services, shared connections, caches — created once and reused for the lifetime of the container.
TransientStateful value objects, builders, or anything that must not share state between callers.
ScopedServices 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().

Aliases

$builder->singleton(RedisCache::class, fn($c) => newRedisCache());
$builder->alias(CacheInterface::class, RedisCache::class);
$cache = $container->get(CacheInterface::class); // resolves to RedisCache

Autowiring

Register 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]);

Zero-config autowiring

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);

#[Inject] — override a specific constructor parameter

useOrdinary\Container\Attribute\Inject;
class ReportMailer
{
publicfunction__construct(
#[Inject(S3Storage::class)] privateStorageInterface$storage,
) {}
}
$builder->autowire(ReportMailer::class);

#[Singleton] / #[Transient] — attribute-driven scope

useOrdinary\Container\Attribute\Singleton;
#[Singleton]
class SessionManager { ... }
$builder->autowireFromAttribute(SessionManager::class);
// Defaults to Scope::Singleton when neither attribute is present.

Autowiring limitations

  • Union and intersection types — parameters typed CacheInterface|NullCache are skipped. Supply them via $overrides or #[Inject].
  • Built-in type parametersstring, int, array, etc. require either a default value, an explicit $overrides entry, or #[Inject]. The container cannot resolve scalar values by type.
  • Nullable built-in with no default — resolved to null.

Calling Callables with Injection

Invoke any PHP callable with parameters resolved from the container:

$result = $container->call(staticfunction (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(
staticfunction (#[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.


Tagging

$builder->singleton(JsonSerializer::class, fn($c) => newJsonSerializer(), 'serializer');
$builder->singleton(MsgpackSerializer::class, fn($c) => newMsgpackSerializer(), 'serializer');
foreach ($container->tagged('serializer') as$serializer) {
// ...
}

Or tag after the fact:

$builder->singleton(JsonSerializer::class, fn($c) => newJsonSerializer());
$builder->tag(JsonSerializer::class, 'serializer');

Contextual Bindings

Inject a different implementation depending on which class is requesting it:

$builder->singleton(S3Storage::class, fn($c) => newS3Storage());
$builder->singleton(LocalStorage::class, fn($c) => newLocalStorage());
$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.


Service Providers

Group related bindings into a reusable provider:

useOrdinary\Container\ServiceProviderInterface;
useOrdinary\Container\ContainerBuilder;
class StorageServiceProvider implements ServiceProviderInterface
{
publicfunctionregister(ContainerBuilder$builder): void
{
$builder->singleton(S3Storage::class, fn($c) => newS3Storage());
$builder->alias(StorageInterface::class, S3Storage::class);
}
}
$builder->provider(newStorageServiceProvider());

Circular Dependency Detection

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.


PSR-11 Compliance

Container implements Psr\Container\ContainerInterface. It is compatible with any framework or library that accepts a PSR-11 container.

usePsr\Container\ContainerInterface;
functionbootstrap(ContainerInterface$c): void
{
$app = $c->get(Application::class);
$app->run();
}
bootstrap($container);

License

MIT

About

PHP 8.5 dependency injection container with lazy loading, autowiring, and PSR-11 compliance.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages