Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Migrations : C'est une question d'hygiène !

Migrations : C'est une question d'hygiène !

Talk given at the AFUP Day 2026 - Bordeaux

Les migrations de nos outils de développement et d’infrastructure sont la hantise de nos business et de nos backlogs. Elles sont synonymes de tunnels de développement à rallonge, de risques incontrôlés et de "feature freeze" qui frustrent le business. On les repousse, on les redoute, jusqu'au jour où elles deviennent une urgence absolue...

Et si l'erreur n'était pas la migration elle-même, mais la façon dont nous l'abordons ? Dans ce talk, nous verrons comment transformer ces montagnes en une série de toutes petites collines. L'idée ? Ne pas attendre la date butoir et la pression de nos managers.

Dans ce talk, nous explorerons comment instaurer une "hygiène de vie" technique continue : des actions ciblées appliquées au quotidien, qui prépare le terrain sans paralyser la vélocité et les nouvelles features. Après tout, c’est comme pour le ménage, un peu tous les jours, c’est beaucoup plus agréable et simple à entretenir !

Avatar for Vincent Amstoutz

Vincent Amstoutz

May 22, 2026

More Decks by Vincent Amstoutz

Other Decks in Programming

Transcript

  1. ENGINEERING PHP, JS, Go, Rust, C DevOps & SRE PRODUCT

    Agile Management UX / UI Design ACCOMPAGNEMENT Conseil, Formation & Tierce Maintenance Applicative API, Web & Cloud experts
  2. { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true, "require":

    { "php": ">=8.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "7.0.*", "symfony/dotenv": "7.0.*", "symfony/flex": "^2", "symfony/framework-bundle": "7.0.*", "symfony/runtime": "7.0.*", "symfony/yaml": "7.0.*" }, "require-dev": { "symfony/maker-bundle": "^1.50" }, "autoload": { "psr-4": { "App\\": "src/" } }, "autoload-dev": { "psr-4": { "App\\Tests\\": "tests/" } } } @vinceAmstoutz composer.json
  3. composer.lock Listées avec précision 1 Versions fixes Fonctionne de pair

    avec le composer.json 2 Synchronisation @vinceAmstoutz
  4. composer.lock Listées avec précision 1 Versions fixes Fonctionne de pair

    avec le composer.json 2 Synchronisation Doit être commité ! 3 Référentiel du projet @vinceAmstoutz
  5. "Autant le faire le plus tard possible, on a autre

    chose de plus important à faire". Upgrade(s) ?
  6. Gros effort d'upgrade Failles de sécurité Projet legacy (moins attractif)

    Inconvénients Focus fonctionnalité Prévisibilité opérationnelle Avantages @vinceAmstoutz
  7. Gros effort d'upgrade Failles de sécurité Projet legacy (moins attractif)

    Ne bénéficie pas des améliorations récentes Inconvénients Focus fonctionnalité Prévisibilité opérationnelle Avantages @vinceAmstoutz
  8. Gros effort d'upgrade Failles de sécurité Projet legacy (moins attractif)

    Ne bénéficie pas des améliorations récentes Repose (souvent) sur 1 personne Inconvénients Focus fonctionnalité Prévisibilité opérationnelle Avantages @vinceAmstoutz
  9. Checking updates for 152 packages... symfony/framework-bundle v4.4.50 ! v6.4.0 symfony/security-bundle

    v4.4.45 ! v6.4.2 psr/log 1.1.4 ! 3.0.0 doctrine/orm 2.7.5 ! 3.0.1 doctrine/dbal 2.13.9 ! 4.0.1 twig/twig v2.15.3 ! v3.8.0 guzzlehttp/guzzle 6.5.8 ! 7.8.1 phpunit/phpunit 9.5.28 ! 11.0.3 phpstan/phpstan 0.12.100 ! 1.10.57 fakerphp/faker v1.14.1 ! v1.23.1 monolog/monolog 2.2.0 ~ 2.9.2 symfony/console v4.4.49 ! v6.4.3 symfony/dotenv v4.4.45 ! v6.4.0 [... 134 more outdated packages ...] composer outdated @vinceAmstoutz Lister les dépendances plus à jour
  10. --- Executing composer audit... Found 7 security vulnerability advisories affecting

    3 packages: +-------------------+----------------------------+----------+ | Package | CVE | Severity | +-------------------+----------------------------+----------+ | symfony/console | CVE-2022-24894, 2023-46737 | High | | guzzlehttp/guzzle | CVE-2022-31090 | High | | doctrine/dbal | CVE-2021-43139 | Low | +-------------------+----------------------------+----------+ composer audit @vinceAmstoutz Lister les CVE
  11. Tâches de dev T3 Montée version T2 Montée version T1

    Résoudre le retard accumulé @vinceAmstoutz
  12. Tâches de dev T4 Tâches de dev T3 Montée version

    T2 Montée version T1 Résoudre le retard accumulé @vinceAmstoutz
  13. L'équipe ou le dev ne délivre plus de valeur. 1

    Feature freeze Problèmes que cela génère @vinceAmstoutz
  14. L'équipe ou le dev ne délivre plus de valeur. 1

    Feature freeze Plus de bugs d'un coup. 2 Risque acru Problèmes que cela génère @vinceAmstoutz
  15. L'équipe ou le dev ne délivre plus de valeur. 1

    Feature freeze Plus de bugs d'un coup. 2 Risque acru La montagne paraît insurmontable. 3 Démotivation Problèmes que cela génère @vinceAmstoutz
  16. CVE intégrées rapidemment Projet plus attractif ( - legacy) Bénéficie

    des améliorations Toute l'équipe est concernée Avantages @vinceAmstoutz
  17. Inconvénients CVE intégrées rapidemment Projet plus attractif ( - legacy)

    Bénéficie des améliorations Toute l'équipe est concernée Avantages @vinceAmstoutz
  18. S'abonner aux mises à jours Inconvénients CVE intégrées rapidemment Projet

    plus attractif ( - legacy) Bénéficie des améliorations Toute l'équipe est concernée Avantages @vinceAmstoutz
  19. S'abonner aux mises à jours Communiquer Inconvénients CVE intégrées rapidemment

    Projet plus attractif ( - legacy) Bénéficie des améliorations Toute l'équipe est concernée Avantages @vinceAmstoutz
  20. Tâches de dev J3 Tâches de dev J2 Upgrade J1

    @vinceAmstoutz Semaine type "en progressif"
  21. Tâches de dev et upgrade J4 Tâches de dev J3

    Tâches de dev J2 Upgrade J1 @vinceAmstoutz Semaine type "en progressif"
  22. Tâches de dev J5 Tâches de dev et upgrade J4

    Tâches de dev J3 Tâches de dev J2 Upgrade J1 @vinceAmstoutz Semaine type "en progressif"
  23. @vinceAmstoutz version: 2 updates: - package-ecosystem: "composer" directory: "/" schedule:

    interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" Dependabot
  24. @vinceAmstoutz composer require --dev phpstan/phpstan Levels de 0 à 9

    # phpstan.dist.neon parameters: level: max paths: - src/ - tests/ 1 2 3 4 5 6 7 8 Analyse statique
  25. @vinceAmstoutz vendor/bin/phpstan analyse --level 9 \ --configuration phpstan.neon \ src/

    tests/ --generate-baseline # phpstan-baseline.neon parameters: ignoreErrors: - message: "#^Only numeric types are ..." count: 1 path: src/Analyser/Scope.php - message: "#^Anonymous function has an ..." count: 2 path: src/Command/CommandHelper.php 1 2 3 4 5 6 7 8 9 10 11 Analyse statique
  26. @vinceAmstoutz vendor/bin/phpstan analyse --level 9 \ --configuration phpstan.neon \ src/

    tests/ --generate-baseline # phpstan.dist.neon includes: - phpstan-baseline.neon parameters: level: max paths: - src/ - tests/ 1 2 3 4 5 6 7 8 9 # phpstan-baseline.neon parameters: ignoreErrors: - message: "#^Only numeric types are ..." count: 1 path: src/Analyser/Scope.php - message: "#^Anonymous function has an ..." count: 2 path: src/Command/CommandHelper.php 1 2 3 4 5 6 7 8 9 10 11 Analyse statique
  27. @vinceAmstoutz // rector.php config file <?php use Rector\Config\RectorConfig; use Rector\ValueObject\PhpVersion;

    return RectorConfig::configure() ->withPhpVersion(PhpVersion::PHP_85); 1 2 3 4 5 6 7 8 9 vendor/bin/rector Rector
  28. @vinceAmstoutz class ProductService { /** * @var string */ private

    $name; /** * @param string|null $description */ public function update($description) { $this->name = $description ?? "Unknown"; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 Avant Rector
  29. @vinceAmstoutz class ProductService { /** * @var string */ private

    $name; /** * @param string|null $description */ public function update($description) { $this->name = $description ?? "Unknown"; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 Avant Après vendor/bin/rector Rector
  30. @vinceAmstoutz class ProductService { /** * @var string */ private

    $name; /** * @param string|null $description */ public function update($description) { $this->name = $description ?? "Unknown"; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 class ProductService { public function __construct( private string $name = "Unknown" ) {} public function update(?string $description): void { $this->name = $description ?? "Unknown"; } } 1 2 3 4 5 6 7 8 9 Avant Après vendor/bin/rector Rector
  31. @vinceAmstoutz { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true,

    "require": { "php": ">=7.2.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "4.4.*", "symfony/dotenv": "4.4.*", "symfony/flex": "^1.17", "symfony/framework-bundle": "4.4.*", "symfony/yaml": "4.4.*", }, "require-dev": { "symfony/maker-bundle": "^1.11", }, "autoload": { "psr-4": { "App\\": "src/" } } } Découper
  32. @vinceAmstoutz { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true,

    "require": { "php": ">=7.2.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "4.4.*", "symfony/dotenv": "4.4.*", "symfony/flex": "^1.17", "symfony/framework-bundle": "4.4.*", "symfony/yaml": "4.4.*", }, "require-dev": { "symfony/maker-bundle": "^1.11", }, "autoload": { "psr-4": { "App\\": "src/" } } } 1. PHP 8.1 Découper
  33. @vinceAmstoutz { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true,

    "require": { "php": ">=7.2.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "4.4.*", "symfony/dotenv": "4.4.*", "symfony/flex": "^1.17", "symfony/framework-bundle": "4.4.*", "symfony/yaml": "4.4.*", }, "require-dev": { "symfony/maker-bundle": "^1.11", }, "autoload": { "psr-4": { "App\\": "src/" } } } 1. PHP 8.1 2. Symfony 5.4 Découper
  34. @vinceAmstoutz { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true,

    "require": { "php": ">=7.2.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "4.4.*", "symfony/dotenv": "4.4.*", "symfony/flex": "^1.17", "symfony/framework-bundle": "4.4.*", "symfony/yaml": "4.4.*", }, "require-dev": { "symfony/maker-bundle": "^1.11", }, "autoload": { "psr-4": { "App\\": "src/" } } } 1. PHP 8.1 3. PHP 8.2 2. Symfony 5.4 Découper
  35. @vinceAmstoutz { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true,

    "require": { "php": ">=7.2.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "4.4.*", "symfony/dotenv": "4.4.*", "symfony/flex": "^1.17", "symfony/framework-bundle": "4.4.*", "symfony/yaml": "4.4.*", }, "require-dev": { "symfony/maker-bundle": "^1.11", }, "autoload": { "psr-4": { "App\\": "src/" } } } 1. PHP 8.1 3. PHP 8.2 4. Annotations → Attributs 2. Symfony 5.4 Découper
  36. @vinceAmstoutz { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true,

    "require": { "php": ">=7.2.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "4.4.*", "symfony/dotenv": "4.4.*", "symfony/flex": "^1.17", "symfony/framework-bundle": "4.4.*", "symfony/yaml": "4.4.*", }, "require-dev": { "symfony/maker-bundle": "^1.11", }, "autoload": { "psr-4": { "App\\": "src/" } } } 1. PHP 8.1 3. PHP 8.2 4. Annotations → Attributs 5. Symfony 6.4 2. Symfony 5.4 Découper
  37. @vinceAmstoutz { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true,

    "require": { "php": ">=7.2.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "4.4.*", "symfony/dotenv": "4.4.*", "symfony/flex": "^1.17", "symfony/framework-bundle": "4.4.*", "symfony/yaml": "4.4.*", }, "require-dev": { "symfony/maker-bundle": "^1.11", }, "autoload": { "psr-4": { "App\\": "src/" } } } 1. PHP 8.1 3. PHP 8.2 4. Annotations → Attributs 5. Symfony 6.4 6. PHP 8.5 2. Symfony 5.4 Découper
  38. @vinceAmstoutz { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true,

    "require": { "php": ">=7.2.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "4.4.*", "symfony/dotenv": "4.4.*", "symfony/flex": "^1.17", "symfony/framework-bundle": "4.4.*", "symfony/yaml": "4.4.*", }, "require-dev": { "symfony/maker-bundle": "^1.11", }, "autoload": { "psr-4": { "App\\": "src/" } } } 1. PHP 8.1 3. PHP 8.2 4. Annotations → Attributs 5. Symfony 6.4 6. PHP 8.5 7. Symfony 7.4 2. Symfony 5.4 Découper
  39. @vinceAmstoutz { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true,

    "require": { "php": ">=7.2.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "4.4.*", "symfony/dotenv": "4.4.*", "symfony/flex": "^1.17", "symfony/framework-bundle": "4.4.*", "symfony/yaml": "4.4.*", }, "require-dev": { "symfony/maker-bundle": "^1.11", }, "autoload": { "psr-4": { "App\\": "src/" } } } 1. PHP 8.1 3. PHP 8.2 4. Annotations → Attributs 5. Symfony 6.4 6. PHP 8.5 7. Symfony 7.4 8. Symfony 8.0 2. Symfony 5.4 Découper
  40. @vinceAmstoutz return RectorConfig::configure() ->withPhpVersion(PhpVersion::PHP_81) ); vendor/bin/rector // Avant class User

    { private string $name; public function __construct(string $name) { $this->name = $name; } } // Après class User { public function __construct(private readonly string $name) {} } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1. PHP 8.1
  41. @vinceAmstoutz return RectorConfig::configure() ->withSets([ SymfonySetList::SYMFONY_54, SymfonySetList::SYMFONY_CODE_QUALITY ]) ; vendor/bin/rector //

    Avant (Code style Symfony 3/4) class LegacyService { // ... public function execute() { $event = new MyCustomEvent(); // Ancienne signature (supprimée depuis Symfony 5.0) $this->dispatcher->dispatch('my.event', $event); } } // Après class LegacyService { // ... public function execute() { $event = new MyCustomEvent(); // Nouvelle signature imposée par le set 5.4 $this->dispatcher->dispatch($event, 'my.event'); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 2. Symfony 5.4
  42. @vinceAmstoutz return RectorConfig::configure() ->withPhpVersion(PhpVersion::PHP_82) ); vendor/bin/rector // Avant class UserDTO

    { public readonly string $name; public readonly string $email; public function __construct(string $name, string $email) { $this->name = $name; $this->email = $email; } } // Après readonly class UserDTO { public function __construct( public string $name, public string $email ) {} } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 3. PHP 8.2
  43. @vinceAmstoutz return RectorConfig::configure() ->withAttributesSets(symfony: true, doctrine: true) ); vendor/bin/rector //

    Avant /** * @ORM\Entity */ class User { // ... } // Après #[ORM\Entity] class User { // ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 4. Attributs PHP
  44. @vinceAmstoutz // Avant namespace App\Controller; use Symfony\Component\Security\Core\Security; // Ancien class

    PostController extends AbstractController { #[Route('/post/new', name: 'post_new')] public function new(Request $request, Security $security) { $user = $security->getUser(); return $this->render('post/new.html.twig'); } } // Après use Symfony\Bundle\SecurityBundle\Security; // Nouveau class PostController extends AbstractController { #[Route('/post/new', name: 'post_new')] public function new(Request $request, Security $security): Response { $user = $security->getUser(); return $this->render('post/new.html.twig'); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 return RectorConfig::configure() ->withSets([ SymfonySetList::SYMFONY_64, ]); vendor/bin/rector 5. Symfony 6.4
  45. @vinceAmstoutz return RectorConfig::configure() ->withPhpVersion(PhpVersion::PHP_85) ); vendor/bin/rector // Avant class Subscription

    { public const STATUS_ACTIVE = 'active'; /** @var int */ public const MAX_RETRIES = 3; } // Après class Subscription { public const string STATUS_ACTIVE = 'active'; public const int MAX_RETRIES = 3; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 6. PHP 8.5 (inclus 8.3 et 8.4)
  46. @vinceAmstoutz // Avant class ContactController extends AbstractController { public function

    index(Request $request) { $name = $request->get('name', 'Guest'); return $this->render('contact.html.twig'); } } // Après class ContactController extends AbstractController { public function index(Request $request) { $name = $request->query->get('name', 'Guest'); return $this->render('contact.html.twig'); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 return RectorConfig::configure() ->withSets([ SymfonySetList::SYMFONY_74, ]); vendor/bin/rector 7. Symfony 7.4
  47. @vinceAmstoutz // Avant use Symfony\Component\Security\Core\User\UserInterface; final class User implements UserInterface

    { private string $password; public function eraseCredentials(): void { } } // Après (Symfony 8.0 via Rector) use Symfony\Component\Security\Core\User\UserInterface; final class User implements UserInterface { private string $password; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 return RectorConfig::configure() ->withSets([ SymfonySetList::SYMFONY_80, ]); vendor/bin/rector 8. Symfony 8.0
  48. { "type": "project", "license": "proprietary", "minimum-stability": "stable", "prefer-stable": true, "require":

    { "php": ">=8.5", "ext-ctype": "*", "ext-iconv": "*", "symfony/console": "8.0.*", "symfony/dotenv": "8.0.*", "symfony/flex": "^2.10", "symfony/framework-bundle": "8.0.*", "symfony/runtime": "8.0.*", "symfony/yaml": "8.0.*" }, "require-dev": { "symfony/maker-bundle": "^1.60", }, "autoload": { "psr-4": { "App\\": "src/" } } } @vinceAmstoutz À jour ! 🎉
  49. return RectorConfig::configure() 1 ->withPreparedSets( 2 deadCode: true, 3 codeQuality: true,

    4 phpunitCodeQuality: true, 5 doctrineCodeQuality: true, 6 symfonyCodeQuality: true, 7 ) 8 ->withComposerBased( 9 twig: true, 10 doctrine: true, 11 phpunit: true, 12 symfony: true 13 ); 14 @vinceAmstoutz Depuis Rector 2.0
  50. return RectorConfig::configure() 1 ->withPreparedSets( 2 deadCode: true, 3 codeQuality: true,

    4 phpunitCodeQuality: true, 5 doctrineCodeQuality: true, 6 symfonyCodeQuality: true, 7 ) 8 ->withComposerBased( 9 twig: true, 10 doctrine: true, 11 phpunit: true, 12 symfony: true 13 ); 14 ->withComposerBased( twig: true, doctrine: true, phpunit: true, symfony: true ); return RectorConfig::configure() 1 ->withPreparedSets( 2 deadCode: true, 3 codeQuality: true, 4 phpunitCodeQuality: true, 5 doctrineCodeQuality: true, 6 symfonyCodeQuality: true, 7 ) 8 9 10 11 12 13 14 @vinceAmstoutz Depuis Rector 2.0
  51. return RectorConfig::configure() 1 ->withPreparedSets( 2 deadCode: true, 3 codeQuality: true,

    4 phpunitCodeQuality: true, 5 doctrineCodeQuality: true, 6 symfonyCodeQuality: true, 7 ) 8 ->withComposerBased( 9 twig: true, 10 doctrine: true, 11 phpunit: true, 12 symfony: true 13 ); 14 ->withComposerBased( twig: true, doctrine: true, phpunit: true, symfony: true ); return RectorConfig::configure() 1 ->withPreparedSets( 2 deadCode: true, 3 codeQuality: true, 4 phpunitCodeQuality: true, 5 doctrineCodeQuality: true, 6 symfonyCodeQuality: true, 7 ) 8 9 10 11 12 13 14 ->withPreparedSets( deadCode: true, codeQuality: true, phpunitCodeQuality: true, doctrineCodeQuality: true, symfonyCodeQuality: true, ) return RectorConfig::configure() 1 2 3 4 5 6 7 8 ->withComposerBased( 9 twig: true, 10 doctrine: true, 11 phpunit: true, 12 symfony: true 13 ); 14 @vinceAmstoutz Depuis Rector 2.0
  52. Ajouter composer audit à vos CI Bloquer dans l'agenda de

    la veille Supprimer les packages inutiles @vinceAmstoutz Bonus