sanmai/di-container
Lightweight PSR-11 compatible dependency injection container for PHP. Supports autowiring via reflection, bindings and shared services, simple configuration, and fast resolution with minimal boilerplate—suitable for small apps and libraries needing a straightforward DI container.
Start by installing the package via Composer:
composer require sanmai/di-container
The core class is Sanmai\DI\Container. Instantiate it, register services via closures (factories), then resolve them:
use Sanmai\DI\Container;
$container = new Container();
// Register a singleton
$container->register(PDO::class, fn () => new PDO('sqlite::memory:'));
// Register a non-shared (transient) service
$container->register(LoggerInterface::class, fn ($c) => new StdOutLogger());
// Resolve a dependency (dependencies are auto-injected if type-hinted)
$logger = $container->resolve(LoggerInterface::class);
First use case: bootstrapping a simple application where services are defined in one place (e.g., bootstrap/app.php), then resolved on demand in controllers, commands, or services.
Configuration-driven registration: Group related services into service providers or static methods:
class DatabaseServiceProviders {
public static function register(Container $container): void {
$container->register(PDO::class, fn () => new PDO($_ENV['DB_DSN']));
$container->register(EntityManager::class, fn ($c) => new EntityManager($c->get(PDO::class)));
}
}
DatabaseServiceProviders::register($container);
Lazy-loading expensive services: Wrap factories in lazy() to defer instantiation:
$container->register('db.connection', fn () => lazy(fn () => new PDO(...)));
// Only instantiated on first access
Interface-based wiring: Prefer registering services as interfaces (e.g., CacheInterface) and resolving concrete implementations — great for swapping implementations (e.g., Redis vs. array cache) or testing.
Integration with PSR-11: Container implements Psr\Container\ContainerInterface, enabling compatibility with PSR-11-consuming libraries (e.g., nyholm/psr7, symfony/http-kernel adapters).
Nested containers: For modular apps or test fixtures, compose containers:
$base = new Container();
$base->register(...);
$testContainer = new Container($base);
$testContainer->override(SessionInterface::class, MockSession::class);
Circular dependencies cause recursion: If A depends on B and B depends on A, resolution will blow the stack. Diagnose via stack traces; refactor to constructor injection with deferred dependencies (e.g., callable or factory arguments).
get() vs resolve(): Both resolve services, but resolve() supports type-hinted autowiring for closure arguments (e.g., fn (PDO $pdo) => new Repository($pdo)), while get() requires explicit class names or IDs.
Shared services must be explicitly declared: By default, services are transient. Use share() to make them singletons:
$container->share(PDO::class, fn () => new PDO('...'));
Overriding registrations: Use override() (non-reentrant) or replace() for test doubles (e.g., mocking services):
$container->replace(PDO::class, fn () => new MockPDO());
IDE autocompletion: For better UX, cast container type hints in your codebase or use Container::with() to build typed sub-containers.
No built-in config layer: You must inject config arrays yourself — e.g., fn () => new Service(config('my.service')). This keeps it minimal but requires you to define a config loader separately (e.g., using symfony/dependency-injection’s config parser or custom loader).
How can I help you explore Laravel packages today?