Hai mai pensato a cosa succede quando una richiesta HTTP arriva al tuo sito Drupal? Come Drupal trova il codice corretto da eseguire? Quali parti di una pagina provengono dalla cache e quali sono costruite da zero? Quante e quali query vengono eseguite sul database? E, perché no, quanto tempo e quanto memoria richiede convertire la richiesta in una risposta?
Che tu sia una persona che sviluppa moduli o solo curiosa, le risposte a queste domande ti aiuteranno a comprendere meglio come funziona internamente il tuo CMS preferito (perché se non lo è già, lo diventerà).
Per fare questo useremo il modulo Webprofiler, che ci aiuterà a capire come le varie componenti di Drupal interagiscono per convertire una richiesta in una risposta. Webprofiler raccoglie dati durante la costruzione di ogni pagina del sito e ci permette di esplorare facilmente cosa succede all’interno di Drupal.
Seguiremo il percorso di una richiesta iniziando dai middleware, passeremo dal routing al controller per finire a Twig. Scopriremo come i servizi forniscono funzionalità e come gli eventi danno la possibilità (o l’opportunità) di scrivere codice disaccoppiato. Il tutto senza perdere di vista le performance e tenendo un occhio sulle risorse, il tempo, le cache e le query.
3. Drupal costruisce tutte le pagine alla stessa maniera:
● Nodi
● Viste
● Pagine di amministrazione
● Form
● …
Tutte seguono lo stesso ciclo di vita: una Richiesta viene processata per generare
una Risposta.
In questa sessione analizzeremo nel dettaglio quello che succede sotto il cofano,
quali componenti e sottosistemi di Drupal sono coinvolti per trasformare una
Richiesta in una Risposta.
4. if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
include $app_root . '/' . $site_path . '/settings.local.php';
}
Drupal 10 è ottimizzato per la produzione. Per analizzare nel dettaglio quello che
succede, conviene spegnere queste ottimizzazioni e disabilitare tutte le cache.
Per fare questo basta aggiungere 3 righe in fondo al file sites/default/settings.php
5. Nel file sites/development.services.yml invece alteriamo la configurazione di Twig
per abilitare il debug e disabilitare la cache dei template.
parameters:
http.response.debug_cacheability_headers: true
twig.config:
debug: true
auto_reload: null
cache: false
services:
cache.backend.null:
class: DrupalCoreCacheNullBackendFactory
6. Per analizzare il funzionamento di Drupal abbiamo bisogno di un tool che ci faccia vedere come
i diversi sottosistemi del CMS interagiscono tra di loro.
● WebProfiler è un modulo per Drupal 8/9/10 (prima era dentro Devel, ora è un progetto a se)
● Basato sul profiler di Symfony
● Colleziona dati su ogni risposta che il sistema genera
● Instrumenta il codice del Core per estrarre informazioni utili
https://blog.sparkfabrik.com/en/webprofiler-updates
7. Una volta installato ed abilitato, il modulo WebProfiler inizia a collezionare e salvare
informazioni per ogni pagina visitata.
I profili vengono salvati all’interno del filesystem di Drupal.
Viene iniettata una toolbar in fondo ad ogni pagina HTML, con un overview dei dati
raccolti. L’insieme di tutti i dati è accessibile su una dashboard che li mostra per
esteso
10. https://www.richardbagshaw.co.uk/stack-php-middleware/
Prima di essere processata, ogni richiesta attraversa un insieme di strati, nei quali
può essere modificata, anche radicalmente.
Dopo essere stata generata, ogni risposta ri-attraversa gli stessi strati, ma alla
rovescia, dall’ultimo al primo. Ogni strato può modificare la risposta, prima di
passare a quello dopo.
12. In questo caso il controller che viene invocato non fa altro che ritornare la più
semplice delle risposte: Hello, world!
<?php
namespace DrupaldrupalconController;
use DrupalCoreControllerControllerBase;
class ExampleController extends ControllerBase {
public function view() {
return [
'#type' => 'markup',
'#markup' => 'Hello, world!',
];
}
}
13. Vediamo qualcosa di più complesso, un controller che chiama un endpoint
esterno, in HTTP, per recuperare un dato e lo ritorna in una risposta.
14. Per fare una chiamata HTTP mi serve un client HTTP. Il Core di Drupal ne
fornisce uno e lo mette a disposizione come servizio.
15. I servizi sono oggetti PHP il cui solo scopo è mettere a disposizione una
qualche funzionalità a chi li utilizza.
Il Core di Drupal ne definisce a centinaia.
Le caratteristiche principali di un servizio sono:
● Non mantengono uno stato
● Meglio se implementano un’interfaccia
● Devono fare una cosa soltanto (la S di SOLID)
16. Un controller Drupal non è un servizio, quindi usare la Dependency Injection
non è un opzione. Dobbiamo usare il metodo create() di ControllerBase.
<?php
namespace DrupaldrupalconController;
use DrupalCoreControllerControllerBase;
use GuzzleHttpClient;
use SymfonyComponentDependencyInjectionContainerInterface;
class MicroserviceController extends ControllerBase {
private Client $httpClient;
public static function create(ContainerInterface $container) {
return new static(
$container->get('http_client')
);
}
final public function __construct(Client $httpClient) {
$this->httpClient = $httpClient;
}
}
17. Il controller delega tutta la Business Logic ai servizi. Il suo unico compito è di
coordinarli per trasformare una richiesta in una risposta.
public function view() {
$response = $this
->httpClient
->get('http://ddev-drupal10-microservice:8080/hello-instrumented');
$json = json_decode($response->getBody()->getContents());
return [
'#type' => 'markup',
'#markup' => $json->message,
];
}
18. Se la rotta richiesta è quella di un nodo (/node/42 ad esempio), il flusso seguito da
Drupal è esattamente lo stesso.
Con solo qualche passaggio in più:
● La rotta è parametrica, 42 identifica l’id del nodo da caricare
● Drupal usa un servizio per fare l’upcast del parametro all’oggetto Node che lo rappresenta
● Il nodo è caricato dal database usando un servizio apposito
19. Un controller però contribuisce solo ad una parte della risposta, quello che ritorna è
stampato all’interno della regione main del template.
Tutto il resto della pagina è contribuito dal sistema dei Blocchi.
I blocchi sono plugin, ma ne parleremo un’altra volta :-)
Un po’ di informazioni qua:
https://drupalize.me/blog/201409/unravelling-drupal-8-plugin-system
20. Fin qua abbiamo visto come il controller contribuisca a costruire il render array della
regione main e quali sono i blocchi che contribuiscono al resto della pagina.
Arrivati a questo punto però, anche se si segue il codice passo passo con un
debugger è difficile capire cosa succede dopo l’invocazione del controller.
Chi carica i blocchi? Chi assembla la pagina?
21. Molti dei sistemi che compongono Drupal sono debolmente accoppiati e la
comunicazione tra loro avviene attraverso gli eventi.
In questo caso il Kernel di Drupal prende il render array ritornato dal controller e lo
“spedisce” a tutti i servizi in ascolto per l’evento kernel.view.
22. Uno o più ascoltatori (listener) si registrano per ricevere eventi di un determinato
tipo. Se qualcuno solleva un evento di quello specifico tipo, tutti gli ascoltatori
vengono invocati, in ordine di priorità, e ciascuno di essi può interagire con l’evento
(per compiere azioni o modificare l’evento stesso).
Ci deve essere quindi un ascoltatore dell’evento kernel.view capace di trasformare il
render array ritornato dal controller in quello di una pagina completa e poi di renderlo
in HTML.
23. public function onViewRenderArray(ViewEvent $event) {
$request = $event->getRequest();
$result = $event->getControllerResult();
// Render the controller result into a response if it's a render array.
if (is_array($result)) {
$wrapper = $request->query->get(static::WRAPPER_FORMAT, 'html');
$renderer = $this->classResolver->getInstanceFromDefinition($this->mainContentRenderers[$wrapper]);
$response = $renderer->renderResponse($result, $request, $this->routeMatch);
$event->setResponse($response);
}
}
core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php
24. public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
[$page, $title] = $this->prepare($main_content, $request, $route_match);
[...]
return $response;
}
protected function prepare(array $main_content, Request $request, RouteMatchInterface $route_match) {
[...]
$event = new PageDisplayVariantSelectionEvent('simple_page', $route_match);
$this->eventDispatcher->dispatch($event, RenderEvents::SELECT_PAGE_DISPLAY_VARIANT);
[...]
}
core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
public function onSelectPageDisplayVariant(PageDisplayVariantSelectionEvent $event) {
$event->setPluginId('block_page');
}
core/modules/block/src/EventSubscriber/BlockPageDisplayVariantSubscriber.php
25. Dopo aver costruito il render array con tutte le informazioni necessarie a costruire
tutta la pagina di risposta, HtmlRenderer usa il servizio renderer per convertire il
tutto in HTML.
Il processo di rendering di un render array coinvolge il tema di Drupal, che a sua volta
usa un’insieme di funzioni di preprocess e di file di Twig.
26. Durante il processo di rendering tutti gli asset CSS e Javascript vengono aggiunti al
markup finale.
27. Come ultima cosa la risposta passa attraverso tutti gli strati di middleware,
dall’ultimo al primo e viene inviata al client.
Il viaggio dentro Drupal finisce qua, la risposta HTML verrà renderizzata dal browser.
Il nostro lavoro è concluso.
Oppure no…
28. Restano (almeno) altre due cose che possiamo analizzare:
● BigPipe
● Performance del frontend