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.
