Il componente HttpKernel

Il componente HttpKernel fornisce un processo strutturato per convertire una Request in una Response, usando il distributore di eventi. È abbastanza flessibile per creare un framework completo (Symfony), un micro-framework (Silex) o un CMS avanzato (Drupal).

Installazione

È possibile installare il componente in due modi:

Quindi, richiedere il file vendor/autoload.php per abilitare il meccanismo di auto-caricamento fornito da Composer. Altrimenti, l’applicazione non sarà in grado di trovare le classi di questo componente di Symfony.

Il flusso di una richiesta

Ogni interazione HTTP inizia con una richiesta e finisce con una risposta. Il compito dello sviluppatore è quello di creare codice PHP che legga le informazioni della richiesta (p.e. l’URL) e crei e restituisca una risposta (p.e. una pagina HTML o una stringa JSON).

../../_images/request-response-flow.png

Di solito, si costruisce una sorta di framework o sistema per gestire tutte le operazioni ripetitive (come rotte, sicurezza, ecc.), in modo che uno sviluppatore possa costruire facilmente ogni pagina dell’applicazione. Come esattamente tali sistemi siano costruiti varia enormemente, Il componente HttpKernel fornisce un’interfaccia che formalizza il processo di iniziare con una richiesta e creare la risposta appropriata. Il componente è pensato per essere il cuore di qualsiasi applicazione o framework, non importa quanto vari l’architettura di tale sistema:

namespace Symfony\Component\HttpKernel;

use Symfony\Component\HttpFoundation\Request;

interface HttpKernelInterface
{
    // ...

    /**
     * @return Response Un'istanza di Response
     */
    public function handle(
        Request $request,
        $type = self::MASTER_REQUEST,
        $catch = true
    );
}

Internamente, HttpKernel::handle(), l’implementazione concreta di HttpKernelInterface::handle(), definisce un flusso che inizia con una Symfony\Component\HttpFoundation\Request e finisce con una Symfony\Component\HttpFoundation\Response.

../../_images/01-workflow.png

I dettagli precisi di tale flusso sono la chiave per capire come funzioni il kernel (e il framework Symfony o qualsiasi altra libreria che usi il kernel).

HttpKernel: guidato da eventi

Il metodo HttpKernel::handle() funziona internamente distribuendo eventi. Questo rende il metodo flessibile, ma anche un po’ astratto, poiché tutto il “lavoro” di un framework/applicazione costruiti con HttpKernel è effettivamente svolto da ascoltatori di eventi.

Per aiutare nella spiegazione di questo processo, questo documento ne analizza ogni passo e spiega come funziona una specifica implementazione di HttpKernel, il framework Symfony.

All’inizio, l’uso di Symfony\Component\HttpKernel\HttpKernel è molto semplice e implica la creazione un distributore di eventi e un risolutore di controllori (spiegato più avanti). Per completare un kernel funzionante, si aggiungeranno ulteriori ascoltatori agli eventi discussi sotto:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;

// creare l'oggetto Request
$request = Request::createFromGlobals();

$dispatcher = new EventDispatcher();
// ... aggiungere degli ascoltatori

// creare un risolutore di controllori
$resolver = new ControllerResolver();
// istanziare il kernel
$kernel = new HttpKernel($dispatcher, $resolver);

// esegue effettivamente il kernel, che trasforma la richiesta in una risposta
// distribuendo eventi, richiamando un controllore e restituendo la risposta
$response = $kernel->handle($request);

// mostra il contenuto e invia gli header
$response->send();

// lancia l'evento kernel.terminate
$kernel->terminate($request, $response);

Vedere “Un esempio funzionante” per un’implementazione più concreta.

Per informazioni generali sull’aggiunta di ascoltatori agli eventi qui sotto, vedere Creare un ascoltatore di eventi.

Suggerimento

