La plupart des projets de développement d’extensions WordPress suivent la même trajectoire. Quelqu’un a besoin d’un formulaire de réservation, d’un importateur de flux ou d’un champ supplémentaire au moment du paiement, un développeur l’écrit, cela fonctionne, chacun passe à autre chose. Deux ans plus tard, le site reste bloqué sur une vieille version de WordPress parce que personne n’est certain que cette extension survivra à une mise à jour, et son auteur est parti depuis longtemps.

La cause tient rarement à un cœur qui évoluerait trop vite. WordPress est prudent quand il s’agit de casser l’existant, et de nombreuses extensions bien écrites il y a cinq ans tournent encore sans modification sur WordPress 7.1. Les extensions cassent à cause de décisions prises la première semaine : des fonctionnalités placées dans le thème, des fichiers du cœur modifiés au lieu d’être accrochés, des données rangées dans la structure la plus proche, et personne pour tester face à une version candidate.

Qu’est-ce qui permet à une extension WordPress sur mesure de survivre aux mises à jour du cœur ? Quatre choses. Le code vit dans une extension plutôt que dans le thème. Il étend WordPress par des actions et des filtres au lieu de modifier les fichiers du cœur. Il range chaque donnée dans la structure qui correspond à sa forme. Et quelqu’un le teste face à chaque version candidate avant que celle-ci ne soit publiée.


Pourquoi le développement d’extensions WordPress appartient à une extension et non au thème

Le domicile par défaut du code sur mesure est le fichier functions.php du thème, parce qu’il est déjà là et qu’il s’exécute déjà. C’est aussi le fichier qui disparaît à la refonte suivante.

Un thème, c’est de la présentation. Changez de thème et tout ce que faisait l’ancien s’arrête. Les types de contenu personnalisés ne sont plus déclarés, donc le contenu reste dans la base sans écran d’administration ni permalien. Les codes courts s’affichent en texte brut au milieu de la page. L’extrait d’analytique, le balisage schema et l’appel nocturne au CRM disparaissent, et rien ne remonte d’erreur.

La règle est assez simple pour tenir dans un cahier des charges. Tout ce qui doit rester vrai après une refonte appartient à une extension : types de contenu et taxonomies personnalisés, intégrations avec quoi que ce soit d’externe, codes courts et blocs, règles métier, tâches planifiées, et tout ce qui écrit dans la base de données. Le thème garde les gabarits, les styles et les fragments de gabarit.

L’addition arrive tard. À la refonte suivante, soit vous payez de nouveau pour reconstruire ce qui existait déjà, soit vous recopiez l’ancien functions.php. Sur un site aux extraits accumulés pendant des années, cela représente plusieurs milliers de livres de travail évitable, et c’est pourquoi un devis de refonte revient au double de ce que le client attendait. Un thème enfant reste un thème.

Le modèle d’extension, et la seule règle qui compte

WordPress est bâti pour être modifié de l’extérieur. Le mécanisme, ce sont les hooks, et la documentation des hooks les décrit comme les emplacements prédéfinis où un morceau de code peut interagir avec un autre ou le modifier. Une action se déclenche à un moment défini et vous laisse faire quelque chose : envoyer une notification après la publication d’un article, ou déclarer un type de contenu. Un filtre vous remet une valeur, attend que vous la changiez ou la laissiez telle quelle, et attend que vous la lui rendiez.

La règle qui en découle est absolue. Si vous modifiez un fichier dans wp-admin, dans wp-includes ou dans le dossier d’une autre extension, vous avez déjà perdu. Ces modifications sont effacées par la mise à jour suivante, sans avertissement, sans erreur et le plus souvent sans être découvertes avant qu’un client ne signale que quelque chose ne marche plus. Posez la question directement à un développeur avant de le recruter.

Quand le hook dont vous avez besoin n’existe pas, enveloppez le comportement au lieu de le remplacer, remontez vers un hook plus large, forkez l’extension tierce sous gestion de versions en documentant l’écart, ou demandez le hook en amont, ce qui est la façon dont la plupart ont été ajoutés.

Nommage, préfixes et un espace de noms très encombré

