Les shortcodes : de WordPress à une architecture moderne avec PHP 8.5
Publié le
Temps de lecture 6 min
J'ai consacré une journée à l'intégration propre d'une galerie d'images dans un article Markdown. Non pas en raison d'une complexité technique — même si j'ai un certain goût pour la souffrance — mais par volonté d'éviter le syndrome du BBCode artisanal : une solution fonctionnelle l'espace d'un instant, mais qui, faute de documentation, devient un cauchemar de maintenance six mois plus tard.
La sortie de Symfony 8 et la migration vers PHP 8.5 m'ont conduit à un détour analytique par WordPress. Souhaitant enrichir mes articles sans changer ma manière de rédiger en Markdown, je cherchais à comprendre pourquoi son API de shortcodes — ces balises du type [gallery id="123"] — a perduré dix-sept ans sans évolution majeure. La réponse tient en un mot : simplicité.
Cependant, cette simplicité soulève une problématique architecturale : est-il possible de reconstruire cette API avec les standards rigoureux de PHP 8.5 et Symfony 8, en conservant cette ergonomie légendaire, mais en y insufflant la fiabilité, la sécurité et les performances qui font défaut à l'originale ?
La réponse est sans équivoque : oui.
L’héritage de 2008
En mars 2008, l'équipe WordPress faisait face à un dilemme d'accessibilité. La plateforme permettait la rédaction en HTML, mais exiger d'un utilisateur standard qu'il manipule un balisage complexe pour insérer une galerie d'images menait invariablement à des mises en page brisées. La solution introduite avec WordPress 2.5 s'inspirait des BBCodes des forums de l'époque :
[gallery id="123"]
[caption]Mon texte avec légende[/caption]
[video src="video.mp4"]
L'implémentation côté développeur brillait par son minimalisme :
add_shortcode('gallery', 'my_gallery_shortcode');
function my_gallery_shortcode($atts) {
$id = $atts['id'] ?? null;
return '<div class="gallery">...</div>';
}
Cinq lignes. Zéro configuration. Une disponibilité immédiate. Ce standard de facto propulse aujourd'hui des milliers d'extensions, de WooCommerce à Elementor. Pourtant, ce succès masque des dettes techniques devenues critiques.
Les failles structurelles du modèle historique
Lors d'un audit de sécurité sur une instance WordPress utilisant une cinquantaine d'extensions, les limites de cette architecture sont devenues flagrantes.
La pollution de l'espace de noms global
On peut découvrir trois shortcodes distincts répondant au nom [button]. Lequel était actif ? Uniquement le dernier enregistré. Les deux précédents étaient silencieusement écrasés, sans avertissement ni journalisation.
Dans une architecture moderne, s'appuyer sur des fonctions globales sans isolation est une hérésie. La simple convention de nommage ne suffit pas à garantir la stabilité d'un système complexe.
L'absence de typage et d'intégrité des données
L'API historique souffre d'un manque criant de définition de types :
function my_shortcode($atts) {
$id = $atts['id']; // Est-ce un int ? une string ? null ?
}
La surface d'attaque XSS (Cross-Site Scripting)
C'est le point le plus critique. L'API ne force aucune sécurisation par défaut :
function unsafe_shortcode($atts) {
return "<div>" . $atts['content'] . "</div>"; // XSS potentiel immédiat
}
L'absence d'échappement automatique ou de politique de sécurité native a conduit à des dizaines de CVE majeurs sur des extensions populaires. La sécurité ne devrait jamais être optionnelle.
Un couplage fort entravant les tests
L'utilisation omniprésente de variables globales (global $wpdb;) et l'absence d'injection de dépendances rendent les tests unitaires impossibles. Tester un shortcode oblige à charger l'intégralité du noyau WordPress, transformant tout test unitaire en un lourd test d'intégration.
Un coût de performance au rendu
WordPress exécute une série d'expressions régulières (Regex) sur l'ensemble du contenu, à chaque requête, via do_shortcode(). Même si votre page ne contient aucun shortcode, cette passe de parsing consomme des ressources.
L’architecture moderne sous PHP 8.5
La RFC des Attributs (introduite en PHP 8.0 et maturée depuis) a été l'élément déclencheur. Les attributs permettent d'associer des métadonnées aux classes de manière déclarative, sans polluer le code métier.
L’objectif attendu : reconstruire le système en utilisant l'injection de dépendances de Symfony, le typage strict et les classes immuables (readonly) de PHP 8.5.
Les attributs PHP
Je remplace l'appel manuel add_shortcode par un attribut natif. C'est déclaratif, propre et auto-découvert par le conteneur de services.
// src/Service/Content/MediaShortcodeRenderer.php
namespace App\Service\Content;
use App\Attribute\Shortcode;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[Shortcode(
name: 'media',
description: 'Intègre une image depuis la médiathèque',
usage: '<!-- Media shortcode error: Variation "..." does not exist. -->'
)]
#[AutoconfigureTag('app.shortcode.renderer')]
final readonly class MediaShortcodeRenderer implements ShortcodeRendererInterface
{
public function render(array $attributes): string
{
// Implémentation...
}
}
Grâce à #[AutoconfigureTag], Symfony indexe automatiquement cette classe au démarrage. Si deux classes réclament le même nom, le compilateur du conteneur lèvera une exception explicite avant même le déploiement. Fini les collisions silencieuses.
Injection de dépendances et immutabilité
// src/Service/Content/MediaShortcodeRenderer.php
final readonly class MediaShortcodeRenderer implements ShortcodeRendererInterface
{
public function __construct(
private Environment $twigEnvironment,
private Resolver $resolver,
private Converter $converter,
) {}
public function render(array $attributes): string
{
// Le typage strict et l'injection permettent une logique métier robuste
$path = $attributes['path'] ?? null;
// ... Logique de résolution et rendu via Twig ...
return $this->twigEnvironment->render('shortcodes/media.html.twig', [
// ...
]);
}
}
Chaque renderer déclare ses dépendances. Le code est découplé, testable unitairement (en injectant des Mocks) et sans état (stateless), ce qui le rend compatible avec des runtimes comme FrankenPHP.
Typage strict et robustesse
L'utilisation de final readonly class garantit l'immutabilité. Un shortcode ne doit pas changer d'état après son instanciation. Le typage strict de PHP 8.5 (array<string, string>, callable) sécurise les échanges de données à la compilation, éliminant une classe entière de bugs en production.
Le mot de la fin
En partant d'un concept vieux de 17 ans, nous avons démontré qu'il est possible de concilier simplicité éditoriale et rigueur technique. Les enseignements clés de cette refonte :
- L'expérience utilisateur ne dicte pas la dette technique : On peut offrir une API simple adossée à une architecture complexe et robuste.
- PHP 8.5 change la donne : Les classes readonly et les attributs permettent d'écrire un code expressif, sûr et performant, reléguant les anciennes pratiques (tableaux associatifs non typés) au rang de souvenirs.
- L'architecture moderne est "Secure by Design" : En remplaçant les conventions implicites par des contrats explicites (interfaces, types), nous éliminons mécaniquement des catégories entières de vulnérabilités.
Cette approche est transposable au-delà des blogs : CMS Headless, génération de documentation technique ou systèmes d'e-learning. Le triptyque Parser → Registre → Renderer reste une fondation solide pour tout contenu dynamique.