Inyección de dependencias en PHP: principios SOLID, containers y autowiring

La inyección de dependencias (DI) es uno de los principios más importantes del diseño orientado a objetos en PHP. Consiste en pasar las dependencias de una clase desde fuera en lugar de crearlas internamente. Este enfoque hace el código más fácil de testear, mantener y extender, y es la base del principio de inversión de dependencias (la D de SOLID).

El problema del acoplamiento interno

<?php
// Sin DI: la clase crea sus propias dependencias
class ServicioRegistro
{
    private PDO     $pdo;
    private Mailer  $mailer;
    private Logger  $logger;

    public function __construct()
    {
        // Acoplado: no se puede cambiar sin modificar esta clase
        $this->pdo    = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
        $this->mailer = new Mailer('smtp.ejemplo.com');
        $this->logger = new FileLogger('/var/log/app.log');
    }
}

// No se puede testear sin BD real, servidor SMTP y disco
?>

Constructor injection

La forma más recomendada de inyectar dependencias en PHP es a través del constructor. Las dependencias son explícitas, obligatorias y están disponibles desde el primer uso del objeto.

<?php
declare(strict_types=1);

interface LoggerInterface
{
    public function info(string $mensaje, array $contexto = []): void;
    public function error(string $mensaje, array $contexto = []): void;
}

interface MailerInterface
{
    public function enviar(string $destino, string $asunto, string $cuerpo): bool;
}

interface UsuarioRepositorioInterface
{
    public function buscarPorEmail(string $email): ?array;
    public function crear(string $nombre, string $email, string $hashPassword): int;
}

class ServicioRegistro
{
    public function __construct(
        private readonly UsuarioRepositorioInterface $repositorio,
        private readonly MailerInterface             $mailer,
        private readonly LoggerInterface             $logger,
    ) {}

    public function registrar(string $nombre, string $email, string $password): int
    {
        if ($this->repositorio->buscarPorEmail($email)) {
            throw new DomainException("El email ya está registrado: $email");
        }

        $id = $this->repositorio->crear($nombre, $email, password_hash($password, PASSWORD_BCRYPT));

        $this->mailer->enviar($email, 'Bienvenida', "Hola, $nombre. Tu cuenta está lista.");
        $this->logger->info('Usuario registrado', ['id' => $id, 'email' => $email]);

        return $id;
    }
}
?>

Contenedor DI manual

Cuando una aplicación tiene muchas clases, montar las dependencias a mano en el punto de entrada puede volverse tedioso. Un contenedor DI centraliza esa configuración.

<?php
class Contenedor
{
    private array $definiciones  = [];
    private array $singletons    = [];

    // Registrar una definición (factory callable)
    public function bind(string $abstract, callable $factory): void
    {
        $this->definiciones[$abstract] = $factory;
    }

    // Registrar un singleton (se crea una sola vez)
    public function singleton(string $abstract, callable $factory): void
    {
        $this->definiciones[$abstract] = function () use ($abstract, $factory) {
            if (!isset($this->singletons[$abstract])) {
                $this->singletons[$abstract] = $factory($this);
            }
            return $this->singletons[$abstract];
        };
    }

    public function make(string $abstract): mixed
    {
        if (!isset($this->definiciones[$abstract])) {
            throw new RuntimeException("No se encontró definición para: $abstract");
        }
        return ($this->definiciones[$abstract])($this);
    }
}

// Configurar el contenedor (bootstrap de la aplicación)
$c = new Contenedor();

$c->singleton(PDO::class, fn() => new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'));
$c->singleton(LoggerInterface::class, fn($c) => new FileLogger('/var/log/app.log'));
$c->singleton(MailerInterface::class, fn() => new SmtpMailer($_ENV['SMTP_HOST']));
$c->bind(UsuarioRepositorioInterface::class, fn($c) => new UsuarioRepositorioPDO($c->make(PDO::class)));

$c->bind(ServicioRegistro::class, fn($c) => new ServicioRegistro(
    $c->make(UsuarioRepositorioInterface::class),
    $c->make(MailerInterface::class),
    $c->make(LoggerInterface::class),
));

// Resolver la dependencia
$servicio = $c->make(ServicioRegistro::class);
$servicio->registrar('Ana', '[email protected]', 'contrasena123');
?>

Autowiring con PHP-DI

Librerías como PHP-DI inspeccionan los tipos de los parámetros del constructor mediante reflexión y resuelven las dependencias automáticamente sin necesidad de configurar cada clase.

<?php
// composer require php-di/php-di

use DIContainerBuilder;

$builder   = new ContainerBuilder();
$builder->addDefinitions([
    // Solo necesario cuando el tipo es una interfaz
    LoggerInterface::class             => DIautowire(FileLogger::class),
    MailerInterface::class             => DIautowire(SmtpMailer::class),
    UsuarioRepositorioInterface::class => DIautowire(UsuarioRepositorioPDO::class),
]);

$container = $builder->build();

// PHP-DI inyecta automáticamente PDO, Mailer, Logger en ServicioRegistro
$servicio = $container->get(ServicioRegistro::class);
?>

El Service Locator es un antipatrón

El Service Locator es un objeto global que actúa como registro de servicios. Las clases lo llaman para obtener sus dependencias. A diferencia de la DI, las dependencias son opacas: no se ven en el constructor, lo que dificulta los tests y la comprensión del código.

<?php
// Antipatrón: Service Locator
class ServicioRegistroMalo
{
    public function registrar(string $nombre, string $email): void
    {
        // Las dependencias están ocultas, obtenidas internamente
        $repo   = ServiceLocator::get(UsuarioRepositorioInterface::class);
        $mailer = ServiceLocator::get(MailerInterface::class);
        $log    = ServiceLocator::get(LoggerInterface::class);
        // ... lógica ...
    }
}

// Correcto: las dependencias son explícitas en el constructor
// class ServicioRegistro { public function __construct(private UsuarioRepositorioInterface $r, ...) {} }
?>

La documentación de POO en PHP y la especificación PSR-11 del contenedor de dependencias definen la interfaz estándar que deben implementar los contenedores DI para ser intercambiables entre frameworks.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP