PHP/Symfony

Travailler avec WordPress sans déprimer

Une vision saine, version 2019, pour une architecture WordPress plus propre

Traduit de l’anglais avec l’aide d’une IA. Lire l’original

Bon, vous avez choisi de démarrer un nouveau projet WordPress (ou quelqu’un l’a choisi pour vous) ? Bonne nouvelle : nous sommes en 2019, et ce n’est plus forcément la galère dès le départ.

Dans cet article, je ne vais pas tout expliquer en détail, il me faudrait un livre entier pour ça. Je vais simplement vous indiquer des pistes : comment faire mieux que la méthode officielle, bancale, qui se justifiait il y a 10 ans. Cet article s’adresse à tout le monde : développeurs web, back-end, front-end…

Promis, il n’y a pas plus de 3 minutes de lecture par chapitre.

Dans l’entreprise où je travaille, nous avons plus de 150 sites WordPress. J’ai décidé que les nouveaux adopteraient tout ce que je décris dans cet article, et que les anciens y migreraient petit à petit. À l’heure où j’écris ces lignes, ~10% d’entre eux utilisent déjà cette architecture.

Remarque : si vous repérez des fautes d’anglais dans la version originale, j’accepte volontiers les pull requests pour les corriger.


Utilisez Bedrock Edition

C’est LE point de départ de cet article. Bedrock est un boilerplate WordPress créé par Roots. Il s’appuie sur des outils de développement modernes et adopte la méthodologie 12 Factor App. Autant de choses que WordPress ne peut pas (ou ne veut pas) faire, à cause de son statut de dinosaure 🦖 du monde PHP.

Voici les principaux avantages de ce boilerplate :

Prise en charge des variables d’environnement

Si vous avez déjà travaillé sur des applications modernes, le principe de stocker la configuration de votre application dans des variables d’environnement vous est familier.

L’intérêt des variables d’environnement, c’est de pouvoir disposer de plusieurs environnements, comme production, staging et development, et par exemple de n’activer l’envoi d’e-mails qu’en production, sans devoir modifier en permanence des valeurs selon l’environnement sur lequel vous vous trouvez.

En 2019, tous les grands frameworks du monde PHP ont une notion de variables d’environnement : Symfony, Laravel, Yii. C’est une pratique courante, qui existe depuis des décennies dans les mondes Windows et Unix.

Les variables d’environnement peuvent être stockées dans un fichier en development, et injectées dans votre serveur web pour les environnements de staging et de production. Pas de panique : beaucoup d’hébergeurs dignes de ce nom proposent une interface pour manipuler les variables d’environnement. Regardez par exemple ce que fait Heroku pour les variables d’environnement.

Des variables d’environnement, ça ressemble à ceci :

DATABASE_URL=mysql://john_doe:p4sSWorD@127.0.0.1:3306/db_awesome_blog
WP_ENV=development
WP_HOME=http://my-awesome-wordpress.vm
WP_SITEURL=${WP_HOME}/wp

Une arborescence mieux pensée

Tout est mieux rangé, et du coup, on trouve plus facilement ce que l’on cherche. L’arborescence ressemble à ceci :

├── composer.json
├── .env <----------------- where majority of the configuration will happen, must not be committed inside git repository
├── config
│   ├── application.php
│   └── environments
│       ├── development.php
│       ├── staging.php
│       └── production.php
├── vendor  <-------------- where the 3rd party code lives
└── web
    ├── app
    │   ├── mu-plugins
    │   ├── plugins
    │   ├── themes
    │   └── uploads
    ├── wp-config.php <-- do not touch this file
    ├── index.php <------ entry point of your application, each web request are send through this file
    └── wp <------------- WordPress files

Point bonus pour la sécurité : la racine web (le point d’entrée de votre application), web/index.php, est isolée du reste de l’arborescence. Vos visiteurs ne peuvent donc pas accéder à votre fichier config/production.php, puisqu’il se trouve en dehors de web/.

