1. Conceptos de arquitectura
  2. Contenedor de Servicios

Únete a nuestra comunidad de Telegram @webblend!

Aquí encontrarás fragmentos de código de Laravel y consejos útiles sobre desarrollo web.

Introducción

El contenedor de servicios de Laravel es una herramienta poderosa para gestionar dependencias de clases y realizar inyección de dependencias. La inyección de dependencias es una frase elegante que significa básicamente esto: las dependencias de clase se "inyectan" en la clase a través del constructor o, en algunos casos, mediante métodos "setter".

Veamos un ejemplo simple:

<?php
 
namespace App\Http\Controllers;
 
use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\Models\User;
use Illuminate\View\View;
 
class UserController extends Controller
{
/**
* Crear una nueva instancia del controlador.
*/
public function __construct(
protected UserRepository $users,
) {}
 
/**
* Mostrar el perfil del usuario proporcionado.
*/
public function show(string $id): View
{
$user = $this->users->find($id);
 
return view('user.profile', ['user' => $user]);
}
}

En este ejemplo, el UserController necesita recuperar usuarios de una fuente de datos. Por lo tanto, inyectaremos un servicio que pueda recuperar usuarios. En este contexto, es probable que nuestro UserRepository utilice Eloquent para recuperar información del usuario desde la base de datos. Sin embargo, dado que el repositorio se inyecta, podemos cambiarlo fácilmente por otra implementación. También podemos "simular" o crear una implementación ficticia del UserRepository al probar nuestra aplicación.

Una comprensión profunda del contenedor de servicios de Laravel es esencial para construir una aplicación grande y potente, así como para contribuir al núcleo de Laravel mismo.

Resolución sin Configuración

Si una clase no tiene dependencias o solo depende de otras clases concretas (no interfaces), el contenedor no necesita instrucciones sobre cómo resolver esa clase. Por ejemplo, puedes colocar el siguiente código en tu archivo routes/web.php:

<?php
 
class Service
{
// ...
}
 
Route::get('/', function (Service $service) {
die($service::class);
});

En este ejemplo, al golpear la ruta / de tu aplicación, se resolverá automáticamente la clase Service e se inyectará en el controlador de tu ruta. Esto es revolucionario. Significa que puedes desarrollar tu aplicación y aprovechar la inyección de dependencias sin preocuparte por archivos de configuración abultados.

Afortunadamente, muchas de las clases que escribirás al construir una aplicación de Laravel reciben automáticamente sus dependencias a través del contenedor, incluyendo controladores, escuchadores de eventos, middleware y más. Además, puedes hacer una referencia de tipo a las dependencias en el método handle de trabajos en cola. Una vez que pruebas el poder de la inyección de dependencias automática y sin configuración, se siente imposible desarrollar sin ella.

Cuándo Usar el Contenedor

Gracias a la resolución sin configuración, a menudo harás referencia de tipo a dependencias en rutas, controladores, escuchadores de eventos y en otros lugares sin interactuar manualmente con el contenedor. Por ejemplo, podrías hacer una referencia de tipo al objeto Illuminate\Http\Request en tu definición de ruta para poder acceder fácilmente a la solicitud actual. Aunque nunca tenemos que interactuar con el contenedor para escribir este código, está gestionando la inyección de estas dependencias tras bambalinas:

use Illuminate\Http\Request;
 
Route::get('/', function (Request $request) {
// ...
});

En muchos casos, gracias a la inyección automática de dependencias y fachadas, puedes construir aplicaciones de Laravel sin nunca enlazar o resolver manualmente nada desde el contenedor. Entonces, ¿cuándo interactuarías manualmente con el contenedor? Examínemos dos situaciones.

Primero, si escribes una clase que implementa una interfaz y deseas hacer referencia de tipo a esa interfaz en una ruta o constructor de clase, debes indicarle al contenedor cómo resolver esa interfaz. En segundo lugar, si estás escribiendo un paquete de Laravel que planeas compartir con otros desarrolladores de Laravel, es posible que necesites enlazar los servicios de tu paquete en el contenedor.

Enlace

Conceptos Básicos de Enlace

Enlaces Simples

