Service Container

Introduzione

Il service container è uno strumento potente per gestire le dipendenze delle classi ed implementare la dependency injection. La dependency injection è un termine elegante che in sostanza significa questo: le dipendenze di una classe vengono "iniettate" nella classe tramite il costruttore o, in alcuni casi, attraverso metodi "setter".

Vediamo un esempio:

<?php

namespace App\Http\Controllers;

use App\Services\AppleMusic;
use Illuminate\View\View;

class PodcastController extends Controller
{
    /**
     * Crea una nuova istanza del controller.
     */
    public function __construct(
        protected AppleMusic $apple,
    ) {}

    /**
     * Mostra le informazioni sul podcast specificato.
     */
    public function show(string $id): View
    {
        return view('podcasts.show', [
            'podcast' => $this->apple->findPodcast($id)
        ]);
    }
}

In questo esempio, il PodcastController ha bisogno di recuperare i podcast da una fonte di dati come Apple Music. Quindi, iniettiamo un servizio in grado di ottenere i podcast. Poiché il servizio viene iniettato, possiamo facilmente "mockare", ovvero creare un’implementazione fittizia del servizio AppleMusic durante i test della nostra applicazione.

Una profonda comprensione del service container è essenziale per costruire applicazioni potenti e di grandi dimensioni, nonché per contribuire al core di Laravel stesso.

Risoluzione senza configurazione

Se una classe non ha dipendenze o dipende solo da altre classi concrete (non interfacce), il container non ha bisogno di istruzioni su come risolvere quella classe. Ad esempio, puoi inserire il seguente codice nel tuo file routes/web.php:

<?php

class Service
{
    // ...
}

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

In questo esempio, accedendo alla rotta / della tua applicazione, la classe Service verrà automaticamente risolta e iniettata nel gestore della tua rotta. Significa che puoi sviluppare la tua applicazione e sfruttare l’iniezione delle dipendenze senza preoccuparti di file di configurazione carichi di istruzioni.

Per fortuna, molte delle classi che scriverai quando costruisci un’applicazione Laravel ricevono automaticamente le loro dipendenze tramite il container, inclusi controller, event listener, middleware e altro. Inoltre, puoi specificare le dipendenze nel metodo handle dei tuoi job. Una volta provata la potenza dell’iniezione delle dipendenze automatica e senza configurazione, ti sembrerà impossibile sviluppare senza.

Quando Usare il Container

Grazie alla risoluzione senza configurazione, spesso indicherai le dipendenze in route, controller, listener di eventi e altrove senza mai interagire manualmente con il container. Ad esempio, potresti indicare l’oggetto Illuminate\Http\Request nella definizione della tua route per accedere facilmente alla richiesta corrente. Anche se non dobbiamo mai interagire con il container per scrivere questo codice, questo gestisce l’iniezione di queste dipendenze dietro le quinte:

use Illuminate\Http\Request;

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

In molti casi, grazie all’iniezione automatica delle dipendenze e alle facades, puoi costruire applicazioni Laravel senza mai legare o risolvere manualmente nulla dal container. Allora, quando dovresti "usare manualmente" il container? Esaminiamo due situazioni.

Innanzitutto, se scrivi una classe che implementa un’interfaccia e vuoi indicare quella interfaccia su una route o sul costruttore di una classe, devi dire al container come risolvere quell’interfaccia. Lo vedremo a breve. In secondo luogo, se stai scrivendo un package Laravel che intendi condividere con altri sviluppatori Laravel, potrebbe essere necessario definire i servizi del tuo package nel proprio container.

Binding

Il service container è uno strumento potente per gestire le dipendenze delle classi e eseguire l’iniezione delle dipendenze. In questa sezione, scopriremo meglio come associare interfacce a implementazioni e risolverle tramite il container.

Fondamenti del Binding

Associazioni Semplici

Quasi tutte le associazioni del tuo service container saranno registrate all’interno dei service provider, quindi la maggior parte degli esempi utilizzerà il container in questo contesto.

In un service provider hai sempre accesso al container tramite la proprietà $this->app. Possiamo registrare un’associazione usando il metodo bind, passando il nome della classe o dell’interfaccia che vogliamo registrare insieme a una closure che restituisce un’istanza della classe:

    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: riceviamo il container stesso come argomento del risolutore. Possiamo quindi usare il container per risolvere le sotto-dipendenze dell’oggetto che stiamo creando.