Dans WordPress, PHP s’exécute dans un unique espace de noms global partagé avec le cœur, le thème actif et toutes les autres extensions actives. Deux extensions qui déclarent chacune une fonction get_settings() ne se disputent pas poliment : la seconde provoque une erreur fatale et le site devient blanc.

Les préfixes sont plus longs que vous ne le pensez

La page des bonnes pratiques du manuel demande un préfixe unique sur tout ce qui est accessible globalement, quatre caractères au minimum et cinq de préférence, en évitant les mots anglais courants et en n’utilisant jamais wp_, _ ni WordPress lui-même. Avec des dizaines de milliers d’extensions en circulation, trois lettres tirées des initiales du client relèvent du pile ou face.

Espaces de noms et chargement automatique

La pratique moderne résout la moitié du problème. Déclarez un espace de noms PHP, mettez une classe par fichier et laissez un autochargeur PSR-4 les trouver : il n’y a plus d’instructions require manuelles ni la moindre chance qu’un nom de classe entre en collision avec celui d’une autre extension. Cela rend aussi le code testable, car les classes dont les dépendances passent par le constructeur peuvent être instanciées sans que WordPress soit chargé.

Le chargement automatique ne règle pas le cas de deux extensions livrant des versions différentes de la même bibliothèque. Celle qui charge la première gagne. Préfixez les espaces de noms des dépendances à la compilation pour tout ce qui est distribué.

Les chaînes que les espaces de noms ne peuvent pas sauver

Un espace de noms couvre les symboles PHP. Or une grande partie de ce qu’une extension déclare n’est pas un symbole PHP mais une chaîne dans un registre partagé, et cela réclame encore l’ancienne convention de préfixe : noms de hooks, clés d’options et de transients, clés de métadonnées d’articles, noms de types de contenu et de taxonomies, balises de codes courts, noms d’événements cron, espaces de noms REST et noms de tables personnalisées. Ils vivent dans un espace plat où la dernière déclaration l’emporte, ou bien deux extensions partagent un état sans le savoir.

Deux limites méritent d’être connues avant de nommer quoi que ce soit. Une clé de type de contenu ne doit pas dépasser 20 caractères et une clé de taxonomie 32, toutes deux en minuscules alphanumériques avec tirets et tirets bas. Un préfixe de cinq caractères laisse 15 caractères pour un nom de type de contenu, ce qui est moins de place qu’il n’y paraît.

Choisir où vivent les données

C’est la décision dont la traîne est la plus longue. Si elle est mauvaise, l’extension fonctionne très bien au lancement, ralentit chaque mois à mesure que les données grossissent, et quand quelqu’un le remarque enfin, le correctif est une migration et non une retouche.

Options et transients

Les options servent aux réglages globaux du site : une poignée de clés, de petites valeurs, lues sur la plupart des requêtes. Le piège est le chargement automatique, car toute option autochargée est récupérée à chaque requête, y compris les appels admin-ajax et REST, que quoi que ce soit s’en serve ou non.

WordPress 6.6 a changé la mécanique, comme l’expose le billet du blog Make WordPress Core sur la désactivation de l’autoload pour les grosses options. La valeur stockée vaut désormais on, off ou auto, et une option supérieure à 150 000 octets n’est plus autochargée par défaut, le seuil restant ajustable via le filtre wp_max_autoloaded_option_size. Traitez cela comme un plafond, pas comme un objectif. Les transients sont des options avec une expiration, et le bon domicile pour tout ce qui provient d’ailleurs.

Les métadonnées d’article ne sont pas un magasin clé-valeur

Les métadonnées d’article servent aux attributs d’un article : un sous-titre, un prix, une référence fournisseur. Ce n’est pas un magasin clé-valeur généraliste, et la raison se lit dans la définition de la table. La table wp_postmeta a quatre colonnes et trois clés. Seuls post_id et les 191 premiers caractères de meta_key sont indexés. La colonne meta_value est un longtext sans le moindre index.

Une requête qui filtre sur une valeur de métadonnée ne peut donc pas utiliser d’index. Chaque clause d’une requête de métadonnées ajoute une jointure, et sur un site de 50 000 articles portant chacun 20 lignes de métadonnées, la table contient un million de lignes. Trois clauses, ce sont trois jointures sur un million de lignes à chaque chargement de page. C’est l’une des causes les plus fréquentes d’un site rapide la première année et inutilisable la troisième, et cela revient sans cesse dans le travail sur la performance WooCommerce.

