Curso de Laravel
Inyección de dependencias en Laravel: el contenedor de servicios
Por Víctor Peña · Publicado el
Hola, ¿cómo están? Empezamos el módulo de Laravel avanzado, y lo hacemos con el mecanismo que está debajo de casi todo lo que hemos usado hasta ahora sin darnos cuenta.
Cuando escribes esto:
public function store(GuardarPacienteRequest $request)
Nadie crea ese $request. Llega solo, ya validado. Lo mismo pasa con los modelos de las rutas y con las clases del framework. Eso es el contenedor de servicios trabajando, y entenderlo cambia bastante cómo escribes el código.
¡Empecemos!
El problema que resuelve
Supongamos que tenemos una clase que envía mensajes por WhatsApp para recordar citas:
class RecordatorioService
{
public function enviar(Cita $cita): void
{
$whatsapp = new WhatsAppApi(
config('servicios.whatsapp.token'),
config('servicios.whatsapp.telefono'),
);
$whatsapp->enviarMensaje($cita->paciente->telefono, '...');
}
}
Funciona, pero tiene tres problemas.
No se puede probar. Cada prueba enviaría un mensaje real.
No se puede cambiar. Si mañana usas otro proveedor, hay que buscar todos los new WhatsAppApi del proyecto.
La clase sabe demasiado. RecordatorioService debería ocuparse de recordar citas, no de saber cómo se construye un cliente de WhatsApp.
La solución es no crearlo dentro, sino recibirlo:
class RecordatorioService
{
public function __construct(
private WhatsAppApi $whatsapp,
) {}
public function enviar(Cita $cita): void
{
$this->whatsapp->enviarMensaje($cita->paciente->telefono, '...');
}
}
Eso es toda la inyección de dependencias: la clase declara qué necesita, y alguien más se lo da.
El nombre suena mucho más complicado de lo que es.
Y aquí entra el contenedor
La pregunta obvia: si nadie hace new RecordatorioService(...), ¿quién arma eso?
El contenedor. Lee el constructor, ve que necesita un WhatsAppApi, lo construye, y te devuelve el servicio listo.
public function store(Request $request, RecordatorioService $recordatorios)
{
$cita = Cita::create($request->validated());
$recordatorios->enviar($cita);
return redirect()->route('citas.show', $cita);
}
Ese segundo parámetro llega solo. Laravel mira el tipo, lo resuelve con todas sus dependencias, y lo pasa.
Se llama resolución automática, y funciona sin configurar nada mientras las clases se puedan construir con lo que el contenedor ya sabe.
Dónde funciona la inyección
El contenedor resuelve los parámetros de:
- Constructores de controladores
- Métodos de controladores (esto sorprende: no solo el constructor)
- Constructores de Form Requests, Jobs, Listeners, Commands
- El método
handle()de trabajos y listeners
class PacienteController extends Controller
{
public function __construct(
private PacienteService $pacientes,
) {}
public function reporte(Request $request, GeneradorPdf $pdf)
{
// los dos llegan resueltos
}
}
Constructor cuando la dependencia se usa en varios métodos; parámetro del método cuando solo se usa en uno.
Cuando el contenedor no puede solo
Si la clase necesita valores que no son objetos, la resolución automática falla:
class WhatsAppApi
{
public function __construct(
private string $token,
private string $telefono,
) {}
}
El contenedor no puede adivinar ese token. Hay que enseñarle, y eso se hace en un service provider:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->bind(WhatsAppApi::class, function ($app) {
return new WhatsAppApi(
config('servicios.whatsapp.token'),
config('servicios.whatsapp.telefono'),
);
});
}
A partir de ahí, cualquier clase que pida un WhatsAppApi lo recibe ya configurado. Y el token está en un solo sitio.
bind y singleton
$this->app->bind(WhatsAppApi::class, fn () => new WhatsAppApi(...));
$this->app->singleton(WhatsAppApi::class, fn () => new WhatsAppApi(...));
La diferencia:
bindcrea una instancia nueva cada vez que alguien la pide.singletoncrea una sola y la reutiliza durante toda la petición.
Usa singleton cuando construir el objeto cuesta —una conexión, un cliente HTTP, un cliente de API— y bind cuando el objeto tiene estado que no debería compartirse entre usos.
En la duda: si el objeto no guarda datos que cambien, singleton.
Interfaces: donde esto se vuelve útil de verdad
Hasta aquí puede parecer ceremonia innecesaria. El valor aparece cuando inyectas una interfaz en lugar de una clase concreta.
interface CanalNotificacion
{
public function enviar(string $destino, string $mensaje): bool;
}
class WhatsAppCanal implements CanalNotificacion
{
public function enviar(string $destino, string $mensaje): bool { /* ... */ }
}
class SmsCanal implements CanalNotificacion
{
public function enviar(string $destino, string $mensaje): bool { /* ... */ }
}
Se le dice al contenedor cuál usar:
$this->app->bind(CanalNotificacion::class, WhatsAppCanal::class);
Y el servicio pide la interfaz:
class RecordatorioService
{
public function __construct(
private CanalNotificacion $canal,
) {}
}
Cambiar de WhatsApp a SMS en todo el sistema es cambiar una línea del provider. El resto del código no se entera, porque nunca supo con qué estaba hablando.
Es el mismo principio que hace que Storage funcione igual con disco local o en la nube, como vimos en la lección de archivos.
Elegir según la configuración
$this->app->bind(CanalNotificacion::class, function ($app) {
return match (config('servicios.canal_por_defecto')) {
'whatsapp' => $app->make(WhatsAppCanal::class),
'sms' => $app->make(SmsCanal::class),
default => $app->make(CorreoCanal::class),
};
});
Con eso, cada instalación del sistema puede usar un canal distinto sin tocar código.
Service providers
Un service provider es el sitio donde se registra todo esto. Los propios se crean así:
php artisan make:provider ServiciosProvider
class ServiciosProvider extends ServiceProvider
{
public function register(): void
{
// solo registrar cosas en el contenedor
}
public function boot(): void
{
// ejecutar lógica: se llama cuando todo ya está registrado
}
}
La separación importa. En register() no puedes usar otros servicios, porque quizá aún no están registrados. Todo lo que sea «hacer algo» va en boot().
En boot() es donde suelen ir los Gates de la lección de autorización, la política de contraseñas y la configuración de Carbon.
Los providers se declaran en bootstrap/providers.php.
Resolver a mano
Cuando no puedes usar inyección —dentro de una función anónima, por ejemplo:
app(RecordatorioService::class);
app()->make(RecordatorioService::class);
resolve(RecordatorioService::class);
Y con parámetros extra:
app()->makeWith(GeneradorReporte::class, ['formato' => 'pdf']);
Úsalo lo menos posible. Cada app() dentro de una clase es una dependencia que no se ve en el constructor, y eso es exactamente lo que la inyección venía a evitar.
Probar sin tocar nada real
Aquí está el beneficio más concreto de todo esto:
public function test_agendar_una_cita_envia_recordatorio(): void
{
$canal = Mockery::mock(CanalNotificacion::class);
$canal->shouldReceive('enviar')->once()->andReturn(true);
$this->app->instance(CanalNotificacion::class, $canal);
$this->post('/citas', $datos);
}
Ese instance() reemplaza la implementación real por una falsa. La prueba verifica que se intentó enviar sin enviar nada.
Con el new WhatsAppApi del principio, esta prueba sería imposible.
Cuándo NO usar todo esto
Y aquí viene la parte que casi nunca se dice.
No crees una interfaz por cada clase. Una interfaz con una sola implementación que nunca va a tener otra no aporta nada: es un archivo más que leer para entender el mismo código.
No abstraigas Eloquent detrás de repositorios solo porque sí. Eloquent ya es una capa de abstracción. Envolverlo en otra suele terminar en un repositorio con cuarenta métodos que reimplementa peor lo que el ORM ya hacía.
La regla que uso: crea la interfaz cuando exista o vaya a existir una segunda implementación, o cuando la dependencia salga hacia fuera —una API externa, un servicio de pago, un canal de mensajería. En esos casos vale la pena. En el resto, la clase directa está bien.
Es lo mismo que dije con los permisos en la lección de autorización: la abstracción se añade cuando se necesita, no por adelantado.
Dónde poner la lógica
Una duda muy frecuente: ¿qué va en el controlador y qué en un servicio?
Controlador: recibir la petición, autorizar, llamar, responder. Servicio: la lógica de negocio, cuando pasa de unas pocas líneas o se usa desde varios sitios. Modelo: lo que tiene que ver con los datos de esa entidad.
class AgendarCita
{
public function __construct(
private CanalNotificacion $canal,
) {}
public function ejecutar(array $datos): Cita
{
return DB::transaction(function () use ($datos) {
$this->verificarDisponibilidad($datos);
$cita = Cita::create($datos);
$this->canal->enviar($cita->paciente->telefono, $this->mensaje($cita));
return $cita;
});
}
}
public function store(GuardarCitaRequest $request, AgendarCita $agendar)
{
$cita = $agendar->ejecutar($request->validated());
return redirect()->route('citas.show', $cita)->with('exito', 'Cita agendada');
}
Una clase por acción, con un solo método público, es un patrón que funciona muy bien cuando la lógica crece. Es fácil de encontrar, fácil de probar y no termina en un servicio de mil líneas.
Pero, otra vez: si la acción son tres líneas, déjala en el controlador. Crear una clase para Paciente::create($datos) no mejora nada.
Errores comunes
newdentro de una clase para dependencias que podrían inyectarse.- Interfaces sin una segunda implementación a la vista.
app()repartido por todo el código en lugar de inyección.- Lógica en
register()en vez deboot(). binddonde correspondíasingleton, construyendo un cliente HTTP en cada uso.- Repositorios sobre Eloquent que no aportan nada.
Para cerrar
El contenedor de servicios lleva funcionando desde la primera lección de este curso: es lo que hace que el Request, los modelos de las rutas y los Form Requests lleguen solos.
Lo práctico se resume así: declara lo que necesitas en el constructor y deja que el contenedor lo resuelva. Y usa interfaces solo cuando haya una razón concreta, no por seguir un patrón.
En la siguiente lección veremos las transacciones de base de datos.
Saludos y éxitos.
Norvic Software
Desarrollamos el software que tu empresa necesita
Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.
- Chatbots con Inteligencia Artificial
- Creación de agentes de IA
- Integración de IA en tus sistemas
- Desarrollo de software a medida
- Aplicaciones móviles iOS y Android
- Consultoría y asesoramiento técnico
Cotización sin costo · Respuesta directa por WhatsApp