Servicios Del Contenedor

Servicios del contenedor

Introduction

El service provider de Laravel es una poderosa herramienta para manejar las dependencias de una clase y realizar la inyección de dependencia. Las dependencias serán inyectadas a través del constructor o en algunos casos a través de setters.

Vamos a ver un ejemplo

<?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 UserController necesita recuperar usuarios de una fuente de datos. Por lo que inyectara un servicio que es capaz de devolver usuarios.En este caso, nuestro UserRepository usa eloquent para devolver información de la base de datos. Sin embargo, como el repositorio es inyectado, podemos cambiarlo fácilmente por otra implementación o mockear o crear una implementación dummy cuando testemos nuestra aplicación.

Zero Configuration Resolution

Si una clase no tiene dependencias o solamente tiene dependencias concretas, es decir no hay interfaces, el contenedor será capaz de resolver esa clase.

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

En este ejemplo al acceder a la ruta, se inyectará automáticamente la clase Service. Por suerte, la mayoria de las clases que escribas recibiran sus dependencias a traves del contenedor, incluyendo controladores, event listeners, middlawers… Tambien se puede indicar dependencias en el método handle de los queued jobs.

When to utilize the container

En la mayoría de los casos solamente con la inyección automática de dependencias y las facades, podrás construir aplicaciones sin necesidad de manualmente bindear or resolver nada en el contenedor.

Hay dos situaciones en las que tendrás que interactuar manualmente con el controlador.

Cuando escribas una clase que implemente una interfaz y quieres inyectar esa interfaz en una ruta o constructor de una clase, deberás decirle al contenedor como resolver esa interfaz.

Segundo, Si estas escribiendo un paquete de laravel y tu plan es compartirlo con otros desarrolladores de Laravel, necesitarás bindear tu servicio al contenedor.

Binding

Binding basics

SimpleBindings

Casi todos bindings de tus servicios se registrarán dentro de un service provider. Así que la mayoría de ejemplos son en ese contexto.

Dentro del service provider, siempre tendrás acceso al contenedor via la propiedad $this→app.

Puedes registrar un binding usando el método bind , pasandole la clase o interfaz que queramos registrar junto con una 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));
});

Nota: Recibimos el contenedor mismo como argumento en la closure, por lo que podemos usarlo para resolver sub-dependencias del objeto que estamos creando.

Como hemos dicho la mayoría de las veces interactuaras con el contenedor dentro de un service provider, pero si quieres interactuar desde fuera, 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 bindIf para registrar en el contenedor solamente si no hay ya un binding registrado.

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

📔 No es necesario bindear clases dentro del contenedor si no dependen de ninguna interfaz

Binding A Singleton

El metodo singleton bindea una clase o interfaz y una vez resuelta, el mismo objeto sera devuelto en siguientes llamadas.

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));
});

Existe también el singletonIf, registra en el contenedor solamente si no hay ya un binding registrado

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

Binding Scoped Singletons

El método scoped bindea una clase o interfaz en el contenedor que deberá ser resuelta una sola vez dado una petición o job lifecycle. Mientras que este metodo es similar a singleton , las instancias registradas usando scope serán eliminadas cada vez que la aplicación de Laravel empiece un nuevo ciclo de vida, como cuando  Laravel Octane worker procesa una nueva request o un worker de una cola( queue worker) procesa un nuevo job.

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

Cuando se tiene una instancia de un objeto, se puede bindear al contenedor y en las siguientes llamadas se devolverá esa instancia.

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

Binding Interfaces to Implementations

Podemos bindear una interfaz a una implementación. Por ejemplo, imaginemos que tenemos la interfaz EventPusher y la clase RedisEventPusher como una implementación. Por lo que podemos registrarlo en el contenedor, de la siguiente manera.

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

Este código indica al contenedor que tiene que inyectar RedisEventPusher cuando una clase necesita una implementación para EventPusher. Ahora podemos poner la interfaz como dependencia en una clase que el contenedor pueda resolver y usara la implementación de RedisEventPusher.

