Je me suis retrouvé confronté à un petit problème lors de mes derniers développements sur mon générateur de sites. J'ai une partie qui installe un système de blog avec des billets de blogs. Ces derniers utilisent un vocabulaire permettant de spécifier des termes afin de catégoriser chaque article comme il se doit.
Ces termes sont ensuites utilisés aussi bien dans le contenu complet que sur la liste des billets de blog, sous forme de menu permettant de filtrer les articles selon la catégorie choisie.
Le problème se pose au niveau du lien qui correspond à chaque terme. Quand on crée un vocabulaire et des termes associés, ces derniers, en tant que terme de taxonomie, ont un lien de type canonical qui correspond au pattern suivant :
/taxonomy/term/{tid}Drupal propose une vue "terme de taxonomie" qui prend en charge ce chemin. Ce qui fait que pour chaque terme, on sera redirigé vers cette vue, qui affichera les contenus qui sont associés au terme correspondant.
Le souci, c'est que parfois, le développeur préfèrerai afficher une liste spécifique, par exemple une vue dédiée à cet effet. Pour rendre les choses un peu plus complexes, cette vue pourrait utiliser un mode d'affichage (view-mode) particulier qui sera dédié aux billets de blog. On se retrouve donc avec un affichage spécifique qui est totalement dédié aux billets de blog, et qui sert à filtrer les articles par catégorie.
Pour résoudre cette problématique, il existe différentes possibilités. La première vers laquelle je m'étais tourné était de réécrire les liens des termes de mes articles via une fonction de preprocess, comme suivant :
/**
* Custom preprocess method called on field_t_blog_categories field.
*
* Change the URL of taxonomy term to redirect to blog filtered by category
* page.
*
* @param array $variables
* The variables array.
*/
function hook_preprocess_field_field_t_blog_categories(&$variables) {
foreach (array_keys($variables['items']) as $index) {
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = $variables['items'][$index]['content']['#entity'];
$url = Url::fromRoute('view.blog.page_2', [
'arg_0' => strtolower(str_replace(' ', '-', $term->get('field_t_blog_category_key')->value)),
]);
$variables['items'][$index]['content']['#url'] = $url;
}
}ça fonctionne très bien. Pour tous les termes de mon champ field_t_blog_categories, je réécris l'url pour faire en sorte qu'elle corresponde à ma page qui liste les billets de blog par catégorie.
Ok... Tout va bien, me direz-vous... Mais non, tout ne va pas bien, car cette réécriture ne se fait QUE sur les termes qui sont dans mon champ field_t_blog_categories. Qu'en est-il de mon bloc de menu qui liste les termes sur la page de listing global des articles ? Hé bien ils ne sont pas réécrits, forcément. Donc que vais-je faire ? réécrire les liens de menu via un autre preprocess ? aller les modifier un par un dans l'interface de gestion des menus ? Et si j'utilise un module tel que taxonomy_menu pour générer mon menu ? Alors à chaque fois que je créerai un nouveau terme, je devrai aller le modifier dans la gestion des menus pour qu'il corresponde bien à la page vers laquelle j'aimerai qu'il redirige... À la limite, en tant que développeur, je peux l'accepter. Mais les utilisateurs finaux vont vite oublier de le faire, et se retrouver perdus avec des liens qui dirigeront vers la mauvaise page...
Bien que la méthode du preprocess soit fonctionnelle dans certains cas, elle ne l'est pas de manière générale. Que faire alors ? Modifier la vue "terme de taxonomie" ? Oui, ça pourrait être possible. Mais admettons maintenant que j'aie d'autres vocabulaires qui utilisent d'autres type de contenus, que se passera-t-il ? Ces autres types de contenu essayeront de s'afficher avec un view-mode qu'ils n'ont pas, ou alors je vais devoir créer le même view-mode pour chaque type de contenu... Stooop... ça devient complexe à gérer, et surtout à maintenir.
La solution est plus simple, et elle s'appelle OutboundPathProcessor. Drupal (via symfony), propose un mécanisme de pathProcessor. En substance, via un service spécifique, il est possible de réécrire les URL's, qu'elles soient "entrantes" (InboundPathProcessor) ou "sortantes" (OutboundPathProcessor).
Ce mécanisme permet d'écrire une méthode "process" qui reçoit le chemin d'entrée. Il est alors possible d'implémenter la logique qui va permettre de réécrire les URL's selon nos conditions particulières.
La première chose à faire est de déclarer un nouveau service dans le fichier monmodule.services.yml :
monmodule.path_processor:
class: Drupal\monmodule\PathProcessor\UrlRewritePathProcessor
arguments: ['@entity_type.manager']
tags:
- { name: path_processor_outbound, priority: 250 }Tout simple... La partie importante se passe dans les tags. On indique le tag "path_processor_outbound" qui va indiquer que le service permet de réécrire les URL's sortantes. On pourrait très bien utiliser "path_processor_inbound", ou même déclarer 2 tags, inbound et outbound, pour indiquer que le service gère aussi bien les réécritures entrantes que sortantes. Je fournis à mon service une instance de EntityTypeManager, qui va me servir à charger le terme de taxonomie lorsque le chemin en possède un.
Dans mon cas, l'implémentation du service doit être la suivante :
- Je contrôle le chemin entrant
- Si ce chemin correspond à /taxonomy/term/{tid} alors je récupère l'identifiant {tid}
- Je charge le terme correspondant
- Je contrôle que ce terme fasse partie du vocabulaire "catégories de blog"
- Si c'est le cas, je réécris le chemin pour qu'il pointe vers /blog/categorie/{tid}
Voici l'implémentation du service :
<?php
namespace Drupal\monmodule\PathProcessor;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Symfony\Component\HttpFoundation\Request;
/**
* Define an url rewrite path processor.
*/
class UrlRewritePathProcessor implements OutboundPathProcessorInterface {
/**
* Constructs a new Url rewrite path processor instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* {@inheritDoc}
*/
public function processOutbound($path, &$options = [], ?Request $request = NULL, ?BubbleableMetadata $bubbleable_metadata = NULL) {
if (preg_match('#^/taxonomy/term/(\d+)$#', $path, $matches)) {
$tid = $matches[1];
$term = $this->entityTypeManager->getStorage('taxonomy_term')->load($tid);
$vocabulary = $term->bundle();
switch ($vocabulary) {
case 'categories_blog':
$slug = $term->id();
return '/blog/categorie/' . $slug;
}
}
return $path;
}
}
Le code est plutôt simple et décrit exactement la définition d'implémentation que j'ai indiqué au dessus. Ainsi, chaque lien qui correspond à un terme de taxonomie du vocabulaire "categories_blog" va être réécrit pour pointer vers le chemin /blog/categorie/{tid}.
Exemple très simple me direz-vous. Je suis allé bien plus loin en créant un petit module de réécriture qui me permet d'indiquer des modèles à suivre selon le type de contenu, un peu comme le fait pathauto. J'indique l'expression régulière à contrôler et le chemin final à utiliser avec la possibilité d'inclure des jetons de remplacement (tokens). Au final, j'ai des url's telles que /blog/categorie/modules-drupal via mon service qui utilise lui-même le service pathauto.alias_cleaner afin de créer des url's propres. La vue finale utilise elle-même le nom du terme plutôt que son identifiant.

De cette manière, je n'ai pas besoin de gérer d'alias d'url's et de redirections, je réécris simplement les url's en début de processus afin qu'elles pointent là ou elles doivent pointer sans me poser plus de questions.