Modern highway interchange with light trails on defined lanes as a metaphor for URL routes

URL dans Drupal 11.4: utilisez les API du core, pas des chaînes

Drupal 8 est sorti en 2015, et pourtant le code Drupal contient encore des URL assemblées en concaténant des chaînes au lieu d’utiliser les outils du core. Ces raccourcis posent toujours problème dans Drupal 11.4.

Un lien construit comme '/node/' . $id . '?source=' . $source peut sembler correct jusqu’au jour où le site utilise un alias, un préfixe de langue ou une valeur de requête contenant &. Drupal dispose déjà d’API pour ces cas. Dans cet article, je montre quand utiliser Url::fromRoute(), Url::fromUri() et Url::fromUserInput(), puis comment afficher des liens sans perdre les métadonnées de cache.

Les exemples ciblent Drupal 11.4 et ont été vérifiés avec la version 11.4.9 du core. Ce sont des fragments pour des modules personnalisés exécutés dans une application Drupal amorcée.

Dans cet article:

Quelle API d’URL utiliser?

Partez de ce que votre code sait de la destination. S’il connaît un nom de route, transmettez ce nom à Drupal au lieu de reconstruire le chemin actuel de la route. S’il dispose déjà d’une entité, utilisez le modèle de lien de cette entité.

Ce que vous avez À utiliser Exemple
Une route Drupal et ses paramètres Url::fromRoute() entity.node.canonical avec un paramètre node
Une entité chargée $entity->toUrl() La page canonique d’un nœud
Une URI externe Url::fromUri() https://example.com/docs
Une ressource locale connue sans route Url::fromUri() avec base: base:robots.txt
Un chemin relatif saisi par un rédacteur Url::fromUserInput() /contact, ?page=2 ou #details
Un texte de lien et un objet Url Link::fromTextAndUrl()->toRenderable() Un lien dans un tableau de rendu

Un objet URL représente une destination. À lui seul, il ne valide pas cette destination et n’accorde aucun accès. Cette distinction compte pour les liens vers des pages d’administration et pour les valeurs fournies par les utilisateurs.

Générer des liens à partir de routes et d’entités

Prenons ce chemin assemblé à la main:

// Avoid building a known Drupal route as a string.
$path = '/node/' . $node_id;

Il fige la structure du chemin et contourne la génération d’URL de Drupal. Remplacez-le par le nom de la route et ses paramètres:

use Drupal\Core\Url;

// Example: node 42 exists on the site.
$url = Url::fromRoute('entity.node.canonical', ['node' => 42]);

Drupal peut alors appliquer le traitement des chemins sortants du site, y compris les alias et les préfixes de langue configurés. Le chemin obtenu dépend de la configuration: ne partez pas du principe que le nœud 42 devient toujours /node/42 ni qu’un préfixe de langue est toujours présent.

Si vous avez déjà chargé un nœud, demandez à l’entité son URL canonique:

// $node is a loaded Drupal\node\NodeInterface instance.
$url = $node->toUrl('canonical');

Pour les autres types d’entités, vérifiez quels modèles de lien ils fournissent. Toutes les entités n’ont pas de page canonique, et générer une URL ne remplace pas la vérification de l’existence de l’entité ni du droit de l’utilisateur actuel à la consulter.

Dans Twig, une route connue peut être liée avec path():

<a href="{{ path('entity.node.canonical', {'node': node.id}) }}">
  {{ 'Read more'|t }}
</a>

Cela suppose que le template reçoit un nœud et que le composant qui l’entoure gère ses exigences d’accès et de mise en cache. Utilisez la fonction Twig url() lorsque vous avez besoin d’une URL de route absolue. Aucune de ces deux fonctions ne rend une destination accessible à un utilisateur qui n’en a pas la permission.

URI externes et chemins saisis par les rédacteurs

Url::fromUri() convient lorsque votre destination est une URI et non un nom de route:

use Drupal\Core\Url;

$documentation = Url::fromUri('https://example.com/docs');
$email = Url::fromUri('mailto:[email protected]');
$robots = Url::fromUri('base:robots.txt');

Le schéma base: construit une URL relative au chemin de base de l’installation Drupal. Il est prévu pour les ressources dont vous savez qu’elles ne passent pas par le système de routage de Drupal. Il ne vérifie pas que le fichier existe.

