Deux mondes cohabitent dans le même dépôt de code : celui des projets internes récents, tous passés sur PHP 8.3 depuis plusieurs mois, et celui d’un plugin distribué publiquement, dont une partie non négligeable des utilisateurs reste sur des hébergements mutualisés figés à PHP 7.4. Renoncer à cette compatibilité étendue couperait l’extension d’une part significative de sa base d’installation ; l’imposer sans discipline finit par produire du code truffé de conditions version_compare() illisibles.
La solution retenue ne consiste pas à écrire deux versions du plugin, mais à borner strictement le sous-ensemble du langage utilisé dans le code partagé, et à vérifier cette contrainte par une matrice de tests plutôt que par la seule vigilance des développeurs.
Ce que le code source s’interdit d’utiliser
Certaines fonctionnalités introduites après PHP 7.4 restent hors de portée du code partagé : les énumérations natives (PHP 8.1), les propriétés en lecture seule (PHP 8.1), ou encore les types d’intersection (PHP 8.1). Ce périmètre est documenté dans le fichier CONTRIBUTING.md du dépôt, avec la liste précise des fonctionnalités autorisées et interdites, régulièrement mise à jour à chaque montée de version du socle interne.
Une matrice de CI qui couvre les deux bornes

La configuration GitHub Actions exécute la suite PHPUnit sur deux versions de PHP, sans dupliquer aucun fichier de test :
strategy:
matrix:
php-version: ['7.4', '8.3']
wordpress-version: ['6.4', '6.7']
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
- run: composer install --no-progress
- run: vendor/bin/phpunit
Chaque combinaison exécute exactement la même suite de tests contre exactement le même code source. Aucune branche conditionnelle dans les tests eux-mêmes ne distingue les deux versions : si le code fonctionne réellement sur les deux, les mêmes assertions passent des deux côtés.
PHPUnit Polyfills pour absorber l’écart de version de PHPUnit
PHP 7.4 impose en pratique une version ancienne de PHPUnit, incompatible avec les versions récentes utilisées sur PHP 8.3. Le paquet yoast/phpunit-polyfills comble cet écart en fournissant des traits qui exposent une API stable de méthodes d’assertion, indépendamment de la version de PHPUnit réellement installée. Les tests eux-mêmes n’ont ainsi pas besoin de connaître la version de PHPUnit exécutée.
PHPCompatibility en complément, pas en substitut
La matrice de tests vérifie que le comportement fonctionnel reste identique sur les deux versions, mais elle ne détecte pas systématiquement l’usage d’une syntaxe incompatible qui échouerait uniquement à l’installation sur un hébergement en PHP 7.4 réel. Une analyse statique complémentaire, exécutée en amont de la matrice, repère ces cas avant même de lancer les tests.
Ce que cette approche ne résout pas
- Elle ne dispense pas de tester manuellement, de temps en temps, une installation réelle sur un hébergement mutualisé représentatif du parc visé.
- Elle ralentit la CI, puisque chaque changement déclenche désormais deux exécutions complètes au lieu d’une.
- Elle impose une discipline de revue stricte : tout code utilisant une fonctionnalité récente sans vérification préalable casse silencieusement la compatibilité descendante.
Organiser la revue de code autour de cette contrainte
Une check-list courte, ajoutée au modèle de pull request du dépôt, rappelle systématiquement le périmètre autorisé avant toute fusion : aucune énumération native, aucune propriété en lecture seule, aucun type d’intersection. Cette liste reste volontairement courte, sous peine d’être ignorée par lassitude, et se limite aux fonctionnalités effectivement rencontrées comme source d’erreur lors des mois précédents.
Un relecteur qui repère une syntaxe hors périmètre lors d’une revue dispose d’un motif de refus clair et objectif, plutôt que d’une appréciation subjective sur la qualité du code proposé. Cette objectivité facilite également l’intégration de nouveaux contributeurs, qui découvrent la contrainte dès leur première pull request plutôt qu’après un incident de compatibilité en production.
Une compatibilité large ne se maintient pas par bonne volonté, elle se maintient par une matrice qui échoue quand quelqu’un l’oublie.
En résumé
Couvrir deux mondes de compatibilité PHP avec une seule base de code repose sur trois piliers distincts : un périmètre de langage documenté et restreint, une matrice de CI qui exécute la même suite sur les deux bornes de version, et une couche de compatibilité PHPUnit qui masque l’écart entre les versions du framework de test. Aucun de ces trois piliers ne fonctionne seul ; ensemble, ils évitent de dupliquer le code du plugin pour chaque version de PHP visée.