Types de contenu et taxonomies personnalisés

Un type de contenu personnalisé s’impose quand la chose est du contenu. Elle a besoin de son propre écran de liste, de permaliens, de révisions et d’un flux éditorial, et elle a du sens comme page que quelqu’un pourrait visiter. Une taxonomie personnalisée s’impose quand il vous faut un vocabulaire commun qui regroupe ces choses et mérite ses propres pages d’archive.

Les deux apportent gratuitement toute une machinerie : écrans d’administration, permissions, recherche, éditeur de blocs et API REST. Mettez show_in_rest à true, sinon l’éditeur de blocs ne saura pas gérer le type, et déclarez les deux sur le hook init, jamais avant.

Quand vous avez vraiment besoin de votre propre table

Votre propre table s’impose quand les données ne sont pas du contenu : des enregistrements en gros volume et en ajout seul, comme des journaux d’événements, des files d’import, un historique de prix ou des pistes d’audit, ou tout ce que vous filtrerez et trierez sur un champ qui n’est pas une colonne d’article. Au-delà de quelques centaines de milliers de lignes interrogées par leurs propres champs, une table dotée des bons index surpasse les métadonnées d’article de plusieurs ordres de grandeur et reste prévisible en grandissant.

Le prix, c’est que tout vous appartient : création de la table et migrations versionnées, nettoyage dans uninstall.php, vos propres écrans d’administration, points d’entrée REST et mise en cache. Voilà pourquoi, pour la plupart des extensions, la réponse honnête reste un type de contenu personnalisé.

La sécurité tient en quatre habitudes, et trois sont sautées

Le manuel de sécurité de WordPress énonce le principe sans détour : ne faites confiance ni aux saisies des utilisateurs, ni aux API tierces, ni aux données déjà présentes dans votre base. Quatre habitudes portent presque tout le risque, et dans les extensions que nous auditons elles sont sautées dans un ordre constant. Les contrôles de capacité d’abord, les nonces ensuite, l’échappement en sortie en troisième. Les requêtes préparées viennent en dernier, parce qu’une requête non préparée se voit en relecture.

Contrôles de capacité

Tout gestionnaire qui modifie quelque chose doit demander si cet utilisateur en a le droit, ce qui signifie current_user_can() avec la capacité précise, vérifiée à l’intérieur du gestionnaire et non seulement autour du bouton qui l’appelle.

is_admin() n’est pas un contrôle de permission. Cette fonction indique de quel côté du site arrive la requête, et renvoie true pour n’importe quel abonné connecté qui atteint un point d’entrée admin-ajax. Une action admin_post_ ou wp_ajax_ sans contrôle de capacité est accessible à chaque utilisateur inscrit, ce qui, sur une boutique, signifie chaque client ayant déjà commandé. Notre liste de durcissement de la sécurité WordPress couvre les contrôles au niveau du site autour de ce sujet.

Les nonces

Un nonce protège un formulaire ou une URL contre une requête que l’utilisateur n’avait pas l’intention de faire. Utilisez wp_nonce_field() dans le formulaire et check_admin_referer() dans le gestionnaire, ou check_ajax_referer() en AJAX. Malgré leur nom, ils ne servent pas qu’une fois : ce sont des empreintes valables sur une fenêtre, un jour par défaut, sous un schéma à deux tics qui place la durée de vie réelle entre douze et vingt-quatre heures.

La documentation des nonces est explicite : on ne doit jamais s’appuyer sur eux pour l’authentification, l’autorisation ou le contrôle d’accès. Un nonce établit que la requête vient bien de votre formulaire. Il ne dit rien sur le droit de cette personne à faire la chose.

Assainir à l’entrée, échapper à la sortie

Validez là où vous le pouvez, car la validation est précise : un code postal correspond au motif ou n’y correspond pas. Assainissez là où vous ne pouvez pas valider, avec sanitize_text_field(), sanitize_email(), sanitize_key(), absint() ou wp_kses_post() selon le champ.