Fabien Potencier ha anche scritto una bella serie sull’uso del componente HttpKernel e altri componenti di Symfony per creare un proprio framework. Vedere Create your own framework... on top of the Symfony2 Components.

1) L’evento kernel.request

Scopi tipici: Aggiungere più informazioni alla Request, inizializzare le parti del sistema oppure restituire una Response se possibile (p.e. un livello di sicurezza che nega accesso)

Tabella informativa sugli eventi del kernel

Il primo evento distribuito in HttpKernel::handle è kernel.request, che può avere vari ascoltatori.

../../_images/02-kernel-request.png

Gli ascoltatori di questo evento possono essere alquanti vari. Alcuni, come un ascoltatore di sicurezza, possono avere informazioni sufficienti a creare un oggetto Response immediatamente. Per esempio, se un ascoltatore di sicurezza determina che un utente non deve accedere, può restituire un Symfony\Component\HttpFoundation\RedirectResponse alla pagina di login o una risposta 403 (accesso negato).

Se a questo punto viene restituita una Response, il processo salta direttamente all’evento kernel.response.

../../_images/03-kernel-request-response.png

Altri ascoltatori inizializzano semplicemente alcune cose o aggiungono ulteriori informazioni alla richiesta. Per esempio, un ascoltatore può determinare e impostare il locale sull’oggetto Request.

Un altro ascoltatore comune è il routing. Un ascoltatore di rotte può processare la Request e determinare il controllore da rendere (vedere la sezione successiva). In effetti, l’oggetto Request ha un insieme di “attributi”, che è il posto ideale per memorizzare tali dati, ulteriori e specifici dell’applicazione, sulla richiesta. Questo vuol dire che se un ascoltatore di rotte determina in qualche modo il controllore, può memorizzarlo negli attributi di Request (che possono essere usati dal risolutore di controllori).

Complessivamente, lo scopo dell’evento kernel.request è quello di creare e restituire una Response direttamente oppure di aggiungere informazioni alla Request (p.e. impostando il locale o altre informazioni sugli attributi della Request).

Nota

Quando si imposta una risposta per l’evento kernel.request, la propagazione si ferma. Questo vuol dire che ascoltatori con priorità inferiore non saranno eseguiti.

2) Risolvere il controllore

Ipotizzando che nessun ascoltatore di kernel.request sia stato in grado di creare una Response, il passo successivo in HttpKernel è determinare e preparare (cioè risolvere) il controllore. Il controllore è la parte del codice dell’applicazione finale responsabile di creare e restituire la Response per una pagina specifica. L’unico requisito è che sia un callable PHP, cioè una funzione, un metodo su un oggetto o una Closure.

Ma come determinare l’esatto controllore per una richiesta è un compito che spetta all’applicazione. Questo è il lavoro del “risolutore di controllori”, una classe che implementa Symfony\Component\HttpKernel\Controller\ControllerResolverInterface ed è uno dei parametri del costruttore di HttpKernel.

../../_images/04-resolve-controller.png

Il compito dello sviluppatore è creare una classe che implementi l’interfaccia e quindi i suoi due metodi: getController e getArguments. In effetti, esiste già un’implementazione, che si può usare direttamente o a cui ci si può ispirare: Symfony\Component\HttpKernel\Controller\ControllerResolver. Tale implementazione è spiegata qui sotto:

namespace Symfony\Component\HttpKernel\Controller;

use Symfony\Component\HttpFoundation\Request;

interface ControllerResolverInterface
{
    public function getController(Request $request);

    public function getArguments(Request $request, $controller);
}

Internamente, il metodo HttpKernel::handle prima richiama getController() sul risolutore di controllori. A questo viene passata la Request ed è responsabile di determinare in qualche modo e restituire un callable PHP (il controllore) in base alle informazioni della richiesta.

Il secondo metodo, getArguments(), sarà richiamato dopo che un altro evento, kernel.controller, è stato distribuito.

3) L’evento kernel.controller

