Symfony 4: i servizi

La programmazione orientata ai servizi (SOP) è un paradigma molto noto e diffuso in diversi ambiti dell’informatica. Com’è intuibile dal nome, tutto ruota attorno al concetto di servizio, un’unità di lavoro che fornisce una certa funzionalità. In pratica è un componente che si occupa di un determinato aspetto: potrebbe esserci un servizio per gestire l’autenticazione, uno per interfacciarsi al database, uno per scrivere dei log e così via.

Anche in Symfony esiste il concetto di servizio, con cui è bene che prendiate confidenza fin da subito, perché, nella stesura delle nostre webapp, la best practice sarebbe quella di mantenere l’intera logica dell’applicazione distribuita proprio in questi “contenitori di funzionalità”. Vuol dire che, dal punto di vista del pattern MVC, i servizi sono parte del Model. Per essere più chiari, ma anche più drastici, il compito dell’action dovrebbe essere quello di richiamare i servizi che servono e mandare i risultati alla View.

Creare un servizio

Passiamo al lato pratico: in cosa diavolo consiste un servizio? Anche se andrebbe visto come concetto più astratto, possiamo dire che in Symfony corrisponde ad un semplice oggetto PHP.

Creare un servizio è molto semplice. Non ci sono vincoli particolari: nessuna classe da ereditare, nessuna interfaccia da implementare, non c’è una nomenclatura da seguire. Basta definire le classi in src, preferibilmente in un sottolivello, ad esempio in una nuova directory src/Services. Basta dare un’occhiata alla configurazione per renderci conto che tutto ciò che sta in src, al di fuori di alcune directory, come quella delle Entity, è in realtà un servizio:

# config/services.yaml
# ...
App\:
    resource: '../src/*'
    exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

Proviamo a sporcarci le mani creando un servizio per generare dei token random alfanumerici, come d0a6476edd2503152cefc85c0, oppure interi, come 89344004646103:

<?php
// src/Services/TokenGenerator.php
namespace App\Services;

class TokenGenerator
{
	public function genAlphanumToken($nChars = 100) : string
	{
		$bytes = $nChars / 2;
		$token = bin2hex(random_bytes($bytes));
		return $token;
	}

	public function genIntegerToken() : int
	{
		$randomInt = random_int(10000000000000, 99999999999999);
		return $randomInt;
	}
}

Come usare un servizio

Proviamo ad usare il servizio appena creato in un’Action:

<?php
// src/Controller/DefaultController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use App\Services\TokenGenerator;

/**
 * @Route("/")
 */
class DefaultController extends AbstractController
{
	/**
	 * @Route("/random", name="random")
	 */
	public function random(TokenGenerator $tokenGenerator)
	{
		dump($tokenGenerator->genAlphanumToken());
		dump($tokenGenerator->genIntegerToken());

		return $this->render('default.html.twig');
	}
}

Con un po’ di intuito è facile capire che, appena entriamo nel blocco dell’action “random”, abbiamo un oggetto di classe TokenGenerator pronto all’uso. L’onere di creare l’istanza non è spettatato a noi: Symfony ha capito ciò di cui avevamo bisogno e ha fatto tutto dietro le quinte! Vedremo tra poco come questo sia possibile.

Iniettare servizi nei servizi

La realtà può essere complicata ed è plausibile che un servizio abbia bisogno di utilizzare altri servizi, in pratica che abbia delle dipendenze. Supponiamo di avere un servizio per gestire autenticazione e autorizzazione degli utenti e prendiamo in considerazione la funzionalità “ho dimenticato la password”, che tutti conosciamo. Il componente potrebbe aver bisogno anche del servizio di gestione delle e-mail (App\Mailer), per inviare all’utente il link di recupero della password, e magari del servizio che abbiamo creato prima (App\TokenGenerator), per generare un token randomico per il link di reset della password:

<?php

namespace App\Services;

use App\Services\Mailer;
use App\Services\TokenGenerator;

class UserManager
{
	private $mailer;
	private $tokenGenerator;

	/* Qui avviene il passaggio delle dipendenze */
	public function __construct(Mailer $mailer, TokenGenerator $tokenGenerator)
	{
		$this->mailer = $mailer;
		$this->tokenGenerator = $tokenGenerator;
	}

	public function sendResetPasswordEmail(string $email) : bool
	{
		$token = $this->tokenGenerator->genAlphanumToken(100);
		$isSuccessful = $this->mailer->sendResetPassword($email, $token);
		return $isSuccessful;
	}
}

Come potete vedere, è bastato specificare come argomenti del costruttore due parametri di tipo Mailer e TokenGenerator per poter usare i servizi corrispondenti. Se il vostro scopo è quello di imparare ad usare i servizi, abbiamo già finito! Tuttavia vi consiglio di continuare nella lettura se volete conoscere alcuni meccanismi che si nascondono dietro a quest’apparente semplicità e che serviranno ad introdurre problematiche di cui ci faremo carico in futuro, parlando di performance e ottimizzazioni.

Service Container e Autowiring

Nei casi illustrati è bastato specificare nell’action del controller o nel costruttore della classe gli argomenti tipizzati. Symfony ha fatto il resto: ha capito che vogliamo utilizzare dei servizi, li ha inizializzati e poi li ha iniettati nelle funzioni. Insomma, se dal punto di vista dell’utilizzatore sembra tutto semplicissimo, in realtà dietro c’è un lavoro non banale! Ma come ha fatto?

Innanzitutto Symfony attribuisce ad ogni servizio un ID univoco e lo inserisce in un componente, detto Service Container, che gestisce tutti i servizi e la loro configurazione: aliasing, visibilità (private/public) e così via. Il meccanismo con cui il framework deduce che stiamo richiedendo un servizio, invece, si chiama autowiring ed è una vera manna. È stato introdotto solo a partire dalle ultime versioni di Symfony 3 ed ha reso le cose molto più fluide. Prima bisognava necessariamente dichiarare ogni servizio nella configurazione e specificare uno per uno i servizi da iniettare. Era una seccatura che portava via minuti preziosi! Inoltre, il meccanismo dell’autowiring non aggiunge alcun overhead al runtime, grazie al fatto che Symfony compila il Service Container e se lo tiene nella sua cache. Perciò, per rispetto ai valorosi programmatori caduti, prendete e sbavatene tutti.

Depedency Injection

Dopo che l’autowiring ha dedotto la richiesta dei servizi, questi devono essere iniettati. La tecnica con cui ciò avviene si chiama Dependency Injection, che è un pattern della programmazione (che esula dall’ambito di Symfony) in cui ci sono due attori: client e servizi. Il servizio è un punto di accesso che offre certe funzionalità (praticamente quello di cui abbiamo parlato fino ad ora). Il client invece è l’utilizzatore dei servizi, quello che fa le richieste. Insomma, si tratta del classico approccio client/server. L’assunto fondamentalmente, però, è che il client non è interessato a costruire il servizio o a sapere cosa c’è dentro: gli basta sfruttare una certa funzionalità. Dunque, invece di lasciare al client il compito di istanziare gli oggetti che costituiscono il servizio, ci sarà un componente, detto iniettore, che si occupa di passargli un’oggetto utilizzabile. Grazie a questo meccanismo, da una parte si rispetta il principio della Separation of Concerns (SoC) e dall’altra si rende più semplice scrivere codice.

Torneremo ancora sui servizi, ma quanto detto è sufficiente per cominciare a dare carattere alle vostre webapp!