Come accennato, di solito interagirai con il container all’interno dei service provider; tuttavia, se desideri interagire con il container al di fuori di un service provider, puoi farlo tramite la facade App:

    use App\Services\Transistor;
    use Illuminate\Contracts\Foundation\Application;
    use Illuminate\Support\Facades\App;

    App::bind(Transistor::class, function (Application $app) {
        // ...
    });

Puoi usare il metodo bindIf per registrare un’associazione nel container solo se non è già stata registrata un’associazione per quel tipo:

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

Non è necessario associare le classi al container se non dipendono da alcuna interfaccia. Il container non ha bisogno di istruzioni su come costruire questi oggetti, poiché può risolverli automaticamente usando la reflection.

Binding di un Singleton

Il metodo singleton associa una classe o un’interfaccia al container che deve essere risolta una sola volta. Una volta che un binding singleton è stato risolto, la stessa istanza dell’oggetto verrà restituita nelle chiamate successive al container:

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

Puoi utilizzare il metodo singletonIf per registrare un binding singleton nel container solo se non è già stato registrato un binding per il tipo specificato:

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

Binding di Scoped Singleton

Il metodo scoped associa una classe o interfaccia nel container che deve essere risolta una sola volta all’interno di un determinato ciclo di vita di richiesta o job di Laravel. Sebbene questo metodo sia simile al metodo singleton, le istanze registrate usando il metodo scoped verranno eliminate ogni volta che l’applicazione Laravel inizia un nuovo "ciclo di vita", come quando un worker di Laravel Octane elabora una nuova richiesta o quando un queue worker di Laravel elabora un nuovo 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));
});

Puoi usare il metodo scopedIf per registrare un binding scoped nel container solo se non è già stato registrato un binding per il tipo specificato:

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

Binding di Istanze

È possibile legare un’istanza di oggetto esistente nel container utilizzando il metodo instance. L’istanza fornita verrà sempre restituita nelle chiamate successive al contenitore:

use App\Services\Transistor;
use App\Services\PodcastParser;

$service = new Transistor(new PodcastParser);

$this->app->instance(Transistor::class, $service);

Binding delle Interfacce alle Implementazioni

Una funzionalità molto potente del service container è la sua capacità di associare un’interfaccia a una specifica implementazione. Per esempio, supponiamo di avere un’interfaccia EventPusher e un’implementazione RedisEventPusher. Dopo aver codificato la nostra implementazione RedisEventPusher di questa interfaccia, possiamo registrarla nel service container in questo modo:

    use App\Contracts\EventPusher;
    use App\Services\RedisEventPusher;

    $this->app->bind(EventPusher::class, RedisEventPusher::class);

Questa istruzione dice al container di iniettare RedisEventPusher quando una classe ha bisogno di un’implementazione di EventPusher. Ora possiamo tipizzare l’interfaccia EventPusher nel costruttore di una classe risolta dal container. Ricorda, controller, listener di eventi, middleware e vari altri tipi di classi nelle applicazioni Laravel sono sempre risolti usando il container:

    use App\Contracts\EventPusher;

    /**
     * Crea una nuova istanza della classe.
     */
    public function __construct(
        protected EventPusher $pusher,
    ) {}

Binding Contestuale

A volte potresti avere due classi che utilizzano la stessa interfaccia, ma desideri iniettare implementazioni diverse in ciascuna classe. Ad esempio, due controller potrebbero dipendere da implementazioni differenti del contratto Illuminate\Contracts\Filesystem\Filesystem. Laravel offre un’interfaccia semplice e fluente per definire questo comportamento:

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

Attributi Contestuali

Poiché il binding contestuale viene spesso usato per iniettare implementazioni di driver o valori di configurazione, Laravel offre una varietà di attributi di binding contestuale che permettono di iniettare questi tipi di valori senza definire manualmente i binding contestuali nei tuoi service provider.

Ad esempio, l’attributo Storage può essere usato per iniettare uno specifico storage disk:

<?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
		) {
    // ...
		}
}

Oltre all’attributo Storage, Laravel offre gli attributi Auth, Cache, Config, DB, Log, RouteParameter e Tag:

<?php

namespace App\Http\Controllers;

use App\Models\Photo;
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\RouteParameter;
use Illuminate\Container\Attributes\Tag;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\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,
			#[RouteParameter('photo')] protected Photo $photo,
			#[Tag('reports')] protected iterable $reports,
		)
		{
			// ...
		}
}			

Inoltre, Laravel fornisce un attributo CurrentUser per iniettare l’utente attualmente autenticato in una determinata route o classe:

use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;

Route::get('/user', function (#[CurrentUser] User $user) {
    return $user;
})->middleware('auth');