Scopi tipici: Inizializzare cose o cambiare il controllore subito prima che il controllore venga eseguito.

Tabella informativa sugli eventi del kernel

Dopo che il callable controllore è stato determinato, HttpKernel::handle distribuisce l’evento kernel.controller. Gli ascoltatori di questo evento possono inizializzare alcune parti del sistema che devono essere inizializzate dopo che alcune cose sono state determinate (p.e. il controllore, informazioni sulle rotte) ma prima che il controllore sia eseguito. Per alcuni esempi, vedere la sezione Symfony più avanti.

../../_images/06-kernel-controller.png

Gli ascoltatori di questo evento possono anche cambiare completamente il callable controllore, richiamando FilterControllerEvent::setController sull’oggetto evento che viene passato agli ascoltatori di questo evento.

4) Ottenere i parametri del controllore

Quindi, HttpKernel::handle richiama getArguments(). Si ricordi che il controllore restituito in getController è un callable. Lo scopo di getArguments è restituire l’array di parametri che vanno passati a tale controllore. Il modo esatto in cui ciò sia realizzato spetta completamente alla progettazione dello sviluppatore, sebbene la classe Symfony\Component\HttpKernel\Controller\ControllerResolver ne sia un buon esempio.

../../_images/07-controller-arguments.png

A questo punto il kernel ha un callable PHP (il controllore) e un array di parametri che vanno passati durante l’esecuzione di tale callable.

5) Richiamare il controllore

Il prossimo passo è semplice! HttpKernel::handle esegue il controllore.

../../_images/08-call-controller.png

Il compito del controllore è costruire la risposta per la risorsa data. Potrebbe essere una pagina HTML, una stringa JSON o qualsiasi altra cosa. Diversamente dalle altre parti del processo viste finora, questo passo è implementato dallo sviluppatore, per ogni pagina da costruire.

Di solito, il controllore restituirà un oggetto Response. Se questo è vero, il lavoro del kernel sta per finire! In questo caso, il prossimo passo è l’evento kernel.response.

../../_images/09-controller-returns-response.png

Se invece il controllore restituisce qualcosa che non sia una Response, il kernel deve fare ancora un piccolo lavoro, kernel.view (perché lo scopo finale è sempre generare un oggetto Response).

Nota

Un controllore deve restituire qualcosa. Se un controllore restituisce null, viene immediatamente lanciata un’eccezione.

6) L’evento kernel.view

Scopi tipici: Trasformare un valore diverso da Response restituito da un controllore in una Response

Tabella informativa sugli eventi del kernel

Se il controllore non restituisce un oggetto Response, iol kernel distribuisce un altro evento, kernel.view. Il compito di un ascoltatore di tale evento è di usare il valore restituito dal controllore (p.e. un array di dati o un oggetto) per creare una Response.

../../_images/10-kernel-view.png

Questo può essere utile se si vuole usare un livello “vista”: invece di restituire una Response dal controllore, si restituiscono dati che rappresentano la pagina. Un ascoltatore di questo evento può quindi usare tali dati per creare una Response che sia nel formato corretto (p.e. HTML, JSON, ecc.).

A questo punto, ne nessun ascoltatore imposta una risposta sull’evento, viene lanciata un’eccezione: o il controllore o uno degli ascoltatori della vista devono sempre restituire una Response.

Nota

Quando si imposta una risposta per l’evento kernel.request, la propagazione si ferma. Questo vuol dire che ascoltatori con priorità inferiore non saranno eseguiti.

7) L’evento kernel.response

Scopi tipici: Modificare l’oggetto Response subito prima che sia inviato

Tabella informativa sugli eventi del kernel

Lo scopo finale del kernel è trasformare una Request in una Response. La Response può essere creata durante l’evento kernel.request, restituita dal controllore oppure restituita da uno degli ascoltatori dell’evento kernel.view.

