Oleksii Siniaiev
Navegación de la página del artículo
Artículos 8 min de lectura March 21, 2026

Dependency Injection y Dependency Inversion en Laravel

En resumen La inyección de dependencias es una mecánica: una clase recibe sus colaboradores en lugar de construirlos. La inversión de dependencias es una decisión de diseño: depender de una abstracción, no de una…

En esta página

En resumen

  • La inyección de dependencias es una mecánica: una clase recibe sus colaboradores en lugar de construirlos. La inversión de dependencias es una decisión de diseño: depender de una abstracción, no de una clase concreta. No son lo mismo.
  • El contenedor de Laravel hace la inyección por ti automáticamente mediante los type-hints del constructor. Solo escribes un binding cuando inviertes una dependencia hacia una interfaz.
  • La mayoría de las clases no necesitan una interfaz. Añádela en un límite que cambia o que necesita una costura para pruebas —pagos, correo, almacenamiento, APIs externas—, no de forma refleja a cada servicio.
  • El antipatrón que lo echa todo a perder: resolver dependencias con app() en lo profundo de los métodos en vez de inyectarlas. Eso es un service locator, y oculta justo lo que intentabas hacer explícito.

Una vez heredé un código Laravel donde cada servicio tenía una interfaz. UserServiceInterface, InvoiceServiceInterface, ReportServiceInterface —cada una con exactamente una implementación, cada una vinculada uno a uno en un service provider de cuatrocientas líneas. El equipo creía que «seguía SOLID». Lo que en realidad habían construido era un código donde saltar a la definición de un método te dejaba siempre en una interfaz, y tenías que ir a buscar el binding para saber qué se ejecutaba de verdad. La abstracción añadía indirección y no quitaba nada.

Ese proyecto me enseñó la distinción de la que trata este artículo. Inyección e inversión de dependencias se usan como sinónimos, y difuminarlas es como acabas con una interfaz por cada clase. Resuelven problemas distintos, y saber cuál necesitas de verdad es lo que mantiene limpio un código Laravel a medida que crece.

Inyección e inversión no son lo mismo

La inyección de dependencias significa que una clase recibe aquello de lo que depende en vez de construirlo. En Laravel eso es casi siempre inyección por constructor, y el contenedor construye la dependencia por ti:

Code
class RegisterUser
{
    public function __construct(
        private Mailer $mailer,
    ) {}
}

Eso ya es una mejora sobre new Mailer(...) dentro del método —la clase ya no posee el cableado. Pero fíjate en que sigue dependiendo de un Mailer concreto. Has inyectado la dependencia sin invertirla.

La inversión de dependencias es el segundo paso, separado: hacer que la clase de alto nivel dependa de una abstracción que ella misma posee, de modo que la implementación concreta se vuelva un detalle que puedes intercambiar.

Code
interface Mailer
{
    public function send(Message $message): void;
}

class RegisterUser
{
    public function __construct(
        private Mailer $mailer, // now an interface
    ) {}
}

Ahora la clase declara un requisito («algo que pueda enviar un mensaje») en lugar de nombrar a un proveedor. SMTP, Mailgun, SES o un fake en las pruebas, todos lo cumplen. Esa es la inversión: la dirección de la dependencia se invirtió de «el código de alto nivel depende del detalle de bajo nivel» a «ambos dependen de un contrato».

Cómo lo resuelve realmente el contenedor de Laravel

Para la inyección simple no escribes ninguna configuración. Cuando el contenedor construye RegisterUser, lee los type-hints del constructor y resuelve cada uno de forma recursiva. Una clase concreta con sus propias dependencias resolubles simplemente funciona —por eso la mayoría de controladores y jobs de Laravel no necesitan ningún binding.

La inyección en métodos funciona igual. Pon un type-hint a una dependencia en una acción de controlador y el contenedor la proporciona por petición:

