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:
- individua la form class
- crea l'oggetto;
- richiama getFormId()
- richiama buildForm()
- applica process callback, alter hook e altri handler
- trasforma il form array nel render array finale
- produce l'HTML
Quando l'utente invia il form:
- Drupal costruisce nuovamente il form
- associa l'input ricevuto agli elementi attesi
- esegue la validazione degli elementi
- esegue i validation handler
- se esistono errori, ricostruisce e ripresenta il form
- se non esistono errori, esegue i submit handler
- 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...