Form API

La Form API è il sistema utilizzato da Drupal per costruire, visualizzare, validare ed elaborare i form. Un form array è un tipo particolare di render array, gli elementi del form sono quindi render element ai quali si aggiungono proprietà specifiche per l'acquisizione e l'elaborazione dell'input.

Drupal mette a disposizione diverse classi base e bisogna scegliere quella corrispondente alla responsabilità del form.

FormBase

Si utilizza quando il form raccoglie dati ed esegue un'operazione che non consiste direttamente nel modificare una configurazione o un, ad esempio:

  • form di ricerca custom
  • richiesta di generazione di un report
  • importazione di dati
  • invio di un messaggio
  • avvio di un'elaborazione batch
  • wizard multi-step

Una classe che estende FormBase deve normalmente implementare:

  • getFormId()
  • buildForm()
  • submitForm()
  • validateForm() non obbligatorio

ConfigFormBase

Si utilizza per modificare impostazioni amministrative esportabili tramite la Configuration API, ad esempio:

  • località predefinita
  • endpoint di un'integrazione
  • comportamento del modulo
  • numero di elementi da mostrare
  • modalità di visualizzazione globale

ConfirmFormBase

Si utilizza quando l'utente deve confermare esplicitamente un'operazione come:

  • eliminare dati
  • cancellare una configurazione
  • rimuovere un collegamento
  • avviare un'azione non facilmente reversibile

Fornisce una struttura standard con:

  • domanda di conferma
  • pulsante di conferma
  • link di annullamento
  • redirect finale

FormInterface

Una form class deve implementare direttamente o indirettamente l'interfaccia Drupal\Core\Form\FormInterface che fornisce quattro metodi:

public function getFormId(); 

public function buildForm( 
  array $form, 
  FormStateInterface $form_state, 
); 

public function validateForm( 
  array &$form, 
  FormStateInterface $form_state, 
); 

public function submitForm( 
  array &$form, 
  FormStateInterface $form_state, 
);

Ciclo di elaborazione di un form

Quando Drupal mostra un form per la prima volta esegue le seguenti operazioni:

  1. individua la form class
  2. crea l'oggetto;
  3. richiama getFormId()
  4. richiama buildForm()
  5. applica process callback, alter hook e altri handler
  6. trasforma il form array nel render array finale
  7. produce l'HTML

Quando l'utente invia il form:

  1. Drupal costruisce nuovamente il form
  2. associa l'input ricevuto agli elementi attesi
  3. esegue la validazione degli elementi
  4. esegue i validation handler
  5. se esistono errori, ricostruisce e ripresenta il form
  6. se non esistono errori, esegue i submit handler
  7. effettua un redirect oppure ricostruisce il form

Esempio di form di configurazione del modulo:

<?php 

declare(strict_types=1);

namespace Drupal\nome_modulo\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

/** 
* Configures Nome Modulo settings. 
*/ 
final class SettingsForm extends ConfigFormBase {

  /** 
  * Name of the editable configuration object. 
  */
    private const CONFIG_NAME = "nome_modulo.settings"; 

   /** 
   * Maximum number of forecast days supported by the provider. 
   */
    private const MAX_FORECAST_DAYS = 16;

    /** 
    * {@inheritdoc} 
    */ 
    public function getFormId(): string { // richiesto perchè estendiamo ConfigFormBase  
        return "nome_modulo_settings"; // se dovrò fare un hook_form_FORM_ID_alter() userò questo come FORM_ID
    }

    /** 
    * {@inheritdoc} 
    */ 
    protected function getEditableConfigNames(): array { 
        return [
          self::CONFIG_NAME // indica cosa il form modifica, in questo caso nome_modulo.settings
        ];
    }