Puis échappez au point de sortie, à chaque fois, avec esc_html(), esc_attr(), esc_url() ou wp_kses_post(). La documentation de l’échappement demande de le faire le plus tard possible, pour qu’un relecteur voie l’échappement et la sortie sur la même ligne. L’échappement est sauté plus souvent que tout le reste, parce que rien n’a l’air anormal quand on le saute. La page s’affiche parfaitement jusqu’au jour où quelqu’un met une balise de script dans un champ.

Les requêtes préparées

Toute requête que vous écrivez vous-même passe par $wpdb->prepare(), qui prend %d pour les entiers, %f pour les flottants, %s pour les chaînes et %i pour les identifiants comme les noms de tables et de colonnes. Les marqueurs restent sans guillemets, un signe pourcent littéral s’écrit deux fois, et un joker LIKE se passe dans l’argument de substitution plutôt que de se taper dans la requête. Concaténer une variable dans du SQL n’est pas un désaccord de style, c’est la faille.

L’API REST et l’éditeur de blocs

Une extension écrite cette année devrait exposer ses données par l’API REST et ses réglages par l’éditeur, plutôt que par une page d’options bricolée à la main.

Les routes se déclarent avec register_rest_route() sur le hook rest_api_init. Depuis WordPress 5.5, un argument permission_callback est obligatoire, et l’omettre déclenche un avis _doing_it_wrong() qui nomme la route. Un point d’entrée réellement public utilise __return_true, et c’est tout l’intérêt de cette conception : rendre une route publique devient une ligne de code délibérée au lieu d’un oubli. La documentation des points d’entrée personnalisés couvre aussi le schéma des arguments, où logent les fonctions de rappel d’assainissement et de validation, pour qu’une mauvaise saisie n’atteigne jamais votre gestionnaire.

Les réglages se déclarent avec register_setting() et show_in_rest à true. Cela les place sur le point d’entrée des réglages du cœur, donc l’éditeur de blocs ou un script externe peut les lire et les écrire par une interface qui gère déjà l’authentification, les permissions et la validation. Cela supprime une page d’options, son nonce, son gestionnaire de formulaire et les bogues qui y logeaient.

Les blocs se déclarent depuis un fichier block.json, la méthode canonique recommandée depuis WordPress 5.8. La documentation des métadonnées de bloc en donne le bénéfice : les ressources qui y sont déclarées ne se chargent que sur les pages où le bloc apparaît, au lieu de tout le site parce qu’une extension est active.

Discipline de performance à l’intérieur d’une extension

Quatre choses expliquent l’essentiel de la lenteur causée par les extensions que nous trouvons en audit, et toutes les quatre sont peu coûteuses à éviter et chères à corriger après coup. La première, ce sont les options autochargées, parce qu’elles coûtent quelque chose à chaque requête et pour toujours.

La deuxième, ce sont les requêtes distantes non mises en cache pendant le chargement d’une page. Un wp_remote_get() vers l’API d’un fournisseur sans cache signifie que chaque visiteur attend ce fournisseur. Quand il est lent, votre site est lent, et quand il est en panne, votre site reste suspendu jusqu’à l’expiration du délai. Mettez la réponse en cache dans un transient, fixez un délai explicite, et décidez à l’avance de ce que la page affiche quand l’appel échoue.

La troisième, ce sont les requêtes à l’intérieur d’une boucle. Appeler get_post_meta() pour chacune de 200 lignes fait 200 allers-retours si le cache de métadonnées n’a pas été préchargé, et WP_Query le précharge pour vous si vous le laissez faire. Le correctif consiste le plus souvent à cesser de désactiver quelque chose, ce qui vaut pour la plupart des constats d’un audit Core Web Vitals.

La quatrième, c’est le travail effectué à l’intérieur de la requête de quelqu’un. WP-Cron n’est pas le cron système : il se déclenche au chargement d’une page, donc une tâche planifiée s’exécute dans la requête d’un visiteur, et sur un site calme la tâche de deux heures du matin n’a pas lieu avant qu’un visiteur ne passe à cinq heures. Définissez DISABLE_WP_CRON, pilotez wp-cron.php depuis un vrai planificateur système, et gardez des tâches courtes et idempotentes.

Survivre aux mises à jour du cœur : la partie que personne ne budgète

