Quando un utente richiede un URL, Drupal deve determinare:

  • quale route corrisponde all'URL
  • se l'utente può accedervi
  • quale codice deve essere eseguito
  • quale risposta deve essere restituita

Una route associa un percorso URL a un componente incaricato di produrre il contenuto, normalmente un controller o un form.

Il controller è una classe PHP il cui metodo viene richiamato dalla route. Il controller può restituire un render array, che Drupal trasformerà nella pagina finale, o un oggetto Response, se deve controllare direttamente la risposta HTTP.

E' possibile creare un menu link per rendere raggiungibile la route attraverso un menu.

Una route può richiedere che l'utente abbia una determinata permission prima di consentire l'accesso.

La route

Le route sono definite nel file .routing.yml. Ogni route è identificata da un nome univoco, normalmente formato dal machine name del modulo seguito da un identificatore descrittivo. Per definire una route bisogna indicare le chiavi:

  • path che definisce il percorso della route
  • defaults che indica valori di default
  • requirements che definisce i vincoli di accesso

E' anche possibile indicare nella chiave methods i metodi HTTP accettati.

nome_modulo.forecast:
  path: '/weather/{display}' # display è un parametro dell'url
  defaults:
    _controller: '\Drupal\nome_modulo\Controller\ForecastController::build' # Fully Qualified Class Name del controller
    _title: 'Weather forecast'# titolo della pagina e tag <title>
    display: 'summary' # valore di default del parametro
  requirements:
    _permission: 'view weather forecast' # questa permission verrà definita in permissions.yml
    _access: 'TRUE' # alternativa se si vuole indicare che la route è accessibile a chiunque
    _custom_access: '\Drupal\nome_modulo\Access\ForecastAccessCheck::access' # alternativa se si vogliono controlli più specifici
    _entity_access: 'node.view' # da usare se la route opera su entità
    display: 'summary|extended' # valori accettati per il parametro display
  methods: [GET]
nome_modulo.settings: 
  path: '/admin/config/services/nome-modulo' 
  defaults: 
    _form: '\Drupal\nome_modulo\Form\SettingsForm' # non si usa _controller perchè è un form
    _title: 'Nome Modulo settings' 
  requirements: 
    _permission: 'administer weather settings'
Importante: il path deve essere scritto con apici singoli e non doppi altrimenti non verrà riconosciuto.
nome_modulo.forecast:
    path: '/weather/{display}' # apici singoli!

Validazione del parametro

Quando il valore del parametro passato nella route non soddisfa il pattern indicato nella chiave requirements Drupal non considera valida la route e restituisce una risposta HTTP 404 Not Found. Quindi nel controller non è necessario inserire questo tipo di validazione:

// codice da NON inserire nel controller
$display = in_array($display, ['summary', 'extended']) ? $display : 'summary';

Route parameter e query parameter

  • route parameter: /weather/summary
  • query parameter: /weather?display=summary

Se il controller deve leggere un query parameter:

use Symfony\Component\HttpFoundation\Request; 

public function build(Request $request): array { 
  $display = $request->query->get('display', 'summary'); 
  // ... 
}

Parameter upcasting

Quando un parametro rappresenta un'entità Drupal, il routing system può convertire automaticamente l'ID presente nell'URL nell'oggetto corrispondente:

nome_modulo.alert: 
  path: '/weather-alert/{node}' 
  defaults: 
    _controller: '\Drupal\nome_modulo\Controller\AlertController::build' 
    _title: 'Weather alert' 
  requirements: 
    _entity_access: 'node.view' # verifica che l'utente possa visualizzare un'entità di tipo node
  options: 
    parameters: 
      node: 
        type: entity:node # indica che il parametro deve essere convertito in un'entità di tipo node

Nel controller:

use Drupal\node\NodeInterface; 

public function build(NodeInterface $node): array { 
  // ... 

}

Se l'entità non esiste Drupal restituisce una risposta di errore 404 Not Found.

Il controller

Nel file src/Controller/ForecastController.php:

<?php 