Des builds reproductibles

Le principe des builds reproductibles peut se résumer ainsi : peu importe quand et où je construis et déploie l’application, j’obtiendrai toujours la même version du code (et des plugins).

Comme vous allez travailler sur votre environnement local (si ce n’est pas déjà le cas 👀), vous devez garantir que ce que vous avez sur votre ordinateur sera exactement identique à ce que vous aurez en production. Prenons un exemple pour comprendre pourquoi c’est important.

Imaginons que votre site tourne en production depuis à peu près un an, jusqu’au jour où une vilaine panne matérielle frappe votre serveur. Tous vos fichiers sont perdus. Heureusement pour vous, vous avez bien fait les choses : votre répertoire uploads/ et une sauvegarde de votre base de données sont stockés quelque part dans le cloud.

L’étape suivante consiste à réinstaller WordPress à partir de la sauvegarde de la base de données et des médias. Puis à réinstaller les 20 plugins qu’utilisait ce site. En un an, les plugins évoluent (et c’est tant mieux), mais votre thème, ou certaines de vos fonctionnalités, ne sont peut-être pas compatibles avec WooCommerce v3.5. Résultat possible : un site cassé.

Autre exemple : quand vous travaillez à plusieurs sur un projet, sans builds reproductibles, la personne chargée du front-end se retrouvera sur WordPress 5.1.1 pendant que le développeur en sera encore à la version 4.9.10, incapable de reproduire le bug que vous rencontrez avec cette configuration.

Ce principe implique d’interdire toute installation ou mise à jour de plugins ou de thèmes depuis le back-office de WordPress. Par défaut, Bedrock s’en charge quand vous êtes dans l’environnement de production. Et comme WordPress est lui aussi une dépendance composer, le mettre à jour est aussi simple qu’un composer update roots/wordpress.

Mais comment garantir des builds reproductibles ? La réponse se trouve juste en dessous : utilisez Composer.

Utilisez Composer

Le boilerplate Bedrock vous permet de gérer les dépendances de votre projet avec Composer, comme devrait le faire tout projet PHP moderne et digne de ce nom (oui, WordPress, c’est de toi que je parle).

Vous voulez installer WooCommerce ? Ouvrez votre terminal et tapez :

composer require woocommerce/woocommerce

Composer récupère la dernière version et la fige dans votre composer.lock. C’est grâce à ce fichier que vous obtenez toujours exactement les mêmes versions, quels que soient le moment et l’endroit où vous installez les dépendances de votre projet avec composer install. Vu son importance, ce fichier doit être commité ⚠️.

Le site roots.io propose un bon article sur l’utilisation de composer avec WordPress.

Tous les plugins peuvent aussi s’installer via composer. Comment ? 3 scénarios sont possibles :

Le plugin fournit un composer.json

Derrière le plugin, il y a un développeur ou une entreprise qui ne souffre pas du syndrome du « WordPress d’un autre âge » et qui développe des applications modernes. Concrètement, son plugin est livré avec un fichier composer.json qui permet de l’installer via composer. Dans ce scénario, vous pouvez chercher votre plugin sur packagist.org, le dépôt qui recense tous les paquets PHP publics de l’univers ✨.

Le plugin ne fournit pas de composer.json

Si vous ne trouvez pas le plugin sur packagist.org, c’est qu’il n’a pas de fichier composer.json. Pas d’inquiétude, wpackagist vient à la rescousse. Outlandish a créé ce projet pour permettre d’installer 100% des plugins et des thèmes à partir du dépôt officiel de WordPress. Ce plugin WordPress-ci, par exemple, est introuvable sur packagist.org ; utilisons donc wpackagist à la place :

composer require wpackagist-plugin/cookie-law-info

Le plugin est payant ou privé et n’a pas de composer.json

Plusieurs options s’offrent alors à vous :

Utilisez un meilleur moteur de templates que PHP : « I’m yelling timber » !