Ne passez pas /node/42 directement à fromUri() comme s’il s’agissait d’une URI absolue: un chemin relatif ordinaire n’a pas de schéma. Pour une référence relative saisie par un rédacteur, utilisez plutôt fromUserInput():

use Drupal\Core\Url;

$contact = Url::fromUserInput('/contact');
$next_page = Url::fromUserInput('?page=2');
$section = Url::fromUserInput('#details');

Cette méthode exige une valeur qui commence par /, ? ou #. Elle peut résoudre un chemin local via le routage et le traitement des chemins de Drupal lorsqu’une route correspondante existe, mais elle accepte aussi des chemins locaux qui ne correspondent à aucune route. Ce n’est pas un outil de détection de liens cassés. Traitez les saisies invalides dans la validation du formulaire plutôt que de laisser une exception atteindre le visiteur.

Pour les destinations externes fournies par les utilisateurs, appliquez d’abord les règles prévues pour le champ, puis construisez l’URL. Un champ limité aux liens web doit autoriser explicitement les schémas voulus, en général http et https. Un processus de redirection peut aussi nécessiter une liste d’hôtes autorisés. fromUri() est un constructeur, pas un validateur général pour les redirections, les requêtes distantes ou les règles métier.

Paramètres de requête, fragments et options de langue

Ajouter '?q=' . $value crée une ambiguïté dès que la valeur contient des espaces, des esperluettes ou d’autres caractères réservés. Passez des valeurs non encodées dans l’option query et laissez Drupal les encoder:

use Drupal\Core\Url;

$url = Url::fromRoute('entity.node.canonical', ['node' => 42], [
  'query' => [
    'search' => 'R&D / Europe',
    'page' => 2,
  ],
  'fragment' => 'technical-details',
  'absolute' => TRUE,
]);

La requête ci-dessus illustre uniquement la construction de l’URL: une page de nœud ne gère pas automatiquement la recherche ou la pagination parce que ces paramètres sont présents. Un fragment nécessite aussi un ID d’élément HTML correspondant sur la page de destination.

N’encodez pas les valeurs de requête à l’avance et n’incluez pas # dans l’option fragment. UrlHelper::buildQuery() de Drupal est utile lorsqu’une intégration distincte a réellement besoin d’une simple chaîne de requête. Pour une URL Drupal, préférez l’option query à l’assemblage manuel de cette chaîne.

L’option language attend un objet langue, pas un code de langue sous forme de chaîne. Dans un service où le gestionnaire de langues est injecté, cela peut ressembler à ceci:

$language = $this->languageManager->getLanguage('pl');
if ($language === NULL) {
  throw new \InvalidArgumentException('Polish is not configured.');
}
$url = $node->toUrl('canonical', ['language' => $language]);

Cet exemple suppose que $node est chargé et que le service language_manager est injecté dans $this->languageManager, typé Drupal\Core\Language\LanguageManagerInterface. Choisir la langue de l’URL ne crée pas une traduction de contenu manquante. Vérifiez aussi la négociation de langue et les alias du site avant d’attendre un résultat précis.

Pour les liens utilisés dans des e-mails ou des flux, absolute => TRUE évite une destination relative. Dans les tâches en ligne de commande, assurez-vous que l’application dispose du bon contexte d’URI du site: demander une URL absolue n’invente pas le nom d’hôte de production voulu.

Lorsqu’un lien apparaît sur une page Drupal, gardez-le dans le système de rendu au lieu de concaténer une balise <a>:

use Drupal\Core\Link;
use Drupal\Core\Url;

$url = Url::fromRoute('user.login');
$build['login'] = Link::fromTextAndUrl(t('Sign in'), $url)
  ->toRenderable();

Renvoyez $build depuis le callback de construction concerné. Dans une classe orientée objet, utilisez le service de traduction injecté ou le trait de traduction plutôt que d’ajouter d’autres appels globaux à des services. Ici, t() garde l’exemple procédural court.

Le tableau de rendu équivalent est le suivant:

$build['login'] = [
  '#type' => 'link',
  '#title' => t('Sign in'),
  '#url' => $url,
];

Un élément de rendu de type lien gère la génération du lien et les métadonnées « bubbleable » associées. En revanche, il n’effectue pas automatiquement la vérification d’accès à la route nécessaire pour un affichage conditionnel. Si le lien ne doit apparaître que lorsque l’utilisateur actuel peut accéder à la destination, ajoutez un résultat d’accès:

