PHP Dependency Injection with PSR-11

Modern PHP development emphasizes modularity, testability, and maintainability. Dependency Injection (DI) is a fundamental design pattern that addresses these concerns by allowing the dependencies of a class to be injected from the outside, rather than being created internally. This approach drastically improves code flexibility and simplifies unit testing. Coupled with standardized interfaces like PSR-11, DI becomes even more powerful, fostering interoperability across different frameworks and libraries. This post will explore the core concepts of Dependency Injection, the role of Service Containers, and how PSR-11 provides a universal interface for managing dependencies in your PHP applications.

Understanding Dependency Injection

At its heart, Dependency Injection is about inverting control over how a class obtains its collaborators (dependencies). Instead of a class being responsible for creating its own instances of other classes it needs, those instances are "injected" into it. This can happen through the constructor (constructor injection), setter methods (setter injection), or interface methods (interface injection).

Consider a simple Mailer class that needs a Logger:

Without Dependency Injection

<?php

class Mailer
{
    private $logger;

    public function __construct()
    {
        $this->logger = new FileLogger('/var/log/mailer.log');
    }

    public function sendEmail(string $to, string $subject, string $body)
    {
        // ... send email logic ...
        $this->logger->log('Email sent to ' . $to);
    }
}

// Usage
$mailer = new Mailer();
$mailer->sendEmail('[email protected]', 'Hello', 'Test email');

In this example, Mailer is tightly coupled to FileLogger. If you wanted to switch to a DatabaseLogger or a NullLogger for testing, you'd have to modify the Mailer class itself.

With Dependency Injection (Constructor Injection)

<?php

interface LoggerInterface
{
    public function log(string $message);
}

class FileLogger implements LoggerInterface
{
    private $filePath;

    public function __construct(string $filePath)
    {
        $this->filePath = $filePath;
    }

    public function log(string $message)
    {
        file_put_contents($this->filePath, $message . PHP_EOL, FILE_APPEND);
    }
}

class Mailer
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function sendEmail(string $to, string $subject, string $body)
    {
        // ... send email logic ...
        $this->logger->log('Email sent to ' . $to);
    }
}

// Usage
$logger = new FileLogger('/var/log/app.log');
$mailer = new Mailer($logger);
$mailer->sendEmail('[email protected]', 'Hello', 'Test email');

Here, Mailer depends on an abstraction (LoggerInterface), and the concrete Logger implementation is passed in. This makes Mailer reusable and testable with different Logger implementations without modification.

The Role of Service Containers

While manual dependency injection works for small applications, managing a large number of dependencies and their creation can become cumbersome. This is where a Service Container (also known as a Dependency Injection Container or DIC) comes into play. A Service Container is an object that knows how to instantiate and configure objects and their dependencies.

Instead of manually instantiating every dependency, you register them with the container. When you need an object, you ask the container for it, and it handles the creation and injection of all necessary dependencies.

Key benefits of Service Containers:

  • Centralized Configuration: Define how objects are created and configured in one place.
  • Automatic Dependency Resolution: The container automatically resolves and injects dependencies.
  • Singleton Management: Easily manage shared instances of objects.
  • Lazy Loading: Objects are only instantiated when they are actually needed.
  • Testability: Simplifies swapping out dependencies for mocks or stubs during testing.

Popular PHP Service Containers include Symfony's DependencyInjection component, PHP-DI, and Laravel's Service Container.

PSR-11: Container Interface

Prior to PSR-11, every Dependency Injection Container had its own way of defining and retrieving services, leading to vendor lock-in and difficult-to-reuse code. PSR-11, published by the PHP Framework Interoperability Group (PHP-FIG), standardizes the interface for containers, providing two simple interfaces:

  • Psr\Container\ContainerInterface
  • Psr\Container\NotFoundExceptionInterface

ContainerInterface

The ContainerInterface defines two methods:

  • get(string $id): Finds an entry of the container by its identifier and returns it. If the identifier is not found, a NotFoundExceptionInterface is thrown.
  • has(string $id): Returns true if the container can return an entry for the given identifier; false otherwise.
<?php

namespace Psr\Container;