Indipendentemente da chi abbia creato la Response, un altro evento, kernel.response, viene distribuito subito dopo. Un tipico ascoltatore di questo evento modificherà l’oggetto Response in qualche modo, modificando header, aggiungendo cookie o anche cambiando il contenuto della Response stessa (p.e. iniettando del codice JavaScript prima della chiusura del tag </body> di una risposta HTML).

Dopo la distribuzione di questo evento, l’oggetto finale Response viene restituito da handle(). Nel caso d’uso più tipico, si può quindi richiamare il metodo send(), che invia gli header e stampa il contenuto della Response.

8) L’evento kernel.terminate

Scopi tipici: Eseguire qualche azione “pesante” dopo che la risposta sia stata inviata all’utente

Tabella informativa sugli eventi del kernel

L’evento finale processato da HttpKernel è kernel.terminate, che è unico, perché avviene dopo il metodo HttpKernel::handle e quindi dopo che la risposta è stata inviata all’utente. Ripreso da sopra, il codice usato dal kernel finisce in questo modo:

// mostra il contenuto e invia gli header
$response->send();

// lancia l'evento kernel.terminate
$kernel->terminate($request, $response);

Come si può vedere, richiamando $kernel->terminate dopo l’invio della risposta, si lancerà l’evento kernel.terminate, in cui si possono eseguire alcune azioni che potrebbero essere state rimandate, per poter restituire la risposta nel modo più veloce possibile al client (p.e. invio di email).

Attenzione

Internamente, HttpKernel fa uso della funzione fastcgi_finish_request di PHP. Questo vuole dire che, al momento, solo le API del server PHP FPM sono in grado di inviare al cliente una risposta mentre il processo PHP del server esegue ancora alcuni compiti. Con le API di ogni altro server, gli ascoltatori di kernel.terminate sono comunque eseguiti, ma la risposta non viene inviata al cliente finché non sono tutti completati.

Nota

L’uso dell’evento kernel.terminate è facoltativo e va limitato al caso in cui il kernel implementi Symfony\Component\HttpKernel\TerminableInterface.

Gestire le eccezioni:: l’evento kernel.exception

Scopi tipici: Gestire alcuni tipi di eccezioni e creare un’appropriata Response da restituire per l’eccezione

Tabella informativa sugli eventi del kernel

Se a un certo punto in HttpKernel::handle viene lanciata un’eccezione, viene lanciato un altro evento, kernel.exception. Internamente, il corpo della funzione handle viene avvolto da un blocco try-catch. Quando viene lanciata un’eccezione, l’evento kernel.exception viene distribuito, in modo che il proprio sistema possa in qualche modo rispondere all’eccezione.

../../_images/11-kernel-exception.png

A ogni ascoltatore di questo evento viene passato un oggetto Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent, che si può usare per accedere all’eccezione originale, tramite il metodo getException(). Un tipico ascoltatore di questo evento verificherà un certo tipo di eccezione e creerà un appropriata Response di errore.

Per esempio, per generare una pagina 404, si potrebbe lanciare uno speciale tipo di eccezione e quindi aggiungere un ascoltatore a tale evento, che cerchi l’eccezione e crei e restituisca una Response 404. In effetti, il componente HttpKernel dispone di un Symfony\Component\HttpKernel\EventListener\ExceptionListener, che, se usato, farà questo e anche di più in modo predefinito (si vedano dettagli più avanti).

Nota

Quando si imposta una risposta per l’evento kernel.request, la propagazione si ferma. Questo vuol dire che ascoltatori con priorità inferiore non saranno eseguiti.

Creare un ascoltatore di eventi

Come abbiamo visto, si possono creare e attaccare ascoltatori di eventi a qualsiasi evento distribuito durante il ciclo HttpKernel::handle. Un tipico ascoltatore è una classe PHP con un metodo da eseguire, ma può essere qualsiasi cosa. Per maggiori informazioni su come creare e attaccare ascoltatori di eventi, si veda Il componente EventDispatcher.

