Contenedor de servicios
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 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 simple:
<?php
namespace App\Http\Controllers;
use App\Services\AppleMusic;
use Illuminate\View\View;
class PodcastController extends Controller
{
/**
* Crear una nueva instancia del controlador.
*/
public function __construct(
protected AppleMusic $apple,
) {}
/**
* Mostrar información sobre el podcast dado.
*/
public function show(string $id): View
{
return view('podcasts.show', [
'podcast' => $this->apple->findPodcast($id)
]);
}
}
En este ejemplo, el PodcastController
necesita recuperar podcasts de una fuente de datos como Apple Music. Entonces, inyectaremos un servicio que pueda recuperar podcasts. Dado que el servicio está inyectado, podemos fácilmente «simular» o crear una implementación ficticia del servicio AppleMusic
al probar nuestra aplicación.
Una comprensión profunda del contenedor de servicios de Laravel es esencial para construir una aplicación poderosa y grande, así como para contribuir al núcleo de Laravel.
Resolución Sin Configuración
Si una clase no tiene dependencias o solo depende de otras clases concretas (no interfaces), el contenedor no necesita ser instruido 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 acceder a la ruta /
de tu aplicación, se resolverá automáticamente la clase Service
y se inyectará en el manejador de la 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 inflados.
Afortunadamente, muchas de las clases que escribirás al construir una aplicación Laravel reciben automáticamente sus dependencias a través del contenedor, incluyendo controladores, listeners de eventos, middleware y más. Además, puedes indicar dependencias en el método handle
de trabajos en cola. Una vez que pruebes el poder de la inyección de dependencias automática y sin configuración, se siente imposible desarrollar sin ella.
Cuándo Utilizar el Contenedor
Gracias a la resolución sin configuración, a menudo indicarás dependencias en rutas, controladores, listeners de eventos y en otros lugares sin interactuar manualmente con el contenedor. Por ejemplo, podrías indicar el objeto Illuminate\Http\Request
en la definición de tu ruta para que puedas acceder fácilmente a la solicitud actual. Aunque nunca tengamos que interactuar con el contenedor para escribir este código, está gestionando la inyección de estas dependencias detrás de escena:
use Illuminate\Http\Request;
Route::get('/', function (Request $request) {
// ...
});
En muchos casos, gracias a la inyección de dependencias automática y a las facades, puedes construir aplicaciones Laravel sin tener que enlazar o resolver manualmente nada del contenedor. Entonces, ¿cuándo interactuarías manualmente con el contenedor? Examinemos dos situaciones.
Primero, si escribes una clase que implementa una interfaz y deseas indicar esa interfaz en una ruta o constructor de clase, debes decirle al contenedor cómo resolver esa interfaz. En segundo lugar, si estás escribiendo un paquete Laravel que planeas compartir con otros desarrolladores de Laravel, es posible que necesites enlazar los servicios de tu paquete en el contenedor.
Enlazado
Conceptos Básicos de Enlazado
Enlazados Simples
Casi todos tus enlaces de contenedor de servicios se registrarán dentro de 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 closure 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 contenedor en sí como un argumento para el resolver. Luego podemos usar el contenedor para resolver sub-dependencias del objeto que estamos construyendo.
Como se mencionó, típicamente estarás interactuando con el contenedor dentro de proveedores de servicios; sin embargo, si deseas interactuar con el contenedor fuera de un proveedor de servicios, puedes hacerlo a través de la facade 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 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));
});
No es necesario enlazar clases en el contenedor si no dependen de ninguna interfaz. El contenedor no necesita ser instruido sobre cómo construir estos objetos, ya que puede resolver automáticamente estos objetos usando reflexión.
Enlazado 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 de singleton, la misma instancia de objeto se devolverá en llamadas posteriores 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 singleton solo si 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));
});
Enlazado de Singletons con Ámbito
El método scoped
enlaza una clase o interfaz en el contenedor que solo debe resolverse una vez dentro de un ciclo de vida de solicitud / trabajo de Laravel dado. Aunque este método es similar al método singleton
, las instancias registradas usando el método scoped
se vaciará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));
});
Enlazado de Instancias
También puedes enlazar una instancia de objeto existente en el contenedor usando el método instance
. La instancia dada siempre se devolverá en llamadas posteriores al contenedor:
use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);
Binding Interfaces to Implementations
Una característica muy poderosa del contenedor de servicios es su capacidad para enlazar 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 declaración le dice al contenedor que debe inyectar RedisEventPusher
cuando una clase necesite una implementación de EventPusher
. Ahora podemos indicar la interfaz EventPusher
en el constructor de una clase que es resuelta por el contenedor. Recuerda, los controladores, listeners de eventos, middleware y varios otros tipos de clases dentro de las aplicaciones Laravel siempre se resuelven usando el contenedor:
use App\Contracts\EventPusher;
/**
* Crear una nueva instancia de clase.
*/
public function __construct(
protected EventPusher $pusher,
) {}
Contextual Binding
A veces puedes tener dos clases que utilizan 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
. Laravel proporciona una interfaz fluida y 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');
});
Contextual Attributes
Dado que el binding contextual se utiliza a menudo para inyectar implementaciones de drivers o valores de configuración, Laravel ofrece una variedad de atributos de binding contextual que permiten inyectar estos tipos de valores sin definir manualmente los bindings contextuales en tus proveedores de servicios.
Por ejemplo, el atributo Storage
puede usarse para inyectar un disco de almacenamiento específico:
<?php
namespace App\Http\Controllers;
use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Filesystem\Filesystem;
class PhotoController extends Controller
{
public function __construct(
#[Storage('local')] protected Filesystem $filesystem
)
{
// ...
}
}
Además del atributo Storage
, Laravel ofrece los atributos Auth
, Cache
, Config
, DB
, Log
y Tag
:
<?php
namespace App\Http\Controllers;
use Illuminate\Container\Attributes\Auth;
use Illuminate\Container\Attributes\Cache;
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\DB;
use Illuminate\Container\Attributes\Log;
use Illuminate\Container\Attributes\Tag;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Database\Connection;
use Psr\Log\LoggerInterface;
class PhotoController extends Controller
{
public function __construct(
#[Auth('web')] protected Guard $auth,
#[Cache('redis')] protected Repository $cache,
#[Config('app.timezone')] protected string $timezone,
#[DB('mysql')] protected Connection $connection,
#[Log('daily')] protected LoggerInterface $log,
#[Tag('reports')] protected iterable $reports,
)
{
// ...
}
}
Además, Laravel proporciona un atributo CurrentUser
para inyectar el usuario autenticado actualmente en una ruta o clase dada:
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
Route::get('/user', function (#[CurrentUser] User $user) {
return $user;
})->middleware('auth');
Definiendo Atributos Personalizados
Puedes crear tus propios atributos contextuales implementando el contrato Illuminate\Contracts\Container\ContextualAttribute
. El contenedor llamará al método resolve
de tu atributo, que debería resolver el valor que debe inyectarse en la clase que utiliza el atributo. En el ejemplo a continuación, reimplementaremos el atributo Config
incorporado de Laravel:
<?php
namespace App\Attributes;
use Illuminate\Contracts\Container\ContextualAttribute;
#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements ContextualAttribute
{
/**
* Crear una nueva instancia de atributo.
*/
public function __construct(public string $key, public mixed $default = null)
{
}
/**
* Resolver el valor de configuración.
*
* @param self $attribute
* @param \Illuminate\Contracts\Container\Container $container
* @return mixed
*/
public static function resolve(self $attribute, Container $container)
{
return $container->make('config')->get($attribute->key, $attribute->default);
}
}
Binding Primitives
A veces puedes tener una clase que recibe algunas clases inyectadas, pero también necesita un valor primitivo inyectado, como un entero. Puedes usar fácilmente el binding 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. Usando el método giveTagged
, puedes inyectar fácilmente todas las bindings del 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');
Binding Typed Variadics
Ocasionalmente, puedes tener una clase que recibe un array de objetos tipados usando un argumento de constructor variádico:
<?php
use App\Models\Filter;
use App\Services\Logger;
class Firewall
{
/**
* Las instancias de filtro.
*
* @var array
*/
protected $filters;
/**
* Crear una nueva instancia de clase.
*/
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}
Usando binding contextual, puedes resolver esta dependencia proporcionando el método give
con un closure que devuelve 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 puedes proporcionar simplemente un array de nombres de clase que serán resueltos por el contenedor siempre que Firewall
necesite instancias de Filter
:
$this->app->when(Firewall::class)
->needs(Filter::class)
->give([
NullFilter::class,
ProfanityFilter::class,
TooLongFilter::class,
]);
Variadic Tag Dependencies
A veces una clase puede tener una dependencia variádica que está tipada como una clase dada (Report ...$reports
). Usando los métodos needs
y giveTagged
, puedes inyectar fácilmente todas las bindings del contenedor con esa etiqueta para la dependencia dada:
$this->app->when(ReportAggregator::class)
->needs(Report::class)
->giveTagged('reports');
Tagging
Ocasionalmente, puedes necesitar resolver todas las bindings de una cierta «categoría». Por ejemplo, quizás estés construyendo un analizador de reportes que recibe un array de muchas implementaciones diferentes de la interfaz Report
. Después de registrar las implementaciones de Report
, puedes asignarles una etiqueta usando 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 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'));
});
Extending Bindings
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 del servicio que estás extendiendo y un closure que debe devolver el servicio modificado. El closure 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);
});
Resolving
The make Method
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 pasando 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]);
El método bound
puede usarse para determinar si una clase o interfaz ha sido explícitamente enlazada en el contenedor:
if ($this->app->bound(Transistor::class)) {
// ...
}
Si estás 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 facade 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 en sí misma se inyecte en una clase que está siendo resuelta por el contenedor, puedes indicar la clase Illuminate\Container\Container
en el constructor de tu clase:
use Illuminate\Container\Container;
/**
* Crear una nueva instancia de clase.
*/
public function __construct(
protected Container $container,
) {}
Automatic Injection
Alternativamente, y lo que es más importante, puedes indicar la dependencia en el constructor de una clase que es resuelta por el contenedor, incluyendo controladores, listeners de eventos, middleware y más. Además, puedes indicar dependencias en el método handle
de trabajos en cola. En la práctica, así es como la mayoría de tus objetos deben ser resueltos por el contenedor.
Por ejemplo, puedes indicar un servicio definido por tu aplicación en el constructor de un controlador. El servicio se resolverá automáticamente y se inyectará en la clase:
<?php
namespace App\Http\Controllers;
use App\Services\AppleMusic;
class PodcastController extends Controller
{
/**
* Crear una nueva instancia del controlador.
*/
public function __construct(
protected AppleMusic $apple,
) {}
/**
* Mostrar información sobre el podcast dado.
*/
public function show(string $id): Podcast
{
return $this->apple->findPodcast($id);
}
}
Method Invocation and Injection
A veces puedes desear invocar un método en una instancia de objeto mientras permites que el contenedor inyecte automáticamente las dependencias de ese método. Por ejemplo, dada la siguiente clase:
<?php
namespace App;
use App\Services\AppleMusic;
class PodcastStats
{
/**
* Generar un nuevo informe de estadísticas de podcast.
*/
public function generate(AppleMusic $apple): array
{
return [
// ...
];
}
}
Puedes invocar el método generate
a través del contenedor de la siguiente manera:
use App\PodcastStats;
use Illuminate\Support\Facades\App;
$stats = App::call([new PodcastStats, 'generate']);
El método call
acepta cualquier callable de PHP. El método call
del contenedor incluso puede usarse para invocar un closure mientras inyecta automáticamente sus dependencias:
use App\Services\AppleMusic;
use Illuminate\Support\Facades\App;
$result = App::call(function (AppleMusic $apple) {
// ...
});
Container Events
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) {
// Llamado cuando el contenedor resuelve objetos del tipo "Transistor"...
});
$this->app->resolving(function (mixed $object, Application $app) {
// Llamado cuando el contenedor resuelve un objeto de cualquier tipo...
});
Como puedes ver, el objeto que se está resolviendo se pasará al callback, lo que te permite establecer cualquier propiedad adicional en el objeto antes de que se entregue a su consumidor.
PSR-11
El contenedor de servicios de Laravel implementa la interfaz PSR-11. Por lo tanto, puedes indicar 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 lanzará una excepción si el identificador dado no puede resolverse. La excepción será una instancia de Psr\Container\NotFoundExceptionInterface
si el identificador nunca fue enlazado. Si el identificador fue enlazado pero no pudo resolverse, se lanzará una instancia de Psr\Container\ContainerExceptionInterface
.