Casi todos tus enlaces de contenedor de servicios se registrarán dentro de los proveedores de servicios, por lo que la mayoría de estos ejemplos demostrarán el uso del contenedor en ese contexto.

Dentro de un proveedor de servicios, siempre tienes acceso al contenedor a través de la propiedad $this->app. Podemos registrar un enlace usando el método bind, pasando el nombre de la clase o interfaz que deseamos registrar junto con un cierre que devuelve una instancia de la clase:

use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->bind(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

Ten en cuenta que recibimos el propio contenedor como argumento para el resolutor. Luego podemos usar el contenedor para resolver subdependencias del objeto que estamos construyendo.

Como se mencionó, normalmente estarás interactuando con el contenedor dentro de los proveedores de servicios; sin embargo, si deseas interactuar con el contenedor fuera de un proveedor de servicios, puedes hacerlo a través de la fachada App:

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;
 
App::bind(Transistor::class, function (Application $app) {
// ...
});

Puedes usar el método bindIf para registrar un enlace de contenedor solo si aún no se ha registrado un enlace para el tipo dado:

$this->app->bindIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

Nota No es necesario enlazar clases en el contenedor si no dependen de ninguna interfaz. El contenedor no necesita instrucciones sobre cómo construir estos objetos, ya que puede resolver automáticamente estos objetos utilizando reflexión.

Enlace de Singleton

El método singleton enlaza una clase o interfaz en el contenedor que solo debe resolverse una vez. Una vez que se resuelve un enlace singleton, la misma instancia del objeto se devolverá en llamadas subsiguientes al contenedor:

use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->singleton(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

Puedes usar el método singletonIf para registrar un enlace de contenedor singleton solo si aún no se ha registrado un enlace para el tipo dado:

$this->app->singletonIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

Enlace de Singleton con Ámbito

El método scoped enlaza una clase o interfaz en el contenedor que solo debe resolverse una vez dentro del ciclo de vida de una solicitud / trabajo de Laravel. Si bien este método es similar al método singleton, las instancias registradas mediante el método scoped se eliminarán cada vez que la aplicación Laravel inicie un nuevo "ciclo de vida", como cuando un trabajador de Laravel Octane procesa una nueva solicitud o cuando un trabajador de cola de Laravel procesa un nuevo trabajo:

use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->scoped(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

Enlace de Instancias

También puedes enlazar una instancia de objeto existente en el contenedor mediante el método instance. La instancia dada siempre se devolverá en llamadas subsiguientes al contenedor:

use App\Services\Transistor;
use App\Services\PodcastParser;
 
$service = new Transistor(new PodcastParser);
 
$this->app->instance(Transistor::class, $service);

Enlazar Interfaces a Implementaciones

Una característica muy poderosa del contenedor de servicios es su capacidad para enlazar una interfaz a una implementación dada. Por ejemplo, asumamos que tenemos una interfaz EventPusher y una implementación RedisEventPusher. Una vez que hayamos codificado nuestra implementación RedisEventPusher de esta interfaz, podemos registrarla en el contenedor de servicios de la siguiente manera:

use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
 
$this->app->bind(EventPusher::class, RedisEventPusher::class);

Esta declaración le indica al contenedor que debe inyectar RedisEventPusher cuando una clase necesite una implementación de EventPusher. Ahora podemos hacer referencia a la interfaz EventPusher en el constructor de una clase que resuelve el contenedor. Recuerda que los controladores, los oyentes de eventos, el middleware y varios otros tipos de clases dentro de las aplicaciones de Laravel siempre se resuelven utilizando el contenedor:

use App\Contracts\EventPusher;
 
/**
* Crear una nueva instancia de la clase.
*/
public function __construct(
protected EventPusher $pusher
) {}

Enlace Contextual

A veces, puede que tengas dos clases que utilicen la misma interfaz, pero deseas inyectar diferentes implementaciones en cada clase. Por ejemplo, dos controladores pueden depender de diferentes implementaciones del contrato Illuminate\Contracts\Filesystem\Filesystem de Laravel. Laravel proporciona una interfaz fluida simple para definir este comportamiento:

use App\Http\Controllers\PhotoController;
use App\Http\Controllers\UploadController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
 
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
 
$this->app->when([VideoController::class, UploadController::class])
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});

Enlace de Primitivas

A veces, puede que tengas una clase que recibe algunas clases inyectadas, pero también necesita un valor primitivo inyectado, como un entero. Puedes usar fácilmente el enlace contextual para inyectar cualquier valor que tu clase pueda necesitar:

use App\Http\Controllers\UserController;
 
$this->app->when(UserController::class)
->needs('$variableName')
->give($value);

A veces, una clase puede depender de una matriz de instancias etiquetadas. Utilizando el método giveTagged, puedes inyectar fácilmente todos los enlaces de contenedor con esa etiqueta:

$this->app->when(ReportAggregator::class)
->needs('$reports')
->giveTagged('reports');

Si necesitas inyectar un valor de uno de los archivos de configuración de tu aplicación, puedes usar el método giveConfig:

$this->app->when(ReportAggregator::class)
->needs('$timezone')
->giveConfig('app.timezone');

Enlace de Tipos Variádicos

Ocasionalmente, puedes tener una clase que recibe una matriz de objetos escritos utilizando un argumento de constructor variádico:

<?php
 
use App\Models\Filter;
use App\Services\Logger;
 
class Firewall
{
/**
* Las instancias del filtro.
*
* @var array
*/
protected $filters;
 
/**
* Crear una nueva instancia de la clase.
*/
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}

Usando el enlace contextual, puedes resolver esta dependencia proporcionando el método give con un cierre que devuelva una matriz de instancias resueltas de Filter:

$this->app->when(Firewall::class)
->needs(Filter::class)
->give(function (Application $app) {
return [
$app->make(NullFilter::class),
$app->make(ProfanityFilter::class),
$app->make(TooLongFilter::class),
];
});

Para mayor comodidad, también puedes simplemente proporcionar una matriz de nombres de clases para ser resueltos por el contenedor cada vez que Firewall necesite instancias de Filter:

$this->app->when(Firewall::class)
->needs(Filter::class)
->give([
NullFilter::class,
ProfanityFilter::class,
TooLongFilter::class,
]);

Dependencias Variádicas Etiquetadas

A veces, una clase puede tener una dependencia variádica que está indicada como un tipo dado (Report ...$reports). Usando los métodos needs y giveTagged, puedes inyectar fácilmente todos los enlaces de contenedor con esa etiqueta para la dependencia dada:

$this->app->when(ReportAggregator::class)
->needs(Report::class)
->giveTagged('reports');

Etiquetado

Ocasionalmente, es posible que necesites resolver todas las implementaciones de una cierta "categoría". Por ejemplo, tal vez estás construyendo un analizador de informes que recibe una matriz de muchas implementaciones diferentes de la interfaz Report. Después de registrar las implementaciones de Report, puedes asignarles una etiqueta utilizando el método tag:

$this->app->bind(CpuReport::class, function () {
// ...
});
 
$this->app->bind(MemoryReport::class, function () {
// ...
});
 
$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');

Una vez que los servicios han sido etiquetados, puedes resolverlos fácilmente a todos mediante el método tagged del contenedor:

$this->app->bind(ReportAnalyzer::class, function (Application $app) {
return new ReportAnalyzer($app->tagged('reports'));
});

Extensión de Enlaces

El método extend permite la modificación de servicios resueltos. Por ejemplo, cuando se resuelve un servicio, puedes ejecutar código adicional para decorar o configurar el servicio. El método extend acepta dos argumentos: la clase de servicio que estás extendiendo y un cierre que debe devolver el servicio modificado. El cierre recibe el servicio que se está resolviendo y la instancia del contenedor:

$this->app->extend(Service::class, function (Service $service, Application $app) {
return new DecoratedService($service);
});

Resolución

El Método make

Puedes usar el método make para resolver una instancia de clase desde el contenedor. El método make acepta el nombre de la clase o interfaz que deseas resolver:

use App\Services\Transistor;
 
$transistor = $this->app->make(Transistor::class);

Si algunas de las dependencias de tu clase no son resolubles a través del contenedor, puedes inyectarlas pasándolas como un array asociativo en el método makeWith. Por ejemplo, podemos pasar manualmente el argumento de constructor $id requerido por el servicio Transistor:

use App\Services\Transistor;
 
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);

