Skip to content

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:

  1. Explicitly provided parameters ($request)
  2. Type-hinted dependencies from the container
  3. 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.

Released under the MIT License.