Le cœur supprime rarement quoi que ce soit d’un coup. Les fonctions passent en obsolescence, continuent de marcher et émettent un avis, et c’est pourquoi faire tourner une préproduction avec WP_DEBUG activé est le système d’alerte précoce le moins cher qui soit. Un avis d’obsolescence est une invitation datée à corriger quelque chose tant que c’est encore bon marché.

Le processus qui évite les surprises coûte environ une heure par trimestre. Suivez le blog de développement du cœur pour savoir quand une bêta puis une version candidate existent. Lisez le Field Guide, publié pendant la phase de version candidate, qui liste les nouveautés destinées aux développeurs et les ruptures de cette version. Installez ensuite la version candidate sur une copie de préproduction et déroulez un test de fumée écrit portant sur les vraies fonctions de l’extension.

Le support des versions compte autant que le code. WordPress exige PHP 7.4 comme plancher absolu et recommande 8.3 ou plus récent, avec MariaDB 10.11 ou MySQL 8.0. Déclarez honnêtement Requires PHP et Requires at least dans l’en-tête de l’extension, puis testez sur la version la plus basse que vous avez déclarée plutôt que sur celle du portable du développeur.

Versionnez votre propre extension de façon sémantique, et tenez parole. Un correctif répare quelque chose, une version mineure ajoute du comportement sans rien casser, et une version majeure peut casser à condition de dire ce qu’elle a cassé. Les clients en mise à jour automatique comptent sur cette promesse.

Distribution, licence et comment les mises à jour atteignent le site

WordPress est publié sous licence GPL version 2 ou ultérieure, et la page de licence de wordpress.org expose la position du projet selon laquelle extensions et thèmes sont des œuvres dérivées qui héritent de la licence, tout en reconnaissant une zone grise juridique sur ce qui compte comme dérivé.

Vous obtenez les sources, toujours, et vous pouvez engager n’importe qui d’autre pour les modifier. Ce que la GPL ne fait pas, c’est vous obliger à les publier : une extension construite pour une entreprise peut donc rester privée. Elle n’empêche pas non plus le développeur de revendre le même travail à quelqu’un d’autre. Si l’exclusivité compte, c’est une clause de contrat et non une clause de licence.

Si l’extension part dans le répertoire public, elle doit satisfaire les règles du répertoire des extensions, au nombre de dix-huit. La première exige une licence compatible GPL pour tout ce que contient le paquet, images comprises. D’autres écartent le trialware, c’est-à-dire des fonctions verrouillées derrière un paiement ou une montée de version, bannissent le code obscurci, interdisent le pistage des utilisateurs sans consentement, et proscrivent les liens ou crédits ajoutés au site public sans autorisation.

Si elle reste privée, les mises à jour deviennent votre problème. Renseignez l’en-tête Update URI, qui existe pour empêcher qu’une extension privée soit écrasée par une extension au nom voisin venue du répertoire, et servez les mises à jour depuis votre propre point d’entrée. Repousser cela à la fin, c’est ainsi qu’un client finit par mettre à jour en FTP.

Ce que coûte une extension WordPress sur mesure

Les fourchettes ci-dessous sont des prix d’agence britannique en livres, pour un travail livré au standard décrit ici : avec tests, documentation et une personne nommément responsable après la mise en ligne. Un développeur WordPress et PHP compétent facture à peu près 400 à 600 £ par jour, ce sont donc des affirmations sur le périmètre et non sur le tarif.

Une petite extension utilitaire coûte 1 500 à 3 000 £. Une seule tâche, quelques hooks, peut-être un réglage : gestion de redirections, un champ supplémentaire sur une commande, un export nocturne vers un fournisseur.

Une intégration de taille moyenne coûte 3 000 à 15 000 £. Une API tierce avec authentification, reprises et gestion d’erreurs, un type de contenu personnalisé, des écrans d’administration et du traitement en arrière-plan. C’est la taille la plus souvent commandée et la plus souvent sous-estimée, car l’intégration prend une semaine et la gestion des échecs en prend deux.

Une extension produit de grande ampleur coûte 20 000 à 75 000 £ et au-delà. Ses propres tables, des interfaces dans l’éditeur de blocs, une infrastructure de licence et de mise à jour, la prise en charge du multisite, et une charge de support qui commence le jour de la livraison.