El método bound se puede utilizar para determinar si una clase o interfaz se ha enlazado explícitamente en el contenedor:

if ($this->app->bound(Transistor::class)) {
// ...
}

Si te encuentras fuera de un proveedor de servicios en una ubicación de tu código que no tiene acceso a la variable $app, puedes usar la fachada App o el helper app para resolver una instancia de clase desde el contenedor:

use App\Services\Transistor;
use Illuminate\Support\Facades\App;
 
$transistor = App::make(Transistor::class);
 
$transistor = app(Transistor::class);

Si deseas que la instancia del contenedor de Laravel se inyecte en una clase que se está resolviendo en el contenedor, puedes hacer una referencia de tipo a la clase Illuminate\Container\Container en el constructor de tu clase:

use Illuminate\Container\Container;
 
/**
* Crear una nueva instancia de la clase.
*/
public function __construct(
protected Container $container
) {}

Inyección Automática

Alternativamente, y lo que es más importante, puedes hacer una referencia de tipo a la dependencia en el constructor de una clase que se resuelve en el contenedor, incluyendo controladores, escuchadores de eventos, middleware y más. Además, puedes hacer una referencia de tipo a las dependencias en el método handle de trabajos en cola. En la práctica, así es como la mayoría de tus objetos deben resolverse mediante el contenedor.

