Dans ce post, nous allons voir comment sauvegarder les valeurs d'un formulaire et les relire plus tard dans un contrôleur. Pour cela, nous utiliserons Form API et le PrivateTempStore, le stockage temporaire par utilisateur de Drupal.
Le cas d'usage est un petit lecteur RSS. Un formulaire demande à l'utilisateur l'URL d'un flux RSS et le nombre d'éléments à en lire. Ensuite, sur une page séparée (un contrôleur), l'application affiche la liste des éléments avec un lien vers chacun.
La façon la plus simple serait de lire les valeurs dans buildForm(), de les traiter et d'afficher le résultat dans un champ du même formulaire. Mais ce n'est pas notre cas : nous voulons traiter les valeurs et afficher le résultat sur une autre page. Il nous faut donc d'abord stocker les valeurs du formulaire, puis les récupérer plus tard dans le contrôleur. Comment, et où ?
La version courte : stocker et relire des données avec PrivateTempStore
Drupal dispose d'un système clé/valeur pour stocker temporairement des données propres à un utilisateur, et les garder disponibles sur plusieurs requêtes même quand l'utilisateur n'est pas connecté. C'est le PrivateTempStore. Voici la recette complète :
// 1. Récupérer la factory du tempstore privé (à injecter dans votre
// formulaire, contrôleur ou service), puis obtenir une collection.
$tempstore = \Drupal::service('tempstore.private');
$store = $tempstore->get('my_module');
// Définir une paire clé/valeur.
$store->set('key_name', $value);
// 2. Ailleurs dans l'application, relire la valeur.
$tempstore = \Drupal::service('tempstore.private');
$store = $tempstore->get('my_module');
$value = $store->get('key_name');
// Supprimer l'entrée. Facultatif : elle expire d'elle-même après une semaine.
$store->delete('key_name');
Cela paraît assez simple, n'est-ce pas ? Un PrivateTempStore est un stockage clé/valeur organisé en collections nommées (par convention, on utilise le nom du module), qui garde des données disponibles pour un utilisateur sur plusieurs requêtes de page.
Maintenant que vous avez la recette, revenons au cas d'usage et voyons où stocker et relire les valeurs de notre formulaire. Une remarque sur le code : appeler \Drupal::service() de façon statique convient pour une illustration rapide, mais dans une vraie classe nous injectons plutôt le service, et c'est ce que nous ferons ci-dessous.
Les types de stockage de données en Drupal 11
Drupal propose plusieurs API de stockage, et il vaut la peine de savoir laquelle convient :
- Database API : pour dialoguer directement avec la base de données.
- State API : un stockage clé/valeur pour des données liées à un environnement (dev, préproduction, prod), comme une clé d'API externe ou la dernière exécution du cron.
- UserData API : des données liées à un environnement mais propres à un utilisateur donné, comme un drapeau ou une préférence.
- TempStore API : un stockage clé/valeur pour des données temporaires (privées ou partagées) sur plusieurs requêtes.
- Entity API : pour stocker du contenu (node, utilisateur, commentaire) ou de la configuration (vues, rôles).
- TypedData API : une API de bas niveau pour décrire des données de façon cohérente.
Nos données sont propres à l'utilisateur, nécessaires seulement un court instant, et non liées à un environnement : la TempStore API est donc le bon choix, dans sa version privée, car les valeurs et les résultats diffèrent pour chaque utilisateur. La seule différence entre tempstore privé et partagé est la propriété : une entrée privée appartient strictement à un utilisateur ; une entrée partagée peut être lue par plusieurs.
1. Stocker les valeurs du formulaire avec PrivateTempStore
Notre but est un formulaire où l'utilisateur saisit l'URL d'un flux RSS et un nombre d'éléments, dont nous stockons les valeurs pour les récupérer plus tard dans un contrôleur. Comme les données viennent d'un formulaire, nous les stockons dans submitForm(), la méthode appelée une fois le formulaire validé et soumis. Remarquez l'injection de dépendances moderne : la classe utilise le trait de cœur AutowireTrait, si bien qu'au lieu d'écrire une méthode create() à la main, nous plaçons un attribut #[Autowire] sur la propriété promue PrivateTempStoreFactory pour indiquer au conteneur quel service injecter.
<?php
declare(strict_types=1);
namespace Drupal\ex_form_values\Form;
use Drupal\Core\DependencyInjection\AutowireTrait;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* Récupère une URL RSS et un nombre d'éléments, et les stocke dans le tempstore.
*/
final class WithStoreForm extends FormBase {
use AutowireTrait;
public function __construct(
#[Autowire(service: 'tempstore.private')]
protected readonly PrivateTempStoreFactory $tempStoreFactory,
) {}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'ex_form_values_with_store_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['url'] = [
'#type' => 'url',
'#title' => $this->t('URL'),
'#description' => $this->t('Saisissez l\'URL du flux RSS.'),
'#default_value' => 'https://www.drupal.org/planet/rss.xml',
'#required' => TRUE,
];
$form['items'] = [
'#type' => 'select',
'#title' => $this->t('Nombre d\'éléments'),
'#description' => $this->t('Combien d\'éléments récupérer.'),
'#options' => ['5' => 5, '10' => 10, '15' => 15],
'#default_value' => 5,
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Envoyer'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
// 1. Rassembler les valeurs du formulaire.
$params = [
'url' => $form_state->getValue('url'),
'items' => $form_state->getValue('items'),
];
// 2. Obtenir la collection du store nommée d'après notre module.
$store = $this->tempStoreFactory->get('ex_form_values');
// 3. Sauvegarder les valeurs, puis rediriger vers le contrôleur qui les lit.
try {
$store->set('params', $params);
$form_state->setRedirect('ex_form_values.show_items');
}
catch (\Exception $e) {
$this->logger('ex_form_values')->error('Impossible de stocker les valeurs : @err', ['@err' => $e->getMessage()]);
$this->messenger()->addWarning($this->t('Impossible de continuer, veuillez réessayer.'));
}
}
}
Toute l'action se passe dans submitForm(), dans ces deux lignes :
$store = $this->tempStoreFactory->get('ex_form_values');
$store->set('params', $params);
La première ligne demande à la PrivateTempStoreFactory un store sur la collection nommée ex_form_values (le même nom que notre module, par convention). La seconde stocke notre paire clé/valeur : la clé est params et la valeur est le tableau $params contenant les valeurs du formulaire.
En coulisses, la factory utilise un stockage clé/valeur avec expiration : les entrées sont écrites dans la table key_value_expire et supprimées automatiquement à leur expiration. Par défaut, une entrée vit une semaine (604800 secondes) ; on ne peut pas le changer par appel, c'est fixé par le store. Lors du set(), Drupal s'assure que même un utilisateur anonyme a une session, afin de savoir à qui appartiennent ces données : un utilisateur authentifié est identifié par son ID utilisateur, un anonyme par son ID de session.
Ça n'était pas si compliqué, non ? Voyons maintenant comment relire les valeurs dans un contrôleur.
2. Relire les valeurs dans un contrôleur
Pour lire les données, les traiter et afficher le résultat, le formulaire redirige vers un contrôleur. Le voici :
<?php
declare(strict_types=1);
namespace Drupal\ex_form_values\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\AutowireTrait;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* Lit les valeurs stockées du formulaire et affiche les éléments RSS.
*/
final class ShowItemsController extends ControllerBase {
use AutowireTrait;
public function __construct(
#[Autowire(service: 'tempstore.private')]
protected readonly PrivateTempStoreFactory $tempStoreFactory,
) {}
public function showItems(): array {
// 1. Lire les valeurs stockées pour cet utilisateur.
$store = $this->tempStoreFactory->get('ex_form_values');
$params = $store->get('params');
if (!$params) {
return ['#markup' => $this->t('Aucune valeur stockée. Veuillez d\'abord soumettre le formulaire.')];
}
// 2. Éventuellement supprimer l'entrée maintenant qu'elle a servi.
// Facultatif : elle expirerait d'elle-même après une semaine.
// $store->delete('params');
// 3. Afficher ce que nous avons relu.
$build['message'] = [
'#markup' => $this->t('URL : @url, éléments : @items', [
'@url' => $params['url'],
'@items' => $params['items'],
]),
];
// 4. Un lien de retour vers le formulaire.
$build['back'] = [
'#type' => 'link',
'#title' => $this->t('Retour au formulaire'),
'#url' => Url::fromRoute('ex_form_values.with_store_form'),
];
// 5. Cette page est propre à l'utilisateur et éphémère : ne pas la mettre en cache.
$build['#cache']['max-age'] = 0;
return $build;
}
}
Les deux lignes qui comptent sont celles qui touchent au tempstore :
$store = $this->tempStoreFactory->get('ex_form_values');
$params = $store->get('params');
La première ligne nous est désormais familière : elle obtient un store sur la collection ex_form_values. La seconde lit la valeur stockée sous la clé params. Comme il s'agit du tempstore privé, get() ne renvoie que les données appartenant à l'utilisateur courant : Drupal vérifie le propriétaire en coulisses, de sorte qu'un utilisateur ne lit jamais les valeurs d'un autre.
Supprimer l'entrée avec $store->delete('params') est facultatif, puisque les données expirent d'elles-mêmes après une semaine. Mais si vous attendez un usage intensif du formulaire, la supprimer une fois lue est une bonne habitude, car vous n'en aurez plus besoin.
Le reste du contrôleur est routinier : à partir d'ici, vous récupéreriez le flux avec le service http_client injecté, construiriez un tableau de rendu à partir des éléments, et le rendriez. Rien de nouveau ici.
En résumé
Nous voulions un formulaire pour récupérer une URL RSS et un nombre d'éléments, et un contrôleur pour afficher le résultat. Pour transporter les valeurs d'une page à l'autre, nous avons utilisé le PrivateTempStore, car les données sont propres à l'utilisateur (anonymes compris), nécessaires brièvement, et non liées à un environnement ou à un profil utilisateur.
Pour stocker les valeurs, dans le submitForm() du formulaire :
$store = $this->tempStoreFactory->get('ex_form_values'): obtenir un store sur notre collection ;$store->set('params', $params): sauvegarder les valeurs sous la cléparams.
Et pour les relire dans le contrôleur :
$store = $this->tempStoreFactory->get('ex_form_values'): le même store ;$params = $store->get('params'): les valeurs, pour cet utilisateur uniquement.
Modernisé pour Drupal 11, le changement par rapport à l'original Drupal 8 est l'injection de dépendances par autowiring (l'attribut #[Autowire] à la place d'une méthode create() écrite à la main) et les types stricts. L'API PrivateTempStore elle-même n'a pas changé.
Et vous ? Dans quelle situation utiliseriez-vous le tempstore privé ? Partagez vos idées dans les commentaires.
Pour en savoir plus
- class PrivateTempStore (api.drupal.org)
- class PrivateTempStoreFactory (api.drupal.org)
- State API overview (drupal.org)