PHP Dependency Injection with PSR Containers

Dependency Injection (DI) is a powerful design pattern that promotes loose coupling and testability in your PHP applications. When combined with Service Containers adhering to PSR standards, it provides a robust and interoperable way to manage your application's dependencies. This post will delve into the principles of Dependency Injection, the role of Service Containers, the significance of PSR-11, and how to integrate these concepts into your PHP projects.

Understanding Dependency Injection

At its core, Dependency Injection is a technique where objects receive their dependencies from an external source rather than creating them themselves. This reverses the traditional control flow, leading to more flexible and maintainable codebases.

Why Use Dependency Injection?

  • Loose Coupling: Components become independent of their concrete implementations, making it easier to swap out dependencies without altering the core logic.
  • Improved Testability: By injecting mock or stub dependencies, you can isolate units of code for easier and more reliable testing.
  • Enhanced Reusability: Loosely coupled components are more easily reused in different parts of your application or even in other projects.
  • Simplified Maintenance: Changes to one dependency are less likely to ripple through the entire application.

Types of Dependency Injection

While the concept is singular, DI can be implemented in a few ways:

  • Constructor Injection: Dependencies are provided through the class constructor. This is generally the preferred method as it ensures that the object is always in a valid state.
    class Mailer
    {
        private LoggerInterface $logger;
    
        public function __construct(LoggerInterface $logger)
        {
            $this->logger = $logger;
        }
    
        public function sendEmail(string $to, string $subject, string $body): void
        {
            // ... send email logic ...
            $this->logger->info("Email sent to {$to}");
        }
    }
    
  • Setter Injection: Dependencies are set via public setter methods. This allows for optional dependencies or changing dependencies during the object's lifecycle.
  • Interface Injection: Dependencies are injected through an interface, requiring the dependent class to implement a specific interface to receive the dependency.

Service Containers: The Dependency Management Hub

A Service Container (also known as a Dependency Injection Container or IoC Container) is an object that knows how to instantiate and configure objects and their dependencies. Instead of manually creating instances, you ask the container for an object, and it provides a fully wired instance.

Benefits of Using a Service Container

  • Centralized Configuration: All service definitions are in one place, making it easier to manage and understand your application's dependencies.
  • Automated Dependency Resolution: The container can automatically resolve and inject dependencies, reducing boilerplate code.
  • Lifecycle Management: Containers can manage the lifecycle of objects (e.g., singletons, new instances per request).
  • Extensibility: Many containers allow for decorators, compilers, and other extensions to customize object creation.

PSR Standards and Interoperability: The Role of PSR-11

PSR-11, the Container Interface, is a standard developed by the PHP Framework Interoperability Group (PHP-FIG). Its primary goal is to provide a common interface for dependency injection containers, fostering interoperability between different frameworks and libraries.

The Psr\Container\ContainerInterface

The ContainerInterface defines two simple methods:

  • get(string $id): Finds an entry of the container by its identifier and returns it.
  • has(string $id): Returns true if the container can return an entry for the given identifier. Returns false otherwise.
namespace Psr\Container;

interface ContainerInterface
{
    public function get(string $id);
    public function has(string $id): bool;
}

By adhering to PSR-11, a container ensures that any application or library expecting a Psr\Container\ContainerInterface can seamlessly integrate with it, regardless of the underlying container implementation (e.g., Symfony's DependencyInjection component, PHP-DI, Pimple).

Integrating PSR Containers in Your PHP Application

Let's walk through a basic example of using a PSR-11 compatible container, specifically PHP-DI, to manage dependencies.

Installation

First, install PHP-DI and the PSR container interfaces via Composer:

composer require php-di/php-di psr/container

Defining Services

Consider our Mailer and Logger example. We'll define a simple Logger interface and implementation:

// src/LoggerInterface.php
namespace App;

interface LoggerInterface
{
    public function info(string $message): void;
}

// src/FileLogger.php
namespace App;

class FileLogger implements LoggerInterface
{
    private string $filePath;

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

    public function info(string $message): void
    {
        file_put_contents($this->filePath, "[INFO] " . $message . PHP_EOL, FILE_APPEND);
    }
}

// src/Mailer.php
namespace App;

use Psr\Container\ContainerInterface;

class Mailer
{
    private LoggerInterface $logger;

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

    public function sendEmail(string $to, string $subject, string $body): void
    {
        // ... actual email sending logic ...
        $this->logger->info("Email sent to {$to}");
        echo "Email sent successfully to {$to} (logged).
";
    }
}

Configuring the Container

Now, let's configure PHP-DI to resolve these dependencies. You can define entries using an array or a PHP-DI definition builder.

// public/index.php

require __DIR__ . '/../vendor/autoload.php';

use DI\ContainerBuilder;
use App\LoggerInterface;
use App\FileLogger;
use App\Mailer;

$containerBuilder = new ContainerBuilder();

$containerBuilder->addDefinitions([
    LoggerInterface::class => DI\create(FileLogger::class)->constructorParameter('filePath', __DIR__ . '/../log/app.log'),
    Mailer::class => DI\autowire(Mailer::class),
]);

$container = $containerBuilder->build();

// Retrieve the Mailer instance from the container
$mailer = $container->get(Mailer::class);

// Use the Mailer to send an email
$mailer->sendEmail('[email protected]', 'Hello from DI', 'This is a test email.');

In this setup:

  • LoggerInterface::class is bound to FileLogger::class, and its constructor receives the filePath parameter.
  • Mailer::class is set to autowire, meaning PHP-DI will automatically resolve its LoggerInterface dependency by looking into the container.

Framework Integration

Most modern PHP frameworks like Symfony, Laravel, and Laminas (formerly Zend Framework) come with their own robust Dependency Injection Containers that are typically PSR-11 compliant. This means you can often leverage the framework's built-in container to manage your application's services, making it easy to integrate third-party libraries that also adhere to PSR-11.

  • Symfony: The Symfony DependencyInjection component is a powerful and flexible container, fully compliant with PSR-11. It heavily relies on configuration files (YAML, XML, PHP) to define services.
  • Laravel: Laravel's Service Container is a core component, providing powerful dependency resolution, including automatic injection and contextual binding. While not explicitly implementing Psr\Container\ContainerInterface in its main Application class, it offers methods (make, bind) that mirror the PSR-11 concepts and often has a PSR-11 compatible adapter.

Conclusion

Dependency Injection, when coupled with PSR-11 compliant Service Containers, transforms how you build PHP applications. It shifts the responsibility of dependency management to a dedicated component, leading to more modular, testable, and maintainable code. Embracing these patterns and standards not only improves your current projects but also enhances the interoperability and long-term viability of your PHP development efforts.

Resources

← Back to php tutorials