Blog
Mettre à niveau PHP 5.6 et les anciennes applications PHP 8 en toute sécurité
La mise à niveau d’une ancienne application PHP ne se résume pas à une commande Composer. Une migration sûre protège le métier avec Pest, introduit PHPStan progressivement, met à niveau framework et dépendances, puis optimise à partir de mesures.
Migration PHP avec Symfony et Laravel : guide technique
Une mise à niveau de PHP change l’environnement d’exécution, pas seulement un numéro de version. Une page peut fonctionner alors qu’un import échoue avec un autre exécutable CLI ou qu’un worker ne sait plus lire un ancien message. Il faut donc vérifier les comportements requis sur les véritables chemins d’exécution de l’application.
Ce guide porte sur la compatibilité, la protection contre les régressions et la vérification du déploiement. Les exemples sont illustratifs et ne relatent pas des migrations réalisées chez des clients GiSoft.
Définir les environnements de migration
Recensez les points d’entrée HTTP, commandes console, tâches planifiées, consommateurs de messages, imports, exports et scripts de maintenance. Pour chaque chemin, relevez l’exécutable PHP, les extensions, la configuration, les pilotes de base de données et les dépendances externes. PDF, traitement d’images, SOAP et courriels peuvent nécessiter autre chose que des paquets Composer.
Une application PHP 5.6 peut demander des combinaisons intermédiaires de PHP, de framework et de dépendances. Une ancienne application PHP 8 peut présenter moins d’écarts. Il n’existe pas de succession universelle de versions : retenez des combinaisons vérifiables à partir des contraintes réelles. Les versions intermédiaires en fin de vie sont des outils de migration en environnement isolé, pas automatiquement des destinations acceptables en production.
Consultez le tableau officiel de maintenance PHP et le guide de mise à niveau du framework pour choisir la cible. Maintenance active et correctifs de sécurité seuls sont deux situations différentes. Choisissez une version corrective maintenue avec une durée de maintenance restante adaptée au projet ; les exemples ci-dessous ne font pas ce choix à votre place.
Environnement Ce qu’il permet de vérifier
Existant Comportement initial, outils compatibles
Migration Compatibilité des versions choisies
Proche de la cible HTTP, CLI, workers et déploiement
L’analyse statique utilise un PHP compatible avec ses outils.
Elle ne remplace pas l’exécution dans l’environnement cible.Les versions actuelles de Pest et PHPStan ne s’installent pas simplement sur PHP 5.6. Conservez-y une suite PHPUnit compatible ou testez l’ancienne application depuis l’extérieur. Exécutez l’analyse moderne dans un environnement distinct approprié, en tenant compte de l’amorçage et de l’autoloading, avec la cible PHP configurée. Les tests qui démarrent l’application exigent des dépendances exécutables dans leur environnement.
Les extraits utilisent une syntaxe moderne : propriétés typées à partir de PHP 7.4, mixed et arguments nommés à partir de PHP 8.0, propriétés promues readonly à partir de PHP 8.1 et classes readonly à partir de PHP 8.2. Ce sont des minima syntaxiques, pas les exigences d’une version précise de Pest ou du framework.
Identifier les blocages avec Composer
Examinez composer.json, composer.lock, les extensions et les plugins avant de modifier les contraintes. Affectez à la variable shell PHP_UPGRADE_TARGET la version PHP exacte à étudier. Les deux dernières commandes sont des alternatives, pas des étapes successives :
composer show --direct
composer outdated --direct
composer why-not php "${PHP_UPGRADE_TARGET:?}"
composer prohibits php "${PHP_UPGRADE_TARGET:?}"show --direct liste les dépendances directes ; outdated --direct recherche des versions plus récentes. why-not et prohibits sont des alias qui expliquent les blocages déclarés. Ils ne prouvent pas la compatibilité applicative et ne construisent pas le plan de migration.
Composer doit lui-même être compatible avec son environnement. Sa documentation indique la branche 2.2 LTS pour les anciens PHP. Vérifiez ses options et la compatibilité des plugins plutôt que de supposer que le dernier exécutable fonctionne sur PHP 5.6. L’analyse concerne tout le graphe de dépendances, pas seulement les paquets directs.
Une valeur simulée par config.platform aide à résoudre les dépendances, mais n’installe ni PHP ni ses extensions. Ensuite, composer check-platform-reqs contrôle l’environnement CLI réel en ignorant cette simulation. Les environnements web et des workers demandent leurs propres vérifications. Ignorer les exigences de plateforme ne corrige pas un déploiement.
Protéger le comportement avant de changer son implémentation
Commencez par les erreurs coûteuses : authentification et droits, commandes, factures, résultats de paiement, imports et réponses publiques de l’API. Un petit test peut protéger une règle sans reconstruire toute l’application :
it(
'ne publie pas après un paiement échoué',
function (): void {
$payment = Payment::failed();
$invoice = Invoice::forPayment($payment);
$publisher = new InMemoryInvoicePublisher();
$service = new InvoicePublishingService($publisher);
$service->publish($invoice);
expect($invoice->isPublished())->toBeFalse()
->and($publisher->publishedInvoices())->toBeEmpty();
},
);Payment, Invoice, InMemoryInvoicePublisher et InvoicePublishingService sont des exemples propres au projet. Le test suppose qu’un paiement échoué empêche la publication sans lever d’exception. Il vérifie cette règle et le service de publication en mémoire, pas la persistance en base ni un service réel d’envoi.
Les tests de caractérisation consignent les comportements à comprendre avant de les modifier :
it(
'conserve l’arrondi de la commande importée',
function (): void {
$calculator = new LegacyOrderCalculator();
$result = $calculator->calculate(
netAmount: 19.995,
taxRate: 23,
);
expect($result->grossAmount)->toBe(24.59);
},
);Cette référence hypothétique suppose que le montant net n’est pas arrondi au préalable et que le brut est arrondi à deux décimales. En calcul décimal, 19.995 × 1.23 = 24.59385 donne 24.59 avec un arrondi usuel au plus proche, les moitiés vers le haut. Arrondir d’abord le net à 20.00 donnerait 24.60. Le calculateur omis et sa règle réelle doivent être vérifiés dans l’environnement initial ; ces chiffres ne documentent aucun traitement comptable réel.
Les nombres flottants représentent volontairement une ancienne API. Leur représentation binaire ne peut pas exprimer exactement de nombreux montants décimaux. Cette assertion n’est donc pas une recommandation pour concevoir des calculs financiers. Une représentation décimale ou des entiers avec une échelle adaptée demandent des règles explicites de précision et d’arrondi. Faites évoluer ce contrat séparément de la conservation du résultat initial.
Vérifiez aussi les comparaisons non strictes, les nombres renvoyés comme chaînes par les pilotes de base, null face à une chaîne vide, les dates et fuseaux horaires, le tri, le JSON et la sérialisation. Réussir un test de caractérisation préserve une observation, pas nécessairement une règle métier juste.
Conserver les tests utiles et choisir des outils compatibles
Adopter Pest ne nécessite pas de réécrire les tests PHPUnit existants. Pest repose sur PHPUnit, mais sa version, celle de PHPUnit, les plugins et la configuration doivent convenir à l’environnement PHP choisi. Une ancienne suite ne fonctionne pas automatiquement avec n’importe quelle nouvelle version de Pest. Garder PHPUnit pendant la migration est un choix valable.
Les tests unitaires portent sur des règles isolées, les tests d’intégration sur les dépôts et dépendances réelles, les tests fonctionnels sur le comportement HTTP et les droits. Les tests d’architecture ne contrôlent que les règles configurées. Les smoke tests confirment quelques parcours essentiels, sans remplacer ces vérifications approfondies.
Rendre les hypothèses de types visibles avec PHPStan
Commencez à un niveau d’analyse utile, incluez les appelants et dépendances concernés, puis corrigez les constats les plus risqués avant de renforcer les exigences :
vendor/bin/phpstan analyse src --no-progressCette commande s’exécute dans l’environnement d’analyse préparé. Configurez si nécessaire la cible PHP, les extensions d’analyse du framework et la découverte des symboles. Une liste de référence des erreurs existantes, ou baseline, peut les distinguer des nouvelles. Des exclusions larges ou une régénération répétée de cette liste peuvent masquer des régressions. Le niveau maximal n’est pas un préalable à toute mise à niveau.
Si une propriété contient réellement un client ou null, élargir son type fait perdre une information utile :
private mixed $customer;La déclaration plus précise décrit ce contrat et initialise la propriété :
private ?Customer $customer = null;Ce sont des extraits de membres de classe. N’initialisez pas une relation obligatoire à null simplement pour faire taire un avertissement. Le code plus ancien peut utiliser un PHPDoc adapté tant que son environnement ne prend pas en charge les propriétés typées.
De même, une signature de dépôt qui promet seulement un tableau renseigne peu les appelants :
/**
* @return array
*/
public function findActiveCustomers(): array;Un contrat plus utile décrit chaque ligne :
/**
* @return list<array{
* id: int,
* email: string,
* active: bool
* }>
*/
public function findActiveCustomers(): array;Il s’agit de deux déclarations alternatives de méthode d’interface. Le mapper doit réellement produire les entiers, chaînes et booléens annoncés ; les annotations ne convertissent pas les valeurs issues de la base.
Une collection Doctrine a besoin du type de ses éléments et d’une initialisation pour les nouvelles entités :
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
// Membres d’une entité illustrative.
/** @var Collection<int, Order> */
private Collection $orders;
public function __construct()
{
$this->orders = new ArrayCollection();
}
/** @return Collection<int, Order> */
public function getOrders(): Collection
{
return $this->orders;
}Cet extrait n’est pas un mapping d’entité complet. Intégrez l’initialisation au véritable constructeur et adaptez le type de clé si l’association utilise des clés textuelles. Le PHPDoc décrit la collection pour l’analyse ; il ne valide pas chaque objet ajouté à l’exécution. Mappings ORM, hydratation et colonnes acceptant null nécessitent encore des tests d’intégration.
Ne modifier les frontières du framework que lorsque c’est utile
Pour Symfony, examinez le guide concerné pour la configuration des services, l’authentification, la résolution d’arguments, les formulaires, la validation, les mappings Doctrine et la sérialisation. Regroupez les dépréciations par responsable et dépendance. Une mise à niveau de paquet peut être nécessaire avant une modification du code applicatif. Masquer globalement les avertissements n’aide pas à les résoudre.
Pour Laravel, vérifiez les versions réelles des fournisseurs de services, middleware, mécanismes d’authentification, conversions Eloquent, sérialisation des files, courriels, intégrations de fichiers et paquets communautaires. Remplacer toutes les façades ou refaire l’architecture n’est pas une exigence du changement de PHP.
Une frontière de contrôleur aide lorsque les changements de transport risquent de se propager à la logique métier :
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
final class CreateOrderController
{
public function __construct(
private readonly CreateOrderServiceInterface $service,
) {
}
public function __invoke(
CreateOrderRequest $request,
): JsonResponse
{
$result = $this->service->create($request->toDto());
return new JsonResponse(
data: $result->toArray(),
status: Response::HTTP_CREATED,
);
}
}Cet extrait Symfony nécessite PHP 8.1 ou plus. Il suppose que le CreateOrderRequest du projet est construit à partir des entrées HTTP et validé avant usage ; ce n’est pas un type de requête natif de Symfony. Routage, enregistrement des services, autorisations, conversion des erreurs et transactions sont omis. Le service applicatif doit toujours faire respecter ses règles métier.
Dans Laravel, un Form Request peut valider les entrées et autoriser la requête avant l’appel au service. Dans les deux frameworks, une interface doit répondre à un vrai contrat ou besoin de remplacement. Une refonte architecturale complète ne fait pas automatiquement partie du travail de compatibilité.
Décider du sort des paquets incompatibles
Une dépendance maintenue peut seulement nécessiter une mise à jour. Un paquet inutilisé peut être retiré après recherche des usages indirects. Un paquet abandonné peut demander un remplacement, une intégration difficile une isolation temporaire.
Pour générer des documents, un port appartenant au projet peut limiter les répercussions du remplacement sur les appelants :
interface DocumentGeneratorInterface
{
public function generate(DocumentData $data): GeneratedDocument;
}DocumentData et GeneratedDocument sont des types du projet. Les adaptateurs ancien et nouveau implémenteraient ce port ; leurs documents doivent être comparés selon le contenu et le format requis. L’interface ne rend pas une bibliothèque incompatible exécutable sur un PHP récent, ni une bibliothèque dangereuse sûre. Un ancien composant hébergé séparément exige lui aussi un plan explicite de sécurisation et de retrait.
Détecter les régressions de performance liées à la mise à niveau
Vérifiez séparément compatibilité et optimisation lorsque c’est possible, même si elles appartiennent au même projet. Une régression due à un ORM ou un pilote peut exiger une correction immédiate. Une refonte indépendante des requêtes ou du cache peut généralement attendre.
Une requête GET /api/en/orders renvoyant 25 enregistrements pourrait présenter ce profil. Ce sont des chiffres pédagogiques inventés, pas des mesures GiSoft :
Réponse totale 780 ms Requêtes SQL 54
Base de données 180 ms Pic mémoire 110 Mo
Hydratation 120 ms Taille de réponse 1.4 Mo
Sérialisation 210 ms HTTP externe 190 msLes composantes ne couvrent pas nécessairement toute la durée et peuvent se chevaucher selon l’instrumentation. Comparez des données, réponses, états de cache et niveaux de concurrence représentatifs dans des environnements équivalents. Suivez les percentiles de latence, erreurs, temps en base, mémoire et attente en file, sans journaliser de contenus sensibles.
Un contrat de lecture stable aide à vérifier si une mise à niveau de l’ORM modifie les types, le chargement ou le nombre de requêtes :
final readonly class OrderListItem
{
public function __construct(
public int $id,
public string $number,
public string $customerName,
public string $status,
public \DateTimeImmutable $createdAt,
) {
}
}interface OrderReadRepositoryInterface
{
/**
* @return list<OrderListItem>
*/
public function findPage(
int $page,
int $limit,
): array;
}Le DTO est une possibilité de modèle de lecture, pas un remplacement obligatoire des entités. Son mapper doit fournir les types déclarés, dont DateTimeImmutable. Le dépôt doit définir les tailles de page admises, un ordre stable et les limites d’accès. Un DTO seul ne prévient pas les requêtes N+1 et ne garantit pas une réponse plus rapide.
Un test existant de nombre de requêtes peut révéler un changement de chargement :
it(
'borne les requêtes de la page de commandes',
function (): void {
$this->createOrders(count: 100);
$collector = $this->startApplicationQueryCollection();
$items = $this->orderQuery()->findPage(
page: 1,
limit: 50,
);
expect($items)->toHaveCount(50)
->and($collector->queryCount())->toBeLessThanOrEqual(4);
},
);Les trois méthodes auxiliaires appartiennent au projet d’exemple, pas à Pest ou Doctrine. Préparez les données, l’amorçage et l’authentification éventuelle avant le comptage. Contrôlez l’état de l’EntityManager ou des modèles et du cache, pour que des relations déjà chargées ne masquent pas des requêtes. Arrêtez ou réinitialisez le collecteur après la mesure. La limite de quatre requêtes est illustrative et n’évalue pas leur coût.
Un test de taille de réponse protège une autre propriété :
it(
'borne la taille de la réponse publique',
function (): void {
$client = static::createClient();
$client->request(
'GET',
'/api/en/orders?page=1&limit=25',
);
self::assertResponseIsSuccessful();
$content = (string) $client->getResponse()->getContent();
expect(strlen($content))->toBeLessThan(300_000);
},
);Il suppose Pest configuré avec Symfony WebTestCase, des données représentatives et l’authentification requise. strlen() compte les octets du corps de réponse dans le test, pas nécessairement les octets compressés transmis sur le réseau. La limite est illustrative ; une petite réponse peut toujours être fausse. Conservez donc les assertions du contrat de réponse. Préférez des mesures contrôlées de performance aux assertions de durée exacte dans une CI partagée ordinaire.
Traiter le cache et les messages comme des contrats de déploiement
Une mise à niveau peut modifier les données sérialisées, les adaptateurs de cache ou les extensions PHP. Vérifiez que les anciens et nouveaux lecteurs comprennent les données partagées, ou versionnez le format et prévoyez expiration ou invalidation. Les entités gérées par Doctrine et les modèles Eloquent ont des cycles de vie différents, mais aucun ne constitue par défaut un format de cache portable. Même les DTO demandent un contrat stable de sérialisation.
Ces clés ne sont que des esquisses :
settings.public.{locale}
menu.header.{locale}
orders.summary.{customerId}.{month}Incluez le contexte d’organisation, de droits et de version des données lorsqu’il affecte le résultat. Testez l’invalidation après validation des transactions et les lectures concurrentes. Redis, Memcached et le stockage sur fichiers ne sont pas des solutions de repli automatiquement interchangeables. Changer de moteur de cache n’est pas requis pour mettre PHP à niveau.
Prévoyez le redémarrage des workers ou le traitement des messages restants, la compatibilité des anciens messages, des tentatives limitées et la gestion des échecs. La mise à niveau ne justifie pas à elle seule de déplacer davantage de travail dans des files. Si l’application utilise déjà une outbox, les données métier et l’entrée d’outbox doivent être validées dans la même transaction de base pour un enregistrement atomique. Publication et consommation peuvent encore se répéter ; les actions ayant des conséquences demandent une gestion des doublons.
Évitez d’associer sans précaution des changements destructeurs de schéma au changement de PHP. Si une transition est nécessaire, ajouts compatibles, reprise des données, validation et retrait ultérieur peuvent préserver des options, mais les écritures pendant cette période doivent être gérées. La double écriture n’est pas obligatoire et introduit ses propres échecs possibles. Revenir à l’ancien code ne restaure pas des données supprimées et n’annule pas des actions externes.
Construire et déployer la combinaison vérifiée
L’exemple CI suivant utilise Bash et les outils GNU. Il n’a pas été exécuté sur ce projet et suppose des dépendances de développement installées et compatibles ainsi que les répertoires de test indiqués :
set -euo pipefail
composer validate --strict
composer check-platform-reqs
find src tests -name '*.php' -print0 \
| xargs -0 -r -n 1 php -l
vendor/bin/phpstan analyse --no-progress
vendor/bin/pest tests/Unit
vendor/bin/pest tests/Integration
vendor/bin/pest tests/FunctionalUtilisez vendor/bin/phpunit si c’est le lanceur compatible déjà établi. Une exécution complète de vendor/bin/pest peut remplacer les trois appels par répertoire lors de la recette ; cumuler les deux n’est pas nécessaire par principe. Les contrôles de syntaxe n’exécutent pas le code et l’analyse statique ne prouve pas le comportement métier.
Préparez et vérifiez le fichier de verrouillage dans un environnement contrôlé. La production doit installer les dépendances testées, pas résoudre un nouvel ensemble. Contrôlez les dépendances de production avec composer check-platform-reqs --no-dev sur leur véritable environnement. Les outils de développement peuvent exiger une version plus récente que l’application.
Avant la mise en production, vérifiez HTTP, CLI, tâches planifiées, workers et intégrations dans des conditions représentatives. Préparez les sauvegardes et testez leur restauration. Un déploiement par client ou pourcentage de trafic n’est utile que si le routage et les données partagées le permettent ; sinon, retenez une bascule testée ou une fenêtre de maintenance.
Adaptez le préchauffage du cache applicatif et le renouvellement des processus PHP ou de l’OPcache au modèle de déploiement. Une réinitialisation de l’OPcache en CLI ne renouvelle pas le cache d’un processus PHP-FPM distinct. Surveillez erreurs, latence, tâches échouées et résultats métier critiques, avec des critères d’arrêt. La reprise doit tenir compte du schéma, des messages, des sessions et des nouvelles écritures, pas seulement de l’ancienne image.
Après la recette, retirez les couches temporaires devenues inutiles et consignez la combinaison de versions testée. La mise à niveau doit laisser un chemin connu de déploiement et de vérification pour le prochain changement, pas une liste plus longue d’exceptions inexpliquées.
Références techniques
Consultez ces sources pour les versions retenues ; les exigences évoluent indépendamment de cet article.
- Maintenance PHP et guides de migration : https://www.php.net/supported-versions.php
- Commandes Composer et contrôle de plateforme : https://getcomposer.org/doc/03-cli.md
- Exigences d’exécution de Composer : https://getcomposer.org/doc/00-intro.md
- Exigences d’installation de Pest : https://pestphp.com/docs/installation
- Installation et configuration de la cible PHPStan : https://phpstan.org/user-guide/getting-started et https://phpstan.org/config-reference
- Mises à niveau majeures de Symfony : https://symfony.com/doc/current/setup/upgrade_major.html
- Guides Laravel par version : https://laravel.com/docs