use App\Contracts\EventPusher;
 
/**
 * Create a new class instance.
 */
public function __construct(
    protected EventPusher $pusher
) {}

Contextual Binding

Para cuando una clase necesita una implementación de una interfaz y otra clase necesita otra, se puede realizar de la siguiente manera:

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

Cuando se necesite inyectar un tipo primitivo en un controlador por ejemplo, podemos hacerlo con el contextual binding.

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

Cuando hay ciertas instancias que tienen un tag (se ve mas adelante) se puede usar giveTagged para inyectar todas las instancias con ese tag.

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

Si necesitas inyectar un valor de algún archivo de configuración, puedes usar giveConfig.

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

Binding Typed Variadics

Algunas clases pueden tener un constructor con un array de objetos de tipo variado

<?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;
    }
}

Usando el contextual binding, puedes resolver esta dependencia proporcionando una closure al metodo give, devolviendo instancias de tipo 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),
                ];
          });

Por conveniencia puedes pasarle un array al método give para indicar las instancias tambien

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

A veces una clase depende de un array de instancias tageadas. Se puede inyectar las instancias requeridas con un tag en específico.

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

Si se necesita un valor de algun archivo de configuración, se puede usar giveConfig()

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

Binding Typed Variadics

Cuando tienes una clase que recibe un numero variable de argumentos en su construcción.

    public function __construct(
        protected Logger $logger,
        Filter ...$filters,
    ) {
        $this->filters = $filters;
    }

Usando el bindeo con contexto, puedes pasarle una función a give devolviendo el array de todas las implementaciones que deseas para “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),
                ];
          });

Por conveniencia se puede realizar de esta forma también.

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

Se pueden resolver las clases también usando un tag.

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

Tagging

Taggear un grupo de dependencias es como darles una categoría. Ejemplo: tienes diferentes implementaciones para realizar un reporte. Después de registrarlas en el contenedor, puedes darles un tag.

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

Facilitando obtener estas instancias del contenedor

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

Extending bindings

Permite obtener un servicio del contenedor, al mismo tiempo que estas extendiendo de el, para modificar o realizar una configuración del mismo.

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

Resolving

El método make()

Se puede usar el método make para obtener una instancia de una clase del contenedor.

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

Si la instancia a recuperar requiere de parámetros podemos usar makeWith

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

Podemos usar el método bound para saber si una clase esta ligada al contenedor.

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

Si estas fuera de un service provider y no tienes acceso a app, puedes usar el facade de App

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

En caso de necesitar el Container de laravel en alguna de las clases, basta con sugerir el tipo en el constructor

use Illuminate\Container\Container;
 
/**
 * Create a new class instance.
 */
public function __construct(
    protected Container $container
) {}

Automatic Injection

Puedes sugerir el tipo de la dependencia de una clase que es resuelta por el contenedor, como en el caso de controladores, eventos, listeners y middlewares incluso en el método handle de los jobs para que laravel automáticamente realice la inyección de dependencias.

<?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;
    }
}

Method invocation and injection

A veces se necesita llamar a un método que tiene una dependencia, el contenedor es capaz de resolver esta dependencia.

<?php
 
namespace App;
 
use App\Repositories\UserRepository;
 
class UserReport
{

    public function generate(UserRepository $repository): array
    {
        return [
            // ...
        ];
    }
}

Para esta clase podemos usar el facade App con el método call

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

El método call acepta cualquier callable . Incluso permite invocar una closure inyectando automáticamente sus dependencias.

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

Container Events

Cada vez que el contenedor resuelve una dependencia, lanza estos eventos:

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 usar 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 no se puede resolver el identificador dado. La excepción será una instancia de Psr\Container\NotFoundExceptionInterface si el identificador nunca fue enlazado. Si el identificador fue enlazado pero no se pudo resolver, se lanzará una instancia de Psr\Container\ContainerExceptionInterface.