Et côté templates, alors ? PHP n’est pas un bon moteur de templates. Difficile à lire, difficile à écrire, et ne nous voilons pas la face : nous ne sommes plus au début des années 2010, nous avons des outils conçus spécialement pour chaque usage. Le meilleur moteur de templates PHP, c’est Twig. Il est très utilisé dans l’écosystème Symfony (mais pas seulement).

Alors, comment utiliser Twig dans un projet WordPress ? Heureusement pour nous, il existe un projet appelé Timber.

Un plugin pour écrire des thèmes WordPress avec du code orienté objet et le moteur de templates Twig

Il ajoute un modèle Vue / Contrôleur, ce qui profitera énormément à la lisibilité et à la maintenance de votre thème.

Une fois que vous aurez commencé à écrire vos thèmes en Twig, vous ne reviendrez plus au PHP brut. Point.

Astuce de pro : comme vous ne concevez plus vos thèmes en PHP brut mais en Twig, le serveur doit transformer vos fichiers .twig en fichiers .php. Cette phase s’appelle la compilation, et elle prend du temps. Heureusement, il existe un système de cache qui stocke les templates compilés sur votre système de fichiers, pour que Twig ne recompile pas vos templates à chaque requête que votre serveur doit traiter.

Par défaut, Timber n’active pas ce cache.

Voici comment l’activer sans risque. Vous gagnerez ainsi jusqu’à 40% de temps de génération. Dans le functions.php de votre thème :

// Active le cache de Twig
if (class_exists('Timber') && WP_ENV === 'production') {
    Timber::$cache = true;
}

// Remplacez WP_ENV === 'production' par !WP_DEBUG si vous n'êtes PAS dans une structure Bedrock
Écart de temps de rendu avant/après l’activation du système de cache de Twig. Plus de détails sur cette trace Blackfire.
Écart de temps de rendu avant/après l’activation du système de cache de Twig. Plus de détails sur cette trace Blackfire.

Notez que ce sont les templates PHP compilés qui sont mis en cache, pas les données. Une modification des templates .twig en production ne sera pas prise en compte tant que vous n’aurez pas vidé le cache. (Mais qui modifie des fichiers en production ? 😱 Certainement pas vous !)

Stockez de préférence vos assets dans le cloud

Stocker vos assets sur le système de fichiers local, dans le répertoire uploads/, c’est pratique, mais tôt ou tard, il vous faudra synchroniser ce dossier entre tous vos environnements. Sinon, votre version locale aura un jeu de médias complètement différent de celui de la production, et inversement.

Vous vous souvenez des builds reproductibles ? Ce serait bien de pouvoir stocker nos médias à distance pour ne plus dépendre du système de fichiers local, non ? Spoiler : oui, c’est bien.

L’autre gros avantage de cette technique, c’est qu’elle permet à votre site de monter en charge pour absorber un énorme pic de trafic. Il vous suffit d’ajouter des serveurs (scaling horizontal) : comme votre build est reproductible, le projet et les plugins seront identiques sur les serveurs numéro 1, 2, … 23, 24.

Quelques services pour stocker vos médias dans le cloud :

  • Amazon S3. Le plus connu, mais pas le moins cher 💸.
  • Digital Ocean Spaces. Compatible S3, 45 emplacements disponibles.
  • Scaleway. Stockage bon marché, compatible S3, mais seulement 2 emplacements disponibles (Paris et Amsterdam).

✅ Pensez à toujours choisir un bucket situé près de la majorité de votre audience. Je vous conseille de choisir une « API compatible S3 », car il n’existe pas beaucoup de plugins WordPress qui permettent d’utiliser un stockage distant et qui fonctionnent dans la majorité des cas.

Voici une liste de plugins gratuits qui permettent de stocker vos médias sur différents stockages cloud :