Il nome di ogni evento del kernel è definito come costante della classe Symfony\Component\HttpKernel\KernelEvents. Inoltre, a ogni ascoltatore di evento viene passato un singolo parametro, che è una sotto-classe di Symfony\Component\HttpKernel\Event\KernelEvent. Questo oggetto contiene informazioni sullo stato attuale del sistema e ogni vento ha il suo oggetto evento:

Nome Costante KernelEvents Parametro passato all’ascoltatore
kernel.request KernelEvents::REQUEST Symfony\Component\HttpKernel\Event\GetResponseEvent
kernel.controller KernelEvents::CONTROLLER Symfony\Component\HttpKernel\Event\FilterControllerEvent
kernel.view KernelEvents::VIEW Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent
kernel.response KernelEvents::RESPONSE Symfony\Component\HttpKernel\Event\FilterResponseEvent
kernel.finish_request KernelEvents::FINISH_REQUEST Symfony\Component\HttpKernel\Event\FinishRequestEvent
kernel.terminate KernelEvents::TERMINATE Symfony\Component\HttpKernel\Event\PostResponseEvent
kernel.exception KernelEvents::EXCEPTION Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent

Un esempio funzionante

Quando si usa il componente HttpKernel, si è liberi di connettere qualsiasi ascoltatore agli eventi del nucleo e di usare qualsiasi risolutore di controllori che implementi Symfony\Component\HttpKernel\Controller\ControllerResolverInterface. Tuttavia, il componente HttpKernel dispone di alcuni ascoltatori predefiniti e di un ControllerResolver predefinito, utilizzabili per creare un esempio funzionante:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;

$routes = new RouteCollection();
$routes->add('hello', new Route('/hello/{name}', array(
        '_controller' => function (Request $request) {
            return new Response(
                sprintf("Ciao %s", $request->get('name'))
            );
        }
    )
));

$request = Request::createFromGlobals();

$matcher = new UrlMatcher($routes, new RequestContext());

$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new RouterListener($matcher));

$resolver = new ControllerResolver();
$kernel = new HttpKernel($dispatcher, $resolver);

$response = $kernel->handle($request);
$response->send();

$kernel->terminate($request, $response);

Sotto-richieste

Oltre alla richiesta “principale”, inviata a HttpKernel::handle, si possono anche inviare delle cosiddette “sotto-richieste”. Una sotto-richiesta assomiglia e si comporta come ogni altra richiesta, ma tipicamente serve a rendere solo una breve porzione di una pagina, invece di una pagina completa. Solitamente si fanno sotto-richieste da un controllore (o forse da dentro a un template, che viene reso da un controllore).

../../_images/sub-request.png

Per eseguire una sotto-richiesta, usare HttpKernel::handle, ma cambiando il secondo parametro, come segue:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

// ...

// creare qualche altra richiesta a mano, come necessario
$request = new Request();
// per resempio, si potrebbe impostare a mano _controller
$request->attributes->add('_controller', '...');

$response = $kernel->handle($request, HttpKernelInterface::SUB_REQUEST);
// fare qualcosa con questa risposta

Questo crea un altro ciclo richiesta-risposta, in cui la nuova Request è trasformata in una Response. L’unica differenza interna è che alcuni ascoltatori (p.e. security) possono intervenire solo per la richiesta principale. A ogni ascoltatore viene passata una sotto-classe di Symfony\Component\HttpKernel\Event\KernelEvent, il cui metodo getRequestType() può essere usato per capire se la richiesta corrente sia una richiesta principale o una sotto-richiesta.

Per esempio, un ascoltatore che deve agire solo sulla richiesta principale può assomigliare a questo:

use Symfony\Component\HttpKernel\HttpKernelInterface;
// ...

public function onKernelRequest(GetResponseEvent $event)
{
    if (!$event->isMasterRequest()) {
        return;
    }

    // ...
}