interface ContainerInterface
{
    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @param string $id Identifier of the entry to look for.
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
     * @throws ContainerExceptionInterface Error while retrieving the entry.
     * @return mixed Entry.
     */
    public function get(string $id);

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string $id Identifier of the entry to look for.
     * @return bool
     */
    public function has(string $id);
}

NotFoundExceptionInterface

This interface simply extends Psr\Container\ContainerExceptionInterface (which itself extends Throwable) and signifies that the requested service was not found in the container.

By adhering to PSR-11, different DI containers can be swapped out without affecting the application code that interacts with them. This promotes a truly decoupled architecture.

Building a Simple PSR-11 Compliant Container

Let's create a very basic container that adheres to the PSR-11 standard.

First, you'll need to install the PSR-11 interfaces via Composer:

composer require psr/container

Now, implement a simple container:

<?php

use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

class SimpleContainerException extends \Exception implements \Psr\Container\ContainerExceptionInterface {}
class SimpleNotFoundException extends SimpleContainerException implements NotFoundExceptionInterface {}

class SimpleContainer implements ContainerInterface
{
    private array $definitions = [];
    private array $instances = [];

    public function set(string $id, callable $definition):
    {
        $this->definitions[$id] = $definition;
    }

    public function get(string $id)
    {
        if (!$this->has($id)) {
            throw new SimpleNotFoundException(sprintf('Service with ID "%s" not found.', $id));
        }

        if (!isset($this->instances[$id])) {
            $this->instances[$id] = $this->definitions[$id]($this);
        }

        return $this->instances[$id];
    }

    public function has(string $id): bool
    {
        return isset($this->definitions[$id]);
    }
}

// Define some services
$container = new SimpleContainer();

$container->set('logger', function ($c) {
    return new FileLogger('/tmp/app.log');
});

$container->set('mailer', function ($c) {
    return new Mailer($c->get('logger'));
});

// Retrieve and use services
$mailer = $container->get('mailer');
$mailer->sendEmail('[email protected]', 'Hello!', 'This is a test email from a PSR-11 container.');

try {
    $nonExistentService = $container->get('nonExistent');
} catch (SimpleNotFoundException $e) {
    echo "\nError: " . $e->getMessage() . "\n";
}

In this example:

  • We define a SimpleContainer that implements ContainerInterface.
  • The set method (our custom addition) allows us to register service definitions as closures.
  • The get method retrieves an instance, instantiating it only if it hasn't been already (basic singleton behavior).
  • has checks for the existence of a service definition.
  • Custom exceptions SimpleContainerException and SimpleNotFoundException are created to fulfill the PSR-11 exception requirements.

Best Practices with DI and PSR-11

  • Favor Constructor Injection: It makes dependencies explicit and ensures that an object is in a valid state upon creation.
  • Depend on Abstractions: Always type-hint interfaces (LoggerInterface) rather than concrete implementations (FileLogger). This is the core of flexible and testable code.
  • Keep Container Simple: The container's primary job is to wire up dependencies. Avoid putting business logic inside service definitions.
  • Avoid Service Location: While the container allows you to get services anywhere, resist the urge to pass the container itself into other objects. This leads to the "Service Locator" anti-pattern, which reintroduces tight coupling to the container. Let the container inject what's needed.
  • Use Existing Libraries: For production applications, leverage robust, well-tested DI containers like Symfony's DependencyInjection, PHP-DI, or Laravel's IoC container. They offer advanced features like auto-wiring, compilation, and comprehensive error handling.

Conclusion

Dependency Injection, augmented by the PSR-11 Container Interface, is a cornerstone of modern, maintainable, and testable PHP applications. By separating object creation from object usage, you gain significant flexibility and reduce coupling. PSR-11 ensures that your application remains portable and compatible with various container implementations, allowing you to choose the best tool for your project without fear of vendor lock-in. Embrace these patterns to build more robust and scalable PHP systems.

Resources

Next Steps

  • Experiment with a full-featured DI container like PHP-DI in a small project.
  • Explore auto-wiring and how it simplifies dependency configuration.
  • Read more about the "Service Locator" anti-pattern and why it should be avoided.
← Back to php tutorials