declare(strict_types=1);

namespace Drupal\nome_modulo\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\nome_modulo\Service\ForecastClientInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/** 
* Returns responses for the Nome Modulo forecast routes. 
*/ 
final class ForecastController extends ControllerBase
{
    /** 
    * Constructs a ForecastController object. 
    */ 
    public function __construct(
        private readonly ForecastClientInterface $forecastClient // Dependency Injection
    ) {}

    /** 
    * {@inheritdoc} 
    */ public static function create( ContainerInterface $container): static {
        return new static($container->get(ForecastClientInterface::class));
    }

    /** 
    * Builds the weather forecast page. 
    */ 
    public function build(string $display): array {
        return [
            "#theme" => "nome_modulo_forecast",
            "#forecast" => $this->forecastClient->getForecast(),
            "#display" => $display,
            "#cache" => [
                "tags" => ["nome_modulo:forecast"], 
                "max-age" => 1800
             ],
        ];
    }
}

ControllerBase

ControllerBase è una classe di utilità destinata ai controller che contengono soltanto una quantità limitata di logica.

Il controller non dovrebbe occuparsi direttamente di:

  • eseguire chiamate HTTP
  • accedere al database
  • trasformare dati complessi
  • gestire la cache applicativa
  • contenere business logic
  • compiere operazioni che richiedono test unitari articolati

Queste responsabilità devono essere delegate ai servizi.

Restituzione di una risposta in JSON

use Symfony\Component\HttpFoundation\JsonResponse; 

public function status(): JsonResponse { 
    return new JsonResponse([ 
      'status' => 'ok', 
    ]); 
}

Redirect

use Symfony\Component\HttpFoundation\RedirectResponse; 

public function redirectToForecast(): RedirectResponse { 
  return $this->redirect('nome_modulo.forecast'); 
}

Restituzione di una risposta di errore 404

use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 

throw new NotFoundHttpException();

Permissions

Nel file .permissions.yml:

view weather forecast: # le permissions si possono scrivere con gli spazi!
  title: 'View weather forecast'
  description: 'View the weather forecast provided by Nome Modulo.'
administer weather settings: 
  title: 'Administer weather settings' 
  description: 'Change the weather provider and forecast settings.' 
  restrict access: true

Si può anche aggiungere la chiave restrict access: true che mostra nell'interfaccia un avviso che invita ad assegnare la permission soltanto a ruoli fidati.

Avendo dichiarato nel modulo che è necessaria la permission view weather forecast Drupal verifica automaticamente che l'utente ce l'abbia e in caso contrario restituisce una risposta HTTP 403 Forbidden (non sono necessari controlli sul ruolo dell'utente).

E' consigliabile indicare una permission per ogni azione che un utente può compiere, ad esempio:

view weather forecast:
  title: '...'
  description: '...'
administer weather settings:
  title: '...'
  description: '...' 
manage weather alerts:
  title: '...'
  description: '...'

Custom access check

Quando una permission non è sufficiente perché l'accesso dipende da condizioni dinamiche, ad esempio la località in cui si trova l'utente o un certo intervallo temporale, la route può utilizzare:

requirements:
  _custom_access: '\Drupal\nome_modulo\Access\ForecastAccessCheck::access'

Il metodo custom deve restituire un oggetto che implementa AccessResultInterface.

Aggiungere link in un menu

Per fornire un link alla route in un menu si usa il file .links.menu.yml:

nome_modulo.forecast:  # Plugin ID del menu link, cioè l'identificativo univoco
  title: 'Weather forecast' # il testo mostrato nel menu, è traducibile 
  description: 'View the current weather forecast.' 
  route_name: nome_modulo.forecast 
  menu_name: main 
  weight: 10
nome_modulo.external_weather: 
  title: 'External weather service' 
  url: 'https://example.com'
nome_modulo.settings: 
  title: 'Weather forecast' 
  description: 'Configure the weather forecast integration.' 
  route_name: nome_modulo.settings 
  parent: system.admin_config_services 
  weight: 10