Definizione di Attributi Personalizzati

Puoi creare i tuoi attributi contestuali implementando il contratto Illuminate\Contracts\Container\ContextualAttribute. Il container chiamerà il metodo resolve del tuo attributo, risolvendo il valore da iniettare nella classe che utilizza l’attributo. Nell’esempio seguente, reimplementeremo l’attributo integrato Config di Laravel:

<?php

namespace App\Attributes;

use Illuminate\Contracts\Container\ContextualAttribute;

#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements ContextualAttribute
{
    /**
     * Crea una nuova istanza dell'attributo.
     */
    public function __construct(public string $key, public mixed $default = null)
    {
    }

    /**
     * Risolve il valore di configurazione.
     *
     * @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 delle Primitive

A volte potresti avere una classe che riceve alcune classi iniettate, ma ha anche bisogno di un valore primitivo iniettato come, ad esempio, un intero. Puoi facilmente usare il binding contestuale per iniettare qualsiasi valore di cui la tua classe possa necessitare:

    use App\Http\Controllers\UserController;

    $this->app->when(UserController::class)
              ->needs('$variableName')
              ->give($value);

A volte una classe può dipendere da un array di istanze taggate. Usando il metodo giveTagged, puoi facilmente iniettare tutte le associazioni del container con quel tag:

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

Se hai bisogno di iniettare un valore da uno dei file di configurazione della tua applicazione, puoi usare il metodo giveConfig:

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

Binding di Variadici Tipizzati

A volte, potresti avere una classe che riceve un array di oggetti tipizzati utilizzando un argomento del costruttore variadico:

<?php

use App\Models\Filter;
use App\Services\Logger;

class Firewall
{
    /**
     * Le istanze dei filtri.
     *
     * @var array
     */
    protected $filters;

    /**
     * Crea una nuova istanza della classe.
     */
    public function __construct(
        protected Logger $logger,
        Filter ...$filters,
    ) {
        $this->filters = $filters;
    }
}

Usando il binding contestuale, puoi risolvere questa dipendenza fornendo al metodo give una closure che ritorna un array di istanze Filter risolte:

$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),
                ];
          });

Per comodità, puoi anche semplicemente fornire un array di nomi di classi da risolvere dal container ogni volta che Firewall necessita di istanze di Filter:

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

Dipendenze Variadiche con Tag

A volte una classe può avere una dipendenza variadica tipizzata come una certa classe (Report ...$reports). Utilizzando i metodi needs e giveTagged, puoi facilmente iniettare tutte le binding del contenitore con quel tag per la dipendenza specificata:

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

Tagging

Occasionalmente, potrebbe essere necessario risolvere tutte le binding di una certa "categoria". Ad esempio, se stai creando un analizzatore di report che riceve un array di diverse implementazioni dell’interfaccia Report. Dopo aver registrato le implementazioni di Report, puoi assegnare loro un tag usando il metodo tag:

$this->app->bind(CpuReport::class, function () {
    // ...
});

$this->app->bind(MemoryReport::class, function () {
    // ...
});

$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');

Una volta che i servizi sono stati taggati, puoi facilmente risolverli tutti tramite il metodo tagged del container:

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

Estendere i Binding

Il metodo extend permette di modificare i servizi risolti. Ad esempio, quando un servizio viene risolto, puoi eseguire codice aggiuntivo per decorare o configurare il servizio. Il metodo extend accetta due argomenti: la classe del servizio che stai estendendo e una closure che deve restituire il servizio modificato. La closure riceve il servizio in fase di risoluzione e l’istanza del container:

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

Risoluzione

Il metodo make

Puoi usare il metodo make per ottenere un’istanza di una classe dal container. Il metodo make accetta il nome della classe o dell’interfaccia che desideri risolvere:

use App\Services\Transistor;

$transistor = $this->app->make(Transistor::class);

Se alcune dipendenze della tua classe non possono essere risolte tramite il container, puoi iniettarle passando un array associativo nel metodo makeWith. Ad esempio, possiamo passare manualmente l’argomento $id richiesto dal costruttore del servizio Transistor:

use App\Services\Transistor;

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

Il metodo bound può essere usato per verificare se una classe o un’interfaccia è stata esplicitamente legata nel container:

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

Se ti trovi fuori da un service provider, in una parte del tuo codice che non ha accesso alla variabile $app, puoi usare il facade App oppure l’helper app per ottenere un’istanza di una classe dal container:

use App\Services\Transistor;
use Illuminate\Support\Facades\App;