Payer dans le bas de la fourchette n’est pas automatiquement une erreur. Cela le devient quand le prix vient d’un périmètre qui a discrètement exclu les livrables ci-dessous. Notre guide sur les tarifs des développeurs WordPress et les questions à poser explique comment lire un devis, et notre page développement WordPress expose comment nous cadrons ce travail.

Ce qui devrait figurer dans le livrable

Demandez tout cela par écrit avant le début des travaux, car chaque point est peu coûteux à inclure et cher à ajouter ensuite. Les sources, dans un dépôt qui vous appartient, avec leur historique intact, plutôt qu’une archive envoyée par courriel le dernier jour. Des tests unitaires pour les règles métier et un test d’intégration pour tout ce qui écrit dans la base ou appelle un service externe, car c’est ce qui rend l’ensemble modifiable sans danger dans deux ans par quelqu’un qui n’était pas là.

Un fichier readme qui dit ce que fait l’extension, à quoi elle s’accroche, ce qu’elle stocke et où, quels services externes elle appelle et ce qui se passe quand chacun tombe. Deux pages suffisent, et son absence explique pourquoi les extensions finissent remplacées plutôt qu’entretenues. Un uninstall.php qui retire options, tables, événements cron et métadonnées. Et un contrat de support nommé couvrant les tests face à chaque version du cœur et la correction de ce qu’ils révèlent.

Le faire construire

Mecanik construit des extensions dans les trois tailles ci-dessus, et reprend des extensions écrites par quelqu’un d’autre, souvent la mission la plus utile. Nos pages développeur WordPress et développement logiciel expliquent comment nous cadrons et livrons. Si vous avez déjà une extension à laquelle personne ne veut toucher, un audit au regard des pratiques ci-dessus prend environ une journée et vous dit si elle est réparable ou remplaçable.



Questions fréquentes

Les fonctionnalités sur mesure doivent-elles aller dans une extension ou dans le thème ? Dans une extension, sauf si elles sont purement présentationnelles. Un thème est remplacé à la refonte suivante et tout ce qu’il faisait s’arrête : les types de contenu personnalisés perdent leurs écrans d’administration, les codes courts s’affichent en texte brut et les intégrations cessent silencieusement de tourner. Tout ce qui doit rester vrai après une refonte appartient à une extension.

Combien coûte une extension WordPress sur mesure au Royaume-Uni ? Une petite extension utilitaire coûte généralement 1 500 à 3 000 £, une intégration de taille moyenne avec une API tierce et des écrans d’administration coûte 3 000 à 15 000 £, et une extension produit de grande ampleur avec ses propres tables et son infrastructure de mise à jour coûte 20 000 à 75 000 £ ou davantage. Les développeurs capables de ce travail facturent à peu près 400 à 600 £ par jour.

Est-il parfois acceptable de modifier le cœur de WordPress ou les fichiers d’une autre extension ? Non. Ces modifications sont effacées par la mise à jour suivante, sans erreur et le plus souvent sans être découvertes avant que quelque chose ne cesse de marcher. Utilisez plutôt des actions et des filtres. Si le hook dont vous avez besoin n’existe pas, enveloppez le comportement, forkez l’extension sous gestion de versions, ou demandez le hook en amont.

Suis-je propriétaire d’une extension que j’ai payée à quelqu’un ? Vous possédez votre exemplaire et ce que dit le contrat. La GPL vous donne les sources, le droit de les modifier et celui d’engager n’importe qui d’autre pour les maintenir, et elle ne vous oblige pas à les publier, donc une extension construite pour une entreprise peut rester privée. Elle n’empêche pas le développeur de revendre le même travail, alors inscrivez l’exclusivité au contrat si elle compte.

Comment éviter qu’une extension casse quand WordPress se met à jour ? Testez-la face à chaque version candidate sur une copie de préproduction avant la publication de cette version, faites tourner la préproduction avec WP_DEBUG activé pour que les avis d’obsolescence remontent tôt, et déclarez dans son en-tête les versions de PHP et de WordPress prises en charge. WordPress exige PHP 7.4 comme plancher et recommande 8.3 ou plus récent.