Container
Introduction
The dependency injection (DI) container manages class dependencies and performs dependency injection. It's the heart of Lighthouse's architecture.
Why Dependency Injection?
Without DI:
php
class UserController
{
public function __construct()
{
$this->db = new Database('localhost', 'root', 'secret');
$this->logger = new FileLogger('/var/log/app.log');
$this->mailer = new SmtpMailer('smtp.example.com');
}
}Problems:
- Hard to test (can't mock dependencies)
- Tight coupling (controller knows how to create everything)
- Hard to change (switching loggers requires editing many files)
With DI:
php
class UserController
{
public function __construct(
private Database $db,
private Logger $logger,
private Mailer $mailer
) {}
}The container creates and injects dependencies automatically.
Accessing the Container
php
$container = $app->getContainer();Binding Services
Simple Bindings
php
$container->bind(Logger::class, FileLogger::class);Every time Logger is requested, a new FileLogger is created.
Singletons
php
$container->singleton(Database::class, function ($container) {
return new Database(
host: 'localhost',
user: 'root',
pass: 'secret'
);
});The same instance is returned every time.
Instances
php
$logger = new FileLogger('/var/log/app.log');
$container->instance(Logger::class, $logger);Bind an existing instance.
Interface to Implementation
php
$container->bind(LoggerInterface::class, FileLogger::class);
$container->bind(MailerInterface::class, SmtpMailer::class);Now you can type-hint interfaces:
php
class UserController
{
public function __construct(
private LoggerInterface $logger // Gets FileLogger
) {}
}Resolving Services
By Class Name
php
$logger = $container->get(FileLogger::class);With Auto-Wiring
The container automatically resolves dependencies:
php
class UserService
{
public function __construct(
private Database $db,
private Logger $logger
) {}
}
// Container creates Database, Logger, then UserService
$service = $container->get(UserService::class);Check if Bound
php
if ($container->has(Logger::class)) {
$logger = $container->get(Logger::class);
}Factory Closures
For complex instantiation:
php
$container->bind(Mailer::class, function ($container) {
$config = $container->get(Config::class);
if ($config->get('mail.driver') === 'smtp') {
return new SmtpMailer(
$config->get('mail.host'),
$config->get('mail.port')
);
}
return new ArrayMailer();
});Method Injection
Call methods with automatic injection:
php
$container->call($controller, 'index', [
'request' => $request,
]);The container injects:
- Explicitly provided parameters (
$request) - Type-hinted dependencies from the container
- Remaining parameters by name
Practical Example
php
// config/container.php
use Lighthouse\Container\Container;
return function (Container $container) {
// Database - singleton (one connection)
$container->singleton(Database::class, function () {
return new Database(
$_ENV['DB_HOST'],
$_ENV['DB_USER'],
$_ENV['DB_PASS']
);
});
// Logger - singleton
$container->singleton(LoggerInterface::class, function () {
return new FileLogger(__DIR__ . '/../logs/app.log');
});
// Mailer - new instance each time
$container->bind(MailerInterface::class, SmtpMailer::class);
// Repositories
$container->bind(UserRepository::class, DatabaseUserRepository::class);
};Load in index.php:
php
$configure = require __DIR__ . '/../config/container.php';
$configure($app->getContainer());Testing
Swap implementations for testing:
php
// In tests
$container->instance(MailerInterface::class, new FakeMailer());
$container->instance(Database::class, new InMemoryDatabase());Your code doesn't change, but now uses test doubles.
PSR-11 Compliance
Lighthouse's container implements PSR-11:
php
use Psr\Container\ContainerInterface;
function doSomething(ContainerInterface $container)
{
$logger = $container->get(LoggerInterface::class);
}This means Lighthouse services work with any PSR-11 compatible library.