Code
public function store(Request $request, Mailer $mailer)
{
    // $mailer resolved by the container for this call
}

En el momento en que dependes de una interfaz, el contenedor ya no puede adivinar. Una interfaz no tiene constructor que construir. Tienes que decirle qué implementación usar, y esa es la única razón por la que existen los bindings:

Code
// In a service provider's register() method
$this->app->bind(Mailer::class, MailgunMailer::class);

Ahora, allí donde Mailer tiene type-hint, el contenedor entrega un MailgunMailer. Cambiar toda la aplicación a SES es una línea aquí, no un buscar-y-reemplazar por todo el código. Ese único punto de cambio es toda la recompensa de la inversión.

singleton() comparte estado durante toda la petición

bind() construye una instancia nueva cada vez; singleton() construye una vez y la reutiliza. Un singleton que guarda estado mutable —un usuario cacheado, un array que se acumula— filtra ese estado a todo lo que se resuelve en la misma petición. Usa singleton() para colaboradores sin estado y objetos tipo conexión, no para nada que mute en cada llamada.

Binding contextual: cuando dos consumidores necesitan implementaciones distintas

El caso que justifica una interfaz con más claridad es cuando el mismo contrato debe resolverse de forma distinta según quién pregunte. Laravel lo maneja con binding contextual, y es la característica que hace que la inversión se pague sola:

Code
$this->app->when(PublicUploadController::class)
    ->needs(Filesystem::class)
    ->give(fn () => Storage::disk('s3'));

$this->app->when(PrivateReportController::class)
    ->needs(Filesystem::class)
    ->give(fn () => Storage::disk('local'));

Ambos controladores piden la misma abstracción Filesystem; el contenedor da a cada uno el disco correcto. Sin la interfaz no hay nada que variar. Esta es la diferencia entre una interfaz que se gana su sitio y otra que solo añade un salto.

Cuándo una interfaz se gana su sitio — y cuándo es ruido

El código del principio se torció al tratar «añadir una interfaz» como algo siempre bueno. Es un intercambio: una interfaz compra flexibilidad y una costura para pruebas, y cuesta una capa de indirección y un binding que mantener. Gástala donde la flexibilidad es real.

Situación¿Interfaz?Por qué
Límite externo (pagos, correo, almacenamiento, SMS)El proveedor cambiará, y quieres un fake en las pruebas
Varias implementaciones reales hoyEs literalmente para lo que sirve la abstracción
Un cambio para el que puedes nombrar una razón concreta«Pasaremos de SES a Postmark el próximo trimestre»
Un servicio pequeño y autónomo con una implementaciónNoInyecta la clase concreta; añade la interfaz el día que necesites una segunda
Un objeto de valor o un cálculo de dominio puroNoNo hay nada que invertir; no tiene dependencia de infraestructura

El valor por defecto honesto en Laravel: inyecta clases concretas y recurre a una interfaz cuando una clase se sitúa en el límite entre tu dominio y el mundo exterior. «Quizá lo necesite algún día» no es una razón; «el proveedor de pagos vive detrás de esto» sí lo es.

Probar a través del límite

Aquí es donde la inversión paga el coste de forma más directa. Como los consumidores dependen del contrato, una prueba puede sustituir un fake sin tocar el código bajo prueba. El contenedor convierte el intercambio en una sola llamada:

Code
public function test_registration_sends_a_welcome_message(): void
{
    $mailer = new FakeMailer();
    $this->app->instance(Mailer::class, $mailer);

    $this->post('/register', [...]);

    $this->assertCount(1, $mailer->sent);
}

Sin SMTP, sin red, sin gimnasia de framework de mocking —el verdadero RegisterUser se ejecuta con un colaborador fake porque solo dependió de la interfaz. Esa es la testabilidad que la gente atribuye a la inyección pero que en realidad viene de la inversión: una dependencia concreta no se puede intercambiar tan limpiamente.

Errores que veo en código real de Laravel