$build['login']['#access'] = $url->access(NULL, TRUE);

Le second argument demande un objet de résultat d’accès, avec ses informations de mise en cache, plutôt qu’un simple booléen. En transmettant cet objet via #access, vous permettez à Drupal de tenir compte du résultat lors de la mise en cache du rendu.

Cette vérification a du sens pour les URL routées. Pour une URI non routée, Url::access() renvoie un résultat autorisé: il ne vérifie pas les permissions d’un serveur distant. La route de destination doit toujours appliquer ses propres règles d’accès lorsqu’elle est demandée, que le lien soit visible ou non.

Conserver les métadonnées de cache quand une chaîne est nécessaire

Certaines intégrations exigent une URL sous forme de simple chaîne. Url::toString() en produit une, mais sa valeur de retour par défaut ne contient pas de métadonnées de cache. Si cette chaîne fait partie d’une sortie Drupal mise en cache, collectez les métadonnées et propagez-les explicitement:

use Drupal\Core\Url;

$url = Url::fromRoute('entity.node.canonical', ['node' => 42]);
$generated = $url->toString(TRUE);

$build = [
  '#type' => 'container',
  'address' => [
    '#plain_text' => $generated->getGeneratedUrl(),
  ],
];
$generated->applyTo($build);

toString(TRUE) renvoie un objet GeneratedUrl. L’exemple l’utilise pour afficher une adresse en texte brut tout en conservant les métadonnées collectées pendant la génération de l’URL. Dans un rendu plus complexe, fusionnez-les avec les informations de mise en cache existantes du composant au lieu de les remplacer.

Cela couvre les métadonnées issues de la génération d’URL, pas toutes les dépendances du composant qui l’entoure. Si la sortie dépend aussi du libellé d’une entité, d’un résultat d’accès ou d’un objet de configuration, ajoutez également ces dépendances. Pour un lien cliquable ordinaire, toRenderable() reste le choix le plus simple.

Ne confondez pas Url::toString() et Link::toString(). La première méthode génère une URL, la seconde génère le balisage du lien. La documentation de Link dans le core recommande toRenderable() pour le rendu HTML, afin que les informations de cache restent dans le pipeline de rendu de Drupal.

Que doit tester votre équipe?

La bonne API supprime l’assemblage manuel de chaînes, mais une revue doit tout de même couvrir la configuration du site. Je vérifierais ces cas avant de fusionner une modification:

  • Alias et langues: un nœud avec un alias de chemin, une deuxième langue d’URL et une traduction manquante.
  • Valeurs de requête: espaces, &, caractères non ASCII et valeurs qui révéleraient un double encodage.
  • Contexte d’installation: un site servi depuis un sous-répertoire et des liens absolus générés par une tâche en ligne de commande.
  • Accès et cache: des utilisateurs avec des permissions différentes, y compris avec un cache de rendu déjà rempli après un changement d’utilisateur.
  • Saisie utilisateur: des références locales valides, une destination manquante et des valeurs rejetées par les règles de schéma ou d’hôte du champ.
  • Balisage: des libellés de lien contenant des caractères comme < ou &, sans contourner l’échappement avec du balisage brut.

Utilisez des tests unitaires pour vos propres règles de saisie et des tests fonctionnels pour les liens générés, les alias, l’accès et le rendu. N’attendez pas un chemin fixe lorsque le comportement testé doit respecter des alias configurables.

La règle pour l’équipe est courte: des routes pour les pages Drupal connues, des modèles de lien pour les entités chargées, des URI pour les destinations sans route et des tableaux de rendu pour les liens dans les pages. La conversion en chaîne doit être une frontière délibérée, pas la manière par défaut de produire du HTML.

À lire aussi: Concaténer des chaînes et variables en PHP 8+: exemples et erreurs fréquentes.

Besoin d’aide pour votre code Drupal personnalisé?

Si vos modules contiennent des chemins codés en dur, des vérifications d’accès incohérentes ou des liens mis en cache qui se comportent différemment selon la langue, ces points méritent une revue de code ciblée. L’équipe de développement Drupal de Droptica peut vous aider à refactoriser vos modules personnalisés et à vérifier leur comportement sur votre site. Contactez-nous pour parler du code que vous souhaitez améliorer.