Service Container

I Service Container sono un componente fondamentale del framework Laravel. Il loro scopo è fornire un sistema pratico per iniettare le dipendenze nelle funzionalità.

Nell'ingegneria del software, la dependency injection è una tecnica in cui un metodo o una funzione riceve le sue dipendenze (cioè gli oggetti di cui ha bisogno per funzionare) come parametri. Questa tecnica ci permette di eliminare le dipendenze codificate, evitando di istanziare oggetti all’interno della classe, rendendo il nostro codice più flessibile.

Per fare un esempio, si consideri questo snippet senza dependency injection

class Address
{
    public function getAddress()
    {
        return 'This is my address';
    }
}
class GeoCoding
{
    public function getAddressCoordinates()
    {
        $dependency = new Address();
        return $dependency->getAddress();
    }
}

Lo stesso risultato si può ottenere in maniera più elegante ricorrendo alla dependency injection

class Address
{
    public function getAddress()
    {
        return 'This is my address';
    }
}
class GeoCoding
{
    public function getAddressCoordinates(Address $address)
    {
        return $address->getAddress();
    }
}

Il vantaggio di usare il secondo costrutto è chiaro: abbiamo la possibilità di cambiare il comportamento di una dipendenza, senza dover mettere le mani su tutte le classi che la incapsulano.

Questo esempio è molto semplice, la dipendenza che dobbiamo iniettare nella nostra classe non ha una configurazione iniziale. In un caso più complesso, in cui dobbiamo implementare il costruttore della dipendenza, dobbiamo necessariamente usare un IoC Container. Per definizione, il contenitore IoC è un componente software che fornisce servizi ad altri componenti senza che questi ultimi debbano essere a conoscenza del modo in cui tali servizi vengono forniti.

Non preoccupiamo più di tanto di questa definizione, anche perchè nel core di Laravel questo Container già esiste e funziona benissimo. Noi dobbiamo solo utilizzarlo.

Supponiamo che la classe Address abbia bisogno di un parametro nel costruttore per funzionare correttamente, ad esempio di un’API key. Quindi la classe si presenterà in questa maniera.

class Address
{
    protected $apiKey;
    public function __construct($apiKey)
    {
        $this->apiKey = $apiKey;
    }
    public function getAddress()
    {
        return 'This is my address';
    }
}

Per istanziare questa classe e poterla iniettare senza problemi nella classe GeoCoding, possiamo usare il Container di Laravel, disponibile nel metodo register del Service Provider che abbiamo analizzato nella sezione precedente.

class AppServiceProvider extends ServiceProvider
{
    /**
    * Register any application services.
    * @return void
    */
    public function register()
    {
        $this->app->bind(Address::class, concrete: function() {
            $apiKey = '123456789';
            return new Address($apiKey);
        });
    }
}

Esiste un ulteriore vantaggio usando questo sistema: al container possiamo passare anche un’interfaccia o una classe astratta, in quanto ci penserà lui a risolvere l'implementazione concreta. In questo modo possiamo “allentare” l'accoppiamento tra i vari componenti e rendere il codice più modulare ed estensibile.

Facciamo un esempio pratico di quanto appena esposto, creando un servizio per gestire i social network. Nella cartella Extensions creiamo due sottocartelle: “Contracts” e “Services”. Laravel usa il termine Contract per indicare le interfacce, quindi atteniamoci a questo standard.

Creiamo il contratto nella cartella Contracts, definendo un paio di metodi

namespace App\Extensions\Contracts;
interface SocialContract
{
    public function share();
    public function like();
}

E successivamente definiamo la classe Social nella cartella Services

namespace App\Extensions\Services;
use App\Extensions\Contracts\SocialContract;
class Social implements SocialContract
{
    protected $url;
    public function __construct($url)
    {
        $this->url = $url;
    }
    public function share()
    {
        return 'You shared this content ' . $this->url;
    }
    public function like()
    {
        return 'You liked this content ' . $this->url;
    }
}

Nel Service Provider utilizziamo l’IoC Container per legare i due componenti

namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Extensions\Services\Social;
use App\Extensions\Contracts\SocialContract;
class AppServiceProvider extends ServiceProvider
{
    /**
    * Register any application services.
    * @return void
    */
    public function register()
    {
        $this->app->bind(SocialContract::class, concrete: function() {
            $url = 'https://www.mrw.it';
            return new Social($url);
        });
    }
}

Adesso dobbiamo semplicemente iniettare il Contratto come parametro nella classe deputata al suo utilizzo. Visto che abbiamo definito una Facade nel capitolo precedente, useremo la relativa classe reale che si nasconde dietro la facciata. Implementiamo il costruttore in modo che riceva il nostro Servizio e modifichiamo il metodo hello() per avere un ritorno a video.

namespace App\Extensions\Classes;
use App\Extensions\Contracts\SocialContract;
class Test
{
    private SocialContract $social;
    public function __construct(SocialContract $social)
    {
        $this->social = $social;
    }
    public function hello()
    {
        dd($this->social);
    }
}

Torniamo al ServiceProvider e facciamo la magia. Modifichiamo il binding tra Facade e Classe Concreta implementato nello scorso capitolo, passando al costruttore il Service Container creato poco fa, che possiamo recuperare attraverso il metodo get() della classe Illuminate\Foundation\Application, l’istanza del framework.

public function register()
{
    $this->app->bind(SocialContract::class, concrete: function() {
        $url = 'https://www.mrw.it';
        return new Social($url);
    });
    $this->app->bind('testKey', fn($app) => new Test($app->get(SocialContract::class)));
}

Il risultato sarà quello atteso, il tutto grazie all’IoC Container di Laravel.