Una interfaz por cada clase

FooServiceInterface con una implementación añade indirección y no quita nada. Añade la interfaz cuando una segunda implementación o una costura de pruebas sea real.

Service locator en vez de inyección

Llamar a app(Mailer::class) en lo profundo de un método oculta la dependencia del constructor. Pon un type-hint para que la clase declare lo que necesita.

Filtrar tipos del framework por el contrato

Una interfaz cuyos métodos reciben un modelo Eloquent o un Request no abstrae nada. Pasa datos simples u objetos de valor a través del límite.

Interfaces «Manager» todopoderosas

Un ServiceManagerInterface con quince métodos no relacionados no es un contrato. Divídelo por la capacidad real que necesita cada consumidor.

Singletons con estado

Vincular un objeto mutable como singleton() filtra estado por toda la petición. Reserva los singletons para servicios sin estado o tipo conexión.

Vincular en el lugar equivocado

Resolver servicios dentro de register() se ejecuta antes de cargar todos los providers. Pon los bindings en register() y la resolución en boot().

FAQ

¿Cuál es la diferencia entre inyección e inversión de dependencias?
La inyección de dependencias es una técnica: una clase recibe sus dependencias desde fuera en lugar de crearlas. La inversión de dependencias es un principio de diseño: el código de alto nivel depende de una abstracción y no de una clase concreta. Puedes inyectar una clase concreta (inyección sin inversión); la inversión es el paso adicional de depender de una interfaz.
¿Necesito una interfaz para cada servicio en Laravel?
No. Por defecto inyecta clases concretas. Introduce una interfaz cuando una dependencia se sitúa en un límite externo (pagos, correo, almacenamiento), cuando de verdad tienes varias implementaciones o cuando necesitas una costura de pruebas limpia. Una interfaz con una sola implementación normalmente solo añade indirección.
¿Por qué Laravel resuelve algunas clases sin ningún binding?
El contenedor lee los type-hints del constructor y construye clases concretas automáticamente, resolviendo sus dependencias de forma recursiva. Solo necesitas un binding cuando pones un type-hint a una interfaz, porque una interfaz no tiene constructor que el contenedor pueda construir.
¿Qué es el antipatrón service locator?
Llamar a app() o resolve() para obtener dependencias dentro de un método en lugar de inyectarlas por el constructor. Oculta de qué depende una clase y la hace más difícil de probar, que es lo contrario de para lo que sirve la inyección de dependencias.
¿Cuándo usar bind() y cuándo singleton()?
Usa bind() cuando cada consumidor deba recibir una instancia nueva, que es el valor por defecto seguro. Usa singleton() solo para colaboradores sin estado u objetos tipo conexión que quieras reutilizar durante toda la petición. Un singleton con estado mutable filtra ese estado a todo lo que se resuelve después de él.

Artículos relacionados

Actualizado: June 17, 2026

Compartir este artículo

LinkedIn X Email

Contacto

¿Trabajas en algo parecido? Hablemos.

Estoy abierto a hablar de arquitectura, Laravel, WordPress, rendimiento y problemas prácticos de implementación.

Enviar un mensaje Ver proyectos

Explorar más

Artículos

June 14, 2026

Parte 3. Un mes con un diario de IA: cómo encontrar patrones entre sueño, estrés y entrenamientos

Cómo analizar un diario de IA tras el primer mes: correcciones de transcripción, reflexión…
Artículos

June 14, 2026

Parte 2. Hermes Agent + DeepSeek en Ubuntu: guía completa de un diario con IA en Telegram

Guía paso a paso: Hermes Agent y DeepSeek en Ubuntu, bot privado de Telegram,…
Artículos

June 14, 2026

Parte 1. Cómo convertí un viejo portátil gaming en un diario de bienestar con IA

Cómo un viejo Xiaomi Mi Gaming Laptop se convirtió en servidor doméstico con IA:…