Inyección de Dependencias en PHP: en la práctica
La Inyección de Dependencias (DI) es uno de los principios clave de SOLID que permite escribir código flexible, testeable y mantenible. En este artículo analizaremos cómo implementar la inyección de dependencias en PHP con ejemplos reales, sin teorías abstractas. Aprenderá cómo deshacerse de los acoplamientos rígidos entre clases y hacer que su código esté listo para los cambios.
¿Qué es la Inyección de Dependencias y para qué sirve?
Imagine que tiene una clase UserController que trabaja con una base de datos. Sin DI, lo más probable es que cree el objeto de conexión directamente dentro del controlador:
class UserController { private $db; public function __construct() { $this->db = new MySQLConnection('localhost', 'user', 'pass'); }}Este enfoque crea un acoplamiento rígido (tight coupling). Si desea cambiarse a PostgreSQL o usar un objeto mock para pruebas, tendrá que modificar el código del controlador. La Inyección de Dependencias resuelve este problema: las dependencias se pasan desde el exterior, no se crean dentro de la clase.
Principales ventajas de DI:
- Reducción del acoplamiento del código
- Simplificación de las pruebas (fácil sustitución de dependencias)
- Reutilización de clases
- Arquitectura limpia y cumplimiento del principio de inversión de dependencias (DIP)
Formas de inyectar dependencias en PHP
Existen tres formas principales de inyección de dependencias. Veamos cada una con ejemplos.
1. Inyección a través del constructor
La forma más común y recomendada. Las dependencias se pasan a través de los parámetros del constructor y se almacenan en las propiedades de la clase.
<?phpinterface DatabaseInterface { public function query(string $sql): array;}
class MySQLDatabase implements DatabaseInterface { public function query(string $sql): array { // Lógica real de trabajo con MySQL return ['id' => 1, 'name' => 'John']; }}
class UserRepository { private DatabaseInterface $db; public function __construct(DatabaseInterface $db) { $this->db = $db; } public function getUser(int $id): array { return $this->db->query(\"SELECT * FROM users WHERE id = $id\"); }}
// Uso:$db = new MySQLDatabase();$repo = new UserRepository($db);$user = $repo->getUser(1);?>Observe: usamos la interfaz DatabaseInterface. Esto permite sustituir fácilmente la implementación, por ejemplo, por un simulacro de prueba.
2. Inyección a través de setters
Se utiliza cuando la dependencia no es obligatoria en la etapa de creación del objeto o puede cambiar durante el funcionamiento.
<?phpclass MailService { private ?MailerInterface $mailer = null; public function setMailer(MailerInterface $mailer): void { $this->mailer = $mailer; } public function send(string $to, string $subject, string $body): void { if ($this->mailer === null) { throw new \\RuntimeException('Mailer not configured'); } $this->mailer->send($to, $subject, $body); }}
// Uso:$mailService = new MailService();$mailService->setMailer(new SmtpMailer());$mailService->send('user@example.com', 'Hello', 'Body');?>La desventaja de este enfoque es que no garantiza que la dependencia esté configurada antes de llamar al método send(). Por lo tanto, para dependencias obligatorias es preferible el constructor.
3. Inyección a través del método
La dependencia se pasa directamente al método donde se necesita. Útil para dependencias poco frecuentes o contextuales.
<?phpclass ReportGenerator { public function generate(DataFetcherInterface $fetcher, string $reportType): array { $data = $fetcher->