Por ejemplo, puedes hacer una referencia de tipo a un repositorio definido por tu aplicación en el constructor de un controlador. El repositorio se resolverá automáticamente e inyectará en la clase:

<?php
 
namespace App\Http\Controllers;
 
use App\Repositories\UserRepository;
use App\Models\User;
 
class UserController extends Controller
{
/**
* Crear una nueva instancia del controlador.
*/
public function __construct(
protected UserRepository $users,
) {}
 
/**
* Mostrar el usuario con el ID proporcionado.
*/
public function show(string $id): User
{
$user = $this->users->findOrFail($id);
 
return $user;
}
}

Invocación e Inyección de Métodos

A veces, puede que desees invocar un método en una instancia de objeto permitiendo que el contenedor inyecte automáticamente las dependencias de ese método. Por ejemplo, dada la siguiente clase:

<?php
 
namespace App;
 
use App\Repositories\UserRepository;
 
class UserReport
{
/**
* Generar un nuevo informe de usuario.
*/
public function generate(UserRepository $repository): array
{
return [
// ...
];
}
}

Puedes invocar el método generate a través del contenedor de la siguiente manera:

use App\UserReport;
use Illuminate\Support\Facades\App;
 
$report = App::call([new UserReport, 'generate']);

El método call acepta cualquier llamable de PHP. El método call del contenedor incluso se puede utilizar para invocar un cierre mientras inyecta automáticamente sus dependencias:

use App\Repositories\UserRepository;
use Illuminate\Support\Facades\App;
 
$result = App::call(function (UserRepository $repository) {
// ...
});

Eventos del Contenedor

El contenedor de servicios dispara un evento cada vez que resuelve un objeto. Puedes escuchar este evento usando el método resolving:

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
 
$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
// Se llama cuando el contenedor resuelve objetos del tipo "Transistor"...
});
 
$this->app->resolving(function (mixed $object, Application $app) {
// Se llama cuando el contenedor resuelve objetos de cualquier tipo...
});

Como puedes ver, el objeto que se está resolviendo se pasa al callback, lo que te permite establecer cualquier propiedad adicional en el objeto antes de dárselo a su consumidor.

PSR-11

El contenedor de servicios de Laravel implementa la interfaz PSR-11. Por lo tanto, puedes hacer una referencia de tipo a la interfaz de contenedor PSR-11 para obtener una instancia del contenedor de Laravel:

use App\Services\Transistor;
use Psr\Container\ContainerInterface;
 
Route::get('/', function (ContainerInterface $container) {
$service = $container->get(Transistor::class);
 
// ...
});

Se lanza una excepción si el identificador dado no se puede resolver. La excepción será una instancia de Psr\Container\NotFoundExceptionInterface si el identificador nunca se ha enlazado. Si el identificador se enlazó pero no se pudo resolver, se lanzará una instancia de Psr\Container\ContainerExceptionInterface.