    /** 
     * {@inheritdoc}
      */ 
     public function buildForm( // restituisce il form array
        array $form,
        FormStateInterface $form_state //contiene lo stato dell'elaborazione del form
    ): array {

        $form["location"] = [
            "#type" => "textfield", // input type text
            "#title" => $this->t("Location"),
            "#description" => $this->t(
                "Enter the human-readable name displayed with the forecast."
            ),
            "#required" => true,
            "#maxlength" => 128,
            "#config_target" => self::CONFIG_NAME . ":location", // corrispondenza tra elemento del form e proprietà di configurazione
        ];

        $form["coordinates"] = [
            "#type" => "details",
            "#title" => $this->t("Coordinates"),
            "#description" => $this->t(
                "Coordinates are used to request the weather forecast."
            ),
            "#open" => true,
        ];

        $form["coordinates"]["latitude"] = [
            "#type" => "number",
            "#title" => $this->t("Latitude"),
            "#description" => $this->t("Enter a value between -90 and 90."),
            "#required" => true,
            "#min" => -90,
            "#max" => 90,
            "#step" => 0.000001,
            "#config_target" => self::CONFIG_NAME . ":latitude",
        ];

        $form["coordinates"]["longitude"] = [
            "#type" => "number",
            "#title" => $this->t("Longitude"),
            "#description" => $this->t("Enter a value between -180 and 180."),
            "#required" => true,
            "#min" => -180,
            "#max" => 180,
            "#step" => 0.000001,
            "#config_target" => self::CONFIG_NAME . ":longitude",
        ];

        $form["forecast"] = [
            "#type" => "details",
            "#title" => $this->t("Forecast options"),
            "#open" => true,
        ];

        $timezones = \DateTimeZone::listIdentifiers();

        $form["forecast"]["timezone"] = [
            "#type" => "select",
            "#title" => $this->t("Timezone"),
            "#description" => $this->t(
                "Select the timezone used to group the daily forecast."
            ),
            "#options" => array_combine($timezones, $timezones),
            "#required" => true,
            "#config_target" => self::CONFIG_NAME . ":timezone",
        ];

        $form["forecast"]["forecast_days"] = [
            "#type" => "number",
            "#title" => $this->t("Forecast days"),
            "#description" => $this->t(
                "Enter the number of days requested from the weather provider."
            ),
            "#required" => true,
            "#min" => 1,
            "#max" => self::MAX_FORECAST_DAYS,
            "#step" => 1,
            "#config_target" => self::CONFIG_NAME . ":forecast_days",
        ];

        $form["forecast"]["temperature_unit"] = [
            "#type" => "radios",
            "#title" => $this->t("Temperature unit"),
            "#options" => [
                "celsius" => $this->t("Celsius"),
                "fahrenheit" => $this->t("Fahrenheit"),
            ],
            "#required" => true,
            "#config_target" => self::CONFIG_NAME . ":temperature_unit",
        ];

        return parent::buildForm($form, $form_state); // importante perchè aggiunge il pulsante di submit
    }

    /** 
    * {@inheritdoc} 
    */ 
     public function validateForm(
        array &$form,
        FormStateInterface $form_state //contiene lo stato dell'elaborazione del form
    ): void {

        $location = trim((string) $form_state->getValue("location")); // lettura del valore del campo

        $form_state->setValue("location", $location); // set del valore dopo aver applicato il trim

        if (mb_strlen($location) < 2) {
            $form_state->setErrorByName(
                "location",
                $this->t("The location must contain at least two characters.")
            );
        }

        $latitude = filter_var(
            $form_state->getValue("latitude"),
            FILTER_VALIDATE_FLOAT
        );

        if ($latitude === false || 
            $latitude < -90 || 
            $latitude > 90
        ) {
            $form_state->setErrorByName(
                "latitude",
                $this->t("The latitude must be a number between -90 and 90.")
            );
        }

        $longitude = filter_var(
            $form_state->getValue("longitude"),
            FILTER_VALIDATE_FLOAT
        );

        if ($longitude === false || 
            $longitude < -180 || 
            $longitude > 180
         ) {
            $form_state->setErrorByName(
                "longitude",
                $this->t("The longitude must be a number between -180 and 180.")
            );
        }

        $timezone = (string) $form_state->getValue("timezone"); // lettura del valore del campo

        if (!in_array($timezone, DateTimeZone::listIdentifiers(), true)) {
            $form_state->setErrorByName(
                "timezone",
                $this->t("Select a valid timezone.")
            );
        }

        $forecastDays = filter_var(
            $form_state->getValue("forecast_days"),
            FILTER_VALIDATE_INT,
            [
                "options" => [
                    "min_range" => 1,
                    "max_range" => self::MAX_FORECAST_DAYS,
                ],
            ]
        );

        if ($forecastDays === false) {
            $form_state->setErrorByName(
                "forecast_days",
                $this->t(
                    "The number of forecast days must be between 1 and @maximum.",
                    ["@maximum" => self::MAX_FORECAST_DAYS]
                )
            );
        }

        $temperatureUnit = (string) $form_state->getValue("temperature_unit"); // lettura del valore del campo

        if (!in_array($temperatureUnit, ["celsius", "fahrenheit"], true)) {
            $form_state->setErrorByName(
                "temperature_unit",
                $this->t("Select a valid temperature unit.")
            );
        }

        parent::validateForm($form, $form_state); // ConfigFormBase esegue la validazione base del form
    }

    /** 
    * {@inheritdoc} 
     */ 
      public function submitForm( // viene eseguito solo se la validazione non ha prodotto errori
        array &$form,
        FormStateInterface $form_state //contiene lo stato dell'elaborazione del form
    ): void {
        parent::submitForm($form, $form_state);
        $form_state->setRedirect("nome_modulo.settings"); // impedisce che il refresh del browser invii nuovamente la stessa operazione
    }
}

Il file .settings.yml

Nel file config/install/nome_modulo.settings.yml inseriamo i valori di default disponibili alla prima installazione:

location: 'Turin' 
latitude: 45.0693
longitude: 7.6934
timezone: 'Europe/Rome' 
forecast_days: 5 
temperature_unit: 'celsius'

Form AJAX

La proprietà #ajax permette di inviare una parte del form e aggiornarne un'altra senza ricaricare l'intera pagina, ad esempio:

$form['location']['#ajax'] = [ 
    'callback' => '::updateCoordinates', 
    'wrapper' => 'coordinates-wrapper', 
    'event' => 'change', 
];

Entity form

L'Entity form è quello che si compila quando si crea un nodo di uno specifico content type, oppure uno user, un taxonomy term ecc...