Ce que je souhaite pour WordPress, c’est qu’il utilise une abstraction du système de fichiers comme Flysystem. Nous n’aurions alors plus besoin de plugins qui bidouillent WordPress pour lui dire d’envoyer les médias et les assets ailleurs. En attendant, vous pouvez utiliser l’un des plugins ci-dessus.

Connaissez vos outils : ACF peut plomber les performances de votre site

Si vous utilisez Advanced Custom Fields, alias ACF, vous devez connaître certains goulots d’étranglement que vous risquez de rencontrer si vous abusez de la fonction get_field.

N’exposez que les variables dont vous avez besoin sur chaque page. J’ai vu beaucoup d’exemples, y compris dans la documentation, qui vous invitent à faire :

$context['options'] = get_fields('options'); // retrieve all options fields from the database

Timber::render('index.twig', $context); // render the index.twig page with the options passed to the twig context so you can use them inside your view.

Quand vous faites ça, chaque champ récupéré déclenche une requête en base de données. Si vous avez 35 champs dans la catégorie options, cela déclenche donc 35 requêtes rien que pour récupérer ces champs. Mais avez-vous vraiment besoin de ces 35 champs sur toutes les pages ?

Que faire à la place ?

$context['options']['twitter_link'] = get_fields('twitter_link', 'options');
$context['options']['logo_footer_img'] = get_fields('logo_img', 'options');
Timber::render('index.twig', $context);
{# utilisez-les dans votre template .twig #}
<footer>
    <a href='{{ options.twitter_link }}'>Follow us</a>
    <img src='{{ options.logo_footer_img }}' />
</footer>

Sur un vrai projet, ce genre d’optimisation apporte des gains de performance importants.

Écart de temps de rendu avant/après avoir récupéré uniquement les champs ACF strictement nécessaires. -850 requêtes en base de données et -37% de temps de rendu ! Plus de détails sur cette trace Blackfire.
Écart de temps de rendu avant/après avoir récupéré uniquement les champs ACF strictement nécessaires. -850 requêtes en base de données et -37% de temps de rendu ! Plus de détails sur cette trace Blackfire.

L’explicite vaut mieux que l’implicite. Cela aide aussi vos collègues à faire une revue de code plus précise de votre travail, un exercice bien plus difficile quand on ne sait pas à quoi votre template a accès.

Et les champs vidéo et image ? Si vous en utilisez, sachez que si vous les récupérez « tels quels » avec get_field('my_youtube_video'), le code va essayer de récupérer les métadonnées (taille, orientation, durée…) des vidéos et des images.

Où est le problème ? Eh bien, comme vous hébergez vos médias dans le cloud (ou sur un service distant, YouTube dans cet exemple), les vidéos et les images doivent être téléchargées côté serveur pour obtenir ces informations, dont vous n’aurez pas besoin dans 98% des cas. Faire des appels HTTP externes depuis votre serveur vers Internet fait alors perdre un temps précieux et tue les performances de votre site.

J’ai vu des projets passer presque 1 seconde en appels externes, rien que pour récupérer les informations de 10 images dans le cloud, ce qui empêchait le serveur d’envoyer le HTML au navigateur pendant tout ce temps.

Dans ces cas-là, vous devez utiliser get_field('my_youtube_video', null, false). Le dernier paramètre (false) indique de récupérer la valeur brute du champ (ici, l’URL YouTube de la vidéo, sans mise en forme ni traitement supplémentaire). Autre solution : configurer le champ en tant que champ « URL » (et non oEmbed), et, pour les images, en « Image URL ».

Écart de temps de rendu avant/après la configuration des champs en champs URL plutôt qu’en oEmbed. Tous les appels HTTP externes que faisait le serveur pour récupérer les métadonnées des images ou des vidéos ont disparu (-7), soit 35% de temps gagné (-846ms) ! Plus de détails sur cette trace Blackfire.
Écart de temps de rendu avant/après la configuration des champs en champs URL plutôt qu’en oEmbed. Tous les appels HTTP externes que faisait le serveur pour récupérer les métadonnées des images ou des vidéos ont disparu (-7), soit 35% de temps gagné (-846ms) ! Plus de détails sur cette trace Blackfire.

Lintez votre code

Votre thème embarque du CSS, du JavaScript et un peu de PHP ? Vous devriez les linter, pour vous assurer d’écrire du code conforme aux standards du métier. Utilisez :

Utilisez l’outil de build ou le task runner avec lequel vous êtes à l’aise (Gulp, Brunch). Si vous n’avez pas encore choisi d’outil et qu’il vous en faut un simple, je vous suggère de jeter un œil à yProx-CLI, que nous utilisons depuis un moment chez Yproximité et que nous venons de publier en open source.

Pour votre santé mentale, par pitié, n’ouvrez pas le code des plugins que vous téléchargez. Vous pourriez être choqué par la piètre qualité du code auquel vous avez affaire, et finir par réécrire une bonne partie de vos plugins WordPress.

Utilisez un service d’intégration continue (CI)

Utilisez une CI (Travis, GitLab, CircleCI, Jenkins). Voici notre fichier .travis.yml de base. Il vérifie que notre composer.json est valide, que notre code PHP passe le linter, et pareil pour nos fichiers JavaScript. Aucune pull request ne devrait être mergée avec des erreurs de lint : une fois en production, elles ne seront jamais corrigées. Vos pairs peuvent ainsi relire votre code en se concentrant sur l’essentiel.

dist: trusty

language: php
php:
  - '7.3'

cache:
  yarn: true
  directories:
    - ./vendor
    - ./node_modules
    - ~/.composer/cache/files

before_install:
  - |
    if [ -f "package.json" ]; then
        set -e # properly fails Travis if one of the following commands fails
        nvm install v10
        nvm use 10
        node --version
        npm --version
        npm install --global yarn
        set +e # re-enable previous behaviour
    fi

script:
  - composer validate
  - composer install
  - vendor/bin/php-cs-fixer fix -v --dry-run --using-cache=no
  - if [ -f "package.json" ]; then yarn install; fi;
  - if [ -f "package.json" ]; then yarn lint; fi;
  - if [ -f "package.json" ]; then yarn build; fi;

Mettez en place un outil de mise à jour automatique des dépendances

Avec 150 projets WordPress dans notre organisation, lancer composer update sur chacun d’eux une fois par semaine n’est pas franchement ce dont nous avons envie : il a fallu trouver comment gérer les mises à jour.

Nous utilisions ManageWP pour mettre à jour WordPress et toutes ses dépendances, mais cette option n’est plus envisageable puisqu’elle casse le principe des builds reproductibles. Rien ne doit altérer ni modifier votre environnement. Le code doit être exactement le même que dans votre dépôt git.

Une option viable pour nous a été de déléguer les mises à jour à Dependabot, dont le rôle est d’ouvrir une pull request dès qu’une mise à jour est disponible. Il existe des alternatives, par exemple Renovate.

Nous l’avons configuré pour merger automatiquement les pull requests quand il s’agit d’une mise à jour mineure ou d’un patch (v1.0.0 -> v1.1.4 sera mergée automatiquement, mais pas v1.0.0 -> v2.0.0).

⚠️ L’écosystème WordPress ne respecte pas forcément le SemVer, parce que… (je cherche encore la raison…).

Comme nous avons choisi une « Platform as a Service » (PaaS) pour héberger tous nos WordPress, dès qu’une pull request est mergée sur la branche master, le projet est redéployé en production.

Quel que soit l’hébergement choisi, vous devriez avoir du déploiement automatique. Sinon, si vous devez redéployer chaque projet à la main et que vous en avez un très grand nombre, vos projets seront à jour, mais les mises à jour n’arriveront jamais en production.

Choisissez le bon hébergement

Je vous conseille vivement d’opter pour une offre PaaS. Les principaux avantages d’une Platform as a Service (PaaS) sont :

  • Aucune maintenance à prévoir, ni pour le serveur ni pour les logiciels
  • Des déploiements automatisés
  • Prête pour le scaling vertical et horizontal
  • Bon marché
  • Simple
  • Isolée (pas de contamination croisée d’un site A vers un site B hébergés sur le même serveur)

Quelques exemples de PaaS :

⛔️ N’utilisez jamais de FTP ou de SFTP pour déployer en production. Comme vous devez construire votre projet en lançant composer install, et peut-être compiler vos assets avec yarn build, le bon vieux copier-coller du projet sur votre FTP, qui avait déjà un pied dans la tombe, est définitivement et enfin mort ⛔️

Ce que vous devriez déjà faire

Voici une liste de choses que vous devriez déjà faire, et qui ne méritent donc pas un chapitre à elles seules :

  • Travaillez avec une machine virtuelle, pour que tout le monde ait le même environnement. Vous pouvez utiliser trellis, ou manala si vous êtes plus technique
  • Utilisez la dernière version de PHP disponible
  • Travaillez avec des pull requests
  • Relisez le code de vos pairs, et demandez-leur de relire le vôtre
  • Configurez GitHub pour interdire le merge d’une pull request qui n’a pas été relue.

Note aux développeurs de plugins WordPress

Pour faire de l’écosystème WordPress un monde meilleur, s’il vous plaît, respectez au moins ces 5 règles :

⚠️ Ne partez pas du principe que les utilisateurs de votre plugin stockent leurs fichiers uploadés sur le système de fichiers local. Partez du principe que non. Passez par une couche d’abstraction pour manipuler les fichiers, par exemple Flysystem.

Ce raisonnement s’explique surtout par les cas suivants :

  • Quand vous hébergez votre application dans le cloud, vous voudrez peut-être pouvoir la faire monter en charge en ajoutant des serveurs.
  • Quand vous hébergez votre WordPress sur une infrastructure immuable, ce qui veut dire, en gros :
    • Soit vous ne pouvez pas écrire sur le serveur, ce qui empêche toute modification ;
    • Soit les modifications que vous avez faites sont perdues quand le projet est redéployé. (Un déploiement a lieu chaque fois que vous mergez du code dans votre dépôt, ou quand vous faites varier le nombre de serveurs de votre application, en en ajoutant ou en en retirant. Seul ce qui se trouve dans votre dépôt git est déployé sur le serveur.)

⚙ Proposez un moyen de configurer votre plugin par variables d’environnement, avec un repli sur la base de données si la variable d’environnement est introuvable. Variables d’environnement > configuration stockée en base de données, par ordre d’importance. Cela nous permet d’automatiser l’installation des plugins et de regrouper toute la configuration au même endroit.

🎻 Rendez votre plugin installable avec composer : il faut pour cela un fichier composer.json de 10 lignes minimum. Regardez quelques exemples ici, ici ou là. L’étape suivante consiste à taguer vos versions avec les GitHub Releases en respectant SemVer, puis à soumettre votre paquet sur packagist.org en saisissant l’URL GitHub de votre dépôt.

🔒 Prenez en charge les versions maintenues de PHP

💅🏽 Lintez votre code PHP avec PHP-CS-Fixer, bon sang ! Choisissez entre le preset PSR2 et celui de Symfony, mais choisissez-en un et corrigez votre code.

Conclusion

Si vous voulez voir une structure de projet qui met en œuvre la plupart des points abordés dans cet article, jetez un œil à mon exemple de boilerplate WordPress basé sur Bedrock et Timber. Ce dépôt n’est pas fait pour être utilisé « tel quel », mais il vous aidera à comprendre comment structurer les fichiers de votre projet.

  • Infra 14
  • PHP/Symfony 4
  • Outils de dev 3
  • IA 1

Article 18 sur 22Tous les articles