$transistor = App::make(Transistor::class);

$transistor = app(Transistor::class);

Se desideri che l’istanza del container stesso di Laravel venga iniettata in una classe risolta dal contenitore, puoi tipizzare la classe Illuminate\Container\Container nel costruttore della tua classe:

use Illuminate\Container\Container;

/**
 * Crea una nuova istanza della classe.
 */
public function __construct(
    protected Container $container,
) {}

Iniezione Automatica

Ricordati, inoltre, che puoi usare il type-hint per la dipendenza nel costruttore di una classe che viene risolta dal container, inclusi controller, event listeners, middleware e altro. Inoltre, puoi usare il type-hint per le dipendenze nel metodo handle di un job.

Ad esempio, puoi usare il type-hint per un servizio definito dalla tua applicazione nel costruttore di un controller. Il servizio verrà automaticamente risolto e iniettato nella classe:

<?php

namespace App\Http\Controllers;

use App\Services\AppleMusic;

class PodcastController extends Controller
{
    /**
     * Crea una nuova istanza del controller.
     */
    public function __construct(
        protected AppleMusic $apple,
    ) {}

    /**
     * Mostra informazioni sul podcast specificato.
     */
    public function show(string $id): Podcast
    {
        return $this->apple->findPodcast($id);
    }
}

Invocazione di Metodi e Iniezione

A volte potresti voler invocare un metodo su un’istanza di oggetto consentendo al container di iniettare automaticamente le dipendenze di quel metodo. Ad esempio, data la seguente classe:

<?php

namespace App;

use App\Services\AppleMusic;

class PodcastStats
{
    /**
     * Genera un nuovo rapporto delle statistiche del podcast.
     */
    public function generate(AppleMusic $apple): array
    {
        return [
            // ...
        ];
    }
}

Puoi invocare il metodo generate tramite il container in questo modo:

use App\PodcastStats;
use Illuminate\Support\Facades\App;

$stats = App::call([new PodcastStats, 'generate']);

Il metodo call accetta qualsiasi callable PHP. Il metodo call del container può anche essere usato per invocare una closure mentre inietta automaticamente le sue dipendenze:

use App\Services\AppleMusic;
use Illuminate\Support\Facades\App;

$result = App::call(function (AppleMusic $apple) {
    // ...
});

Eventi del Container

Il service container emette un evento ogni volta che risolve un oggetto. Puoi ascoltare questo evento usando il metodo resolving:

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;

$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
    // Chiamato quando il container risolve oggetti di tipo "Transistor"...
});

$this->app->resolving(function (mixed $object, Application $app) {
    // Chiamato quando il container risolve un oggetto di qualsiasi tipo...
});

Come puoi vedere l’oggetto che viene risolto sarà passato al callback, permettendoti di impostare eventuali proprietà aggiuntive sull’oggetto prima che venga fornito al suo utilizzatore.

Rebinding

Il metodo rebinding ti permette di ascoltare quando un servizio viene ri-associato al container, ossia registrato nuovamente o sovrascritto dopo la sua associazione iniziale. Questo può essere utile quando hai bisogno di aggiornare le dipendenze o modificare il comportamento ogni volta che una specifica associazione viene aggiornata:

use App\Contracts\PodcastPublisher;
use App\Services\SpotifyPublisher;
use App\Services\TransistorPublisher;
use Illuminate\Contracts\Foundation\Application;

$this->app->bind(PodcastPublisher::class, SpotifyPublisher::class);

$this->app->rebinding(
    PodcastPublisher::class,
    function (Application $app, PodcastPublisher $newInstance) {
        //
    },
);

// La nuova associazione attiverà la chiusura di rebinding...
$this->app->bind(PodcastPublisher::class, TransistorPublisher::class);

PSR-11

Il service container di Laravel implementa l’interfaccia PSR-11. Pertanto, puoi utilizzare il type-hint dell’interfaccia PSR-11 container per ottenere un’istanza del container di Laravel:

use App\Services\Transistor;
use Psr\Container\ContainerInterface;

Route::get('/', function (ContainerInterface $container) {
    $service = $container->get(Transistor::class);

    // ...
});

Verrà lanciata un’eccezione se l’identificatore fornito non può essere risolto. L’eccezione sarà un’istanza di Psr\Container\NotFoundExceptionInterface se l’identificatore non è mai stato legato. Se l’identificatore è stato legato ma non è stato possibile risolverlo, verrà lanciata un’istanza di Psr\Container\ContainerExceptionInterface.

Lascia un commento

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *