Contenedor de Servicio
Introducción
El contenedor de servicios de Laravel es una potente herramienta para gestionar las dependencias de clases y realizar la inyección de dependencias. La inyección de dependencias es una frase elegante que esencialmente significa esto: las dependencias de clase se "inyectan" en la clase a través del constructor o, en algunos casos, métodos "setter".
Veamos un ejemplo sencillo:
<?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
{
/**
* Create a new controller instance.
*/
public function __construct(
protected UserRepository $users,
) {}
/**
* Show the profile for the given user.
*/
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, vamos a inyectar un servicio que sea capaz de recuperar usuarios. En este contexto, lo más probable es que nuestro UserRepository
utilice Eloquent para recuperar la información de los usuarios de la base de datos. Sin embargo, dado que el repositorio está inyectado, podemos cambiarlo fácilmente por otra implementación. También podemos fácilmente "mock", o crear una implementación ficticia del UserRepository
cuando probamos nuestra aplicación.
Un profundo conocimiento del contenedor de servicios de Laravel es esencial para construir una aplicación potente y de gran tamaño, así como para contribuir al propio núcleo de Laravel.
Resolución de Configuración Cero
Si una clase no tiene dependencias o sólo depende de otras clases concretas (no de interfaces), no es necesario indicar al contenedor cómo resolver esa clase. Por ejemplo, puede colocar el siguiente código en su archivo routes/web.php
:
<?php
class Service
{
// ...
}
Route::get('/', function (Service $service) {
die(get_class($service));
});
En este ejemplo, al pulsar la ruta /
de tu aplicación se resolverá automáticamente la clase Service
y se inyectará en el manejador de tu ruta. Esto cambia las reglas del juego. Significa que puedes desarrollar tu aplicación y aprovechar las ventajas de la inyección de dependencias sin preocuparte por los archivos de configuración hinchados.
Afortunadamente, muchas de las clases que escribirás cuando construyas una aplicación Laravel reciben automáticamente sus dependencias a través del contenedor, incluyendo controllers, event listeners, middleware, y más. Además, puedes escribir dependencias en el método handle
de queued jobs. Una vez que pruebas el poder de la inyección de dependencias automática y sin configuración, parece imposible desarrollar sin ella.
Cuándo Utilizar el Contenedor
Gracias a la resolución de configuración cero, a menudo escribirás dependencias en rutas, controladores, escuchadores de eventos, y en otros lugares sin tener que interactuar manualmente con el contenedor. Por ejemplo, puedes escribir el objeto Illuminate\Http\Request
en tu definición de ruta para poder acceder fácilmente a la petición actual. A pesar de que nunca tenemos que interactuar con el contenedor para escribir este código, es la gestión de la inyección de estas dependencias detrás de las escenas:
use Illuminate\Http\Request;
Route::get('/', function (Request $request) {
// ...
});
En muchos casos, gracias a la inyección automática de dependencias y facades, puedes construir aplicaciones Laravel sin nunca vincular o resolver manualmente nada desde el contenedor. Entonces, ¿cuándo interactuarías manualmente con el contenedor? Examinemos dos situaciones.
En primer lugar, si escribes una clase que implementa una interfaz y deseas escribir esa interfaz en una ruta o constructor de clase, debes indicar al contenedor cómo resolver esa interfaz. En segundo lugar, si estás escribiendo un paquete Laravel que planeas compartir con otros desarrolladores Laravel, puede que necesites enlazar los servicios de tu paquete en el contenedor.
Binding
Conceptos Básicos de Binding
Simple Bindings
Casi todas las vinculaciones del contenedor de servicios se registrarán en service providers, por lo que la mayoría de estos ejemplos mostrará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 utilizando el método bind
, pasando el nombre de la clase o interfaz que deseamos registrar junto con un closure que devuelva 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));
});
Tenga en cuenta que recibimos el propio contenedor como argumento para el resolver. A continuación, podemos utilizar el contenedor para resolver las subdependencias del objeto que estamos construyendo.
Como se ha mencionado, normalmente interactuará con el contenedor dentro de los proveedores de servicios; sin embargo, si desea interactuar con el contenedor fuera de un proveedor de servicios, puede hacerlo a través de la App
Facade:
use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;
App::bind(Transistor::class, function (Application $app) {
// ...
});
Binding A Singleton
El método singleton
vincula una clase o interfaz al contenedor que sólo debe resolverse una vez. Una vez resuelto un enlace singleton, se devolverá la misma instancia de objeto en las siguientes llamadas 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));
});
Binding Scoped Singletons
El método scoped
vincula una clase o interfaz en el contenedor que sólo debe resolverse una vez dentro de un determinado ciclo de vida de solicitud / trabajo de Laravel. Si bien este método es similar al método singleton
, las instancias registradas utilizando el método scoped
se vaciarán cada vez que la aplicación Laravel inicie un nuevo "ciclo de vida", como cuando un trabajador Laravel Octane procesa una nueva solicitud o cuando un trabajador queue worker 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));
});
Binding Instances
También puede enlazar una instancia de objeto existente en el contenedor utilizando el método instance
. La instancia dada siempre será devuelta en las siguientes llamadas al contenedor:
use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);
Binding de Interfaces a Implementaciones
Una característica muy potente del contenedor de servicios es su capacidad para vincular una interfaz a una implementación dada. Por ejemplo, supongamos que tenemos una interfaz EventPusher
y una implementación RedisEventPusher
. Una vez que hemos codificado nuestra implementación RedisEventPusher
de esta interfaz, podemos registrarla con el contenedor de servicios de la siguiente manera:
use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
$this->app->bind(EventPusher::class, RedisEventPusher::class);
Esta sentencia indica al contenedor que debe inyectar el RedisEventPusher
cuando una clase necesite una implementación de EventPusher
. Ahora podemos inyectar la interfaz EventPusher
en el constructor de una clase que sea resuelta por el contenedor. Recuerda, controladores, escuchadores de eventos, middleware, y varios otros tipos de clases dentro de las aplicaciones Laravel siempre se resuelven utilizando el contenedor:
use App\Contracts\EventPusher;
/**
* Create a new class instance.
*/
public function __construct(
protected EventPusher $pusher
) {}
Binding contextual
A veces puede tener dos clases que utilizan la misma interfaz, pero desea inyectar diferentes implementaciones en cada clase. Por ejemplo, dos controladores pueden depender de diferentes implementaciones del Illuminate\Contracts\Filesystem\Filesystem
contract. Laravel proporciona una interfaz sencilla y fluida 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');
});
Binding Primitives
A veces puedes tener una clase que recibe algunas clases inyectadas, pero también necesita un valor primitivo inyectado como un entero. Puede utilizar fácilmente la vinculación contextual para inyectar cualquier valor que su clase pueda necesitar:
use App\Http\Controllers\UserController;
$this->app->when(UserController::class)
->needs('$variableName')
->give($value);
A veces una clase puede depender de un array de instancias tagged. Usando el método giveTagged
, puedes inyectar fácilmente todos los enlaces del contenedor con esa etiqueta:
$this->app->when(ReportAggregator::class)
->needs('$reports')
->giveTagged('reports');
Si necesitas inyectar un valor de uno de los ficheros de configuración de tu aplicación, puedes utilizar el método giveConfig
:
$this->app->when(ReportAggregator::class)
->needs('$timezone')
->giveConfig('app.timezone');
Binding de variables tipificadas
Ocasionalmente, puedes tener una clase que recibe un array de objetos tipados usando un argumento variadic del constructor:
<?php
use App\Models\Filter;
use App\Services\Logger;
class Firewall
{
/**
* The filter instances.
*
* @var array
*/
protected $filters;
/**
* Create a new class instance.
*/
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}
Utilizando la vinculación contextual, puede resolver esta dependencia proporcionando al método give
un closure que devuelva un array de instancias de Filter
resueltas:
$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 puede proporcionar una matriz de nombres de clase que el contenedor resolverá cada vez que Firewall
necesite instancias de Filter
:
$this->app->when(Firewall::class)
->needs(Filter::class)
->give([
NullFilter::class,
ProfanityFilter::class,
TooLongFilter::class,
]);
Dependencias de etiquetas variádicas
A veces una clase puede tener una dependencia variada que se indica como una clase determinada (Informe ...$informes
). Utilizando los métodos needs
y giveTagged
, puede inyectar fácilmente todos los enlaces de contenedor con ese tag para la dependencia dada:
$this->app->when(ReportAggregator::class)
->needs(Report::class)
->giveTagged('reports');
Etiquetado
Ocasionalmente, puede que necesites resolver todas las vinculaciones de una determinada "categoría". Por ejemplo, puede que estés construyendo un analizador de informes que reciba un array de diferentes implementaciones 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 etiquetados los servicios, puedes resolverlos todos fácilmente a través del método tagged
del contenedor:
$this->app->bind(ReportAnalyzer::class, function (Application $app) {
return new ReportAnalyzer($app->tagged('reports'));
});
Extender Bindings
El método extend
permite modificar los 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);
});
Resolviendo
El Método make
make
Puedes utilizar 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 se pueden resolver a través del contenedor, puedes inyectarlas pasándolas como un array asociativo al método makeWith
. Por ejemplo, podemos pasar manualmente el argumento del constructor $id
requerido por el servicio Transistor
:
use App\Services\Transistor;
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);
Si se encuentra fuera de un proveedor de servicios en una ubicación de su código que no tiene acceso a la variable $app
, puede utilizar la App
facade o la app
helper para resolver una instancia de clase del contenedor:
use App\Services\Transistor;
use Illuminate\Support\Facades\App;
$transistor = App::make(Transistor::class);
$transistor = app(Transistor::class);
Si desea que la instancia del contenedor Laravel se inyecte en una clase que está siendo resuelta por el contenedor, puede escribir la clase Illuminate\Container\Container
en el constructor de su clase:
use Illuminate\Container\Container;
/**
* Create a new class instance.
*/
public function __construct(
protected Container $container
) {}
Inyección automática
Alternativamente, y de manera importante, puedes escribir la dependencia en el constructor de una clase que sea resuelta por el contenedor, incluyendo controllers, event listeners, middleware, y más. Además, puedes escribir dependencias en el método handle
de queued jobs. En la práctica, así es como la mayoría de tus objetos deberían ser resueltos por el contenedor.
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
use App\Models\User;
class UserController extends Controller
{
/**
* Create a new controller instance.
*/
public function __construct(
protected UserRepository $users,
) {}
/**
* Show the user with the given ID.
*/
public function show(string $id): User
{
$user = $this->users->findOrFail($id);
return $user;
}
}
Invocación e inyección de métodos
A veces es posible que desee invocar un método en una instancia de objeto mientras permite 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
{
/**
* Generate a new user report.
*/
public function generate(UserRepository $repository): array
{
return [
// ...
];
}
}
Puede 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 callable de PHP. El método call
del contenedor puede usarse incluso para invocar un cierre mientras se inyectan automáticamente sus dependencias:
use App\Repositories\UserRepository;
use Illuminate\Support\Facades\App;
$result = App::call(function (UserRepository $repository) {
// ...
});
Eventos en contenedores
El contenedor de servicios lanza 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) {
// Called when container resolves objects of type "Transistor"...
});
$this->app->resolving(function (mixed $object, Application $app) {
// Called when container resolves object of any type...
});
PSR-11
El contenedor de servicios de Laravel implementa la interfaz PSR-11. Por lo tanto, puedes teclear la interfaz del 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 no se puede resolver el identificador dado. La excepción será una instancia de Psr\Container\NotFoundExceptionInterface
si el identificador nunca se vinculó. Si el identificador se vinculó pero no se pudo resolver, se lanzará una instancia de PsrContainerContainerExceptionInterface
.
Última actualización
¿Te fue útil?