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

Modernisation Progressive d’Applications PHP

Hugo Hamon
September 19, 2024

Modernisation Progressive d’Applications PHP

Hugo Hamon

September 19, 2024
Tweet

More Decks by Hugo Hamon

Other Decks in Programming

Transcript

  1. @hhamon / [email protected] Hugo HAMON Consultant PHP / Symfony @

    KODERO Symfony Certified Expert Developer ex-SensioLabs
  2. Qu’est-ce que le “code legacy” ? Celui dont vous n’êtes

    pas à l’origine ! Celui que vous venez tout juste d’écrire " Celui qui n’est pas testé # Celui qui fonctionne sur des technologies plus supportées $ Celui qui fonctionne en production mais qu’on a peur de changer %
  3. Raisons pour opérer un changement Ajouter une nouvelle fonctionnalité Corriger

    un bug persistant et impactant Améliorer / simplifier le “design” général Optimiser les performances Pérenniser l’application dans le temps Remplacer une brique par une plus moderne Faire des montées en version de l’infrastructure … https://unsplash.com/@clemono
  4. Laisser le code dans un meilleur état que celui dans

    lequel vous l’avez trouvé Quels gains pour la tech ? Faciliter les futurs changements du code Réduire le risque de bugs Se concentrer davantage sur l’apport de valeur pour le métier https://unsplash.com/@orrbarone
  5. Quelques types de changement Changement purement syntaxique (styles) Correction d’un

    bug Extraction de code dupliqué Remplacement d’une dépendance plus supportée Intégration d’une nouvelle fonctionnalité Abstraction de certains composants Ajout de tests unitaires ou fonctionnels Changement au niveau de l’infrastructure
  6. Points de vigilance ! Impacts sur des services tiers https://unsplash.com/@vinic_

    Impacts sur le modèle de données Changements entraînant une cassure de compatibilité pour les autres Impacts sur le business
  7. Arbitrages Niveau d’effort à mettre en œuvre ? Quels impacts

    pour le business ? Quels impacts pour la tech ? Quel investissement vs économie réalisée ? https://unsplash.com/@imchenyf
  8. Continuer à délivrer de la valeur https://unsplash.com/@nathan_cima Définir une roadmap

    avec les équipes métier Planifier un plan d’action longtemps à l’avance Communiquer avec toutes les parties concernées Avoir un environnement de préproduction Utiliser des feature flags pour beta tester S’outiller un maximum
  9. “Code without tests is bad code. It doesn’t matter how

    well-written it is; it doesn’t matter how pretty or object-oriented or well-encapsulated it is.” “With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don’t know if our code is getting better or worse.” — Michael C. Feathers https://www.pmi.org/learning/library/legacy-it-systems-upgrades-11443
  10. Tests Unitaires “ En programmation informatique, le test unitaire est

    une procédure permettant de vérifier le bon fonctionnement d'une partie précise d'un logiciel ou d'une portion d'un programme (appelée « unité » ou « module »). ” — Wikipedia https://unsplash.com/@alexcioaba
  11. Fast Rapidité d’exécution de la suite de tests L’ordre d’exécution

    n’a pas d’importance Indépendance vis-à-vis de l’environnement Répétition à l’infini dans les mêmes conditions Résultat binaire : réussite ou échec Couverture du “happy path” à minima Couverture des cas d’exception / d’erreur Couverture des cas à la marge … Isolated Repeatable Self-Validating Thorough
  12. Model Class class Loan { // ... getter + setter

    methods public function getInstallmentsValue(): float { $borrowedValue = $this->getBorrowedValue(); $installments = $this->getTotalInstallments(); $fee = $this->getMonthlyFee(); return ($borrowedValue / $installments) * (1 + ($fee / 100)); } }
  13. PHPUnit final class LoanTest extends TestCase { public function testGetLoanInstallmentValue():

    void { $customer = new Customer(); $loan = new Loan(); $loan->setCustomer($customer); $loan->setBorrowedValue(10_000); $loan->setTotalInstallments(60); $loan->setMonthlyFee(3.25); self::assertSame($customer, $loan->getCustomer()); self::assertSame(10_000, $loan->getBorrowedValue()); self::assertSame(3.25, $loan->getMonthlyFee()); self::assertSame(60, $loan->getTotalInstallments()); // Expecting a decimal amount with cents self::assertSame(172.08, $loan->getInstallmentsValue()); } }
  14. Model Class class Loan { // ... public function getInstallmentsValue():

    float { $borrowedValue = $this->getBorrowedValue(); $installments = $this->getTotalInstallments(); $ratio = 1 + $this->getMonthlyFee() * 0.01; $amount = ($borrowedValue / $installments) * $ratio; return round($amount, 2, PHP_ROUND_HALF_UP); } }
  15. Tests “End-to-End” “ Les tests de bout-en-bout permettent de valider

    le système dans son ensemble en prenant en compte toutes les couches applicatives. ” https://unsplash.com/@timmossholder
  16. Avantages Idéal pour tester le comportement extérieur Adapté pour le

    “smoke testing” Exécution de toutes les couches applicatives Validation de l’UI et des interactions Inconvénients Complexité de la mise en place Lenteur d’exécution (réseau, IO, etc.) Plus contraignant à écrire également
  17. Panther use Symfony\Component\Panther\Client; require __DIR__.'/vendor/autoload.php'; $client = Client::createChromeClient(); // Interact

    with the UI elements $client->request('GET', 'https://api-platform.com'); $client->clickLink('Getting started'); // Wait for an element to be present in the DOM (even if hidden) $crawler = $client->waitFor('#installing-the-framework'); // Alternatively, wait for an element to be visible $crawler = $client->waitForVisibility('#installing-the-framework'); echo $crawler->filter('#installing-the-framework')->text(); // Yeah, screenshot! $client->takeScreenshot('screen.png');
  18. Behat + Gherkin Feature: In order to have a localized

    experience,
 users must be able to switch between supported languages. Scenario: A user is able to switch to English language. Given a guest user visits the French speaking website When they switch language to "english" Then they see "Create your account" as page title Scenario: A user is able to switch to French language. Given a guest user visits the English speaking website When they switch language to "français" Then they see "Créez votre compte" as page title
  19. Behat + Gherkin final class SwitchLanguageContext implements Context { private

    Client $client; private Crawler $crawler; public function __construct() { $this->client = Client::createChromeClient(); } #[Given('a guest user visits the English speaking website')] public function guestUserVisitsTheEnglishSpeakingWebsite(): void { $this->crawler = $this->client->request('GET', 'https://test.website.com'); } #[When('they switch language to :language')] public function theySwitchLanguageTo(string $language): void { $this->crawler = $this->client->clickLink($language); } #[Then('they see :title as page title')] public function theySeeAsPageTitle(string $title): void { if ($title !== $this->crawler->filter('title')->text()) { throw new \Exception('Title mismatch'); } } }
  20. “ La gestion des dépendances, quel enfer… ” “ Mais

    c’est quoi tous ces include / require ?! ” https://unsplash.com/@nate_dumlao “ Si seulement je pouvais exécuter tous ces scripts en une seule commande… ”
  21. Composer { "type": "project", "license": "proprietary", "autoload": { "psr-4": {

    "App\\": "src/" } }, "autoload-dev": { "psr-4": { "App\\Tests\\": "tests/" } } }
  22. Composer { "require": { "php": ">=7.2", "ext-iconv": "*", "ext-imap": "*",

    "ext-mbstring": "*", "ext-pdo": "*", "ext-xsl": "*", ... } } $ php composer require ext-iconv ext-imap ext-mbstring …
  23. Composer { "scripts": { "app:install-dev": [ "@composer database:reset-dev", "@composer frontend:build-dev"

    ], "database:reset-dev": [ "@php bin/console d:d:d --if-exists --force -n -e dev", "@php bin/console d:d:c --if-not-exists -n -e dev", "@php bin/console d:m:m --all-or-nothing -n -e dev", "@php bin/console d:f:l --purge-with-truncate -n -e dev" ], "frontend:build-dev": [ "@php bin/console tailwind:build -e dev", "@php bin/console sass:build -e dev", "@php bin/console asset-map:compile -e dev" ] } } $ php composer app:install-dev
  24. “ WOW ! Ces nouvelles fonctions ajoutées à PHP 8

    nous simplifieraient tellement le code ! ” https://unsplash.com/@1nimidiffa_ “ Quel dommage que nous soyons encore en PHP 7 en prod… ”
  25. Polyfills symfony/polyfill-php54 symfony/polyfill-php55 symfony/polyfill-php56 symfony/polyfill-php70 symfony/polyfill-php71 symfony/polyfill-php72 symfony/polyfill-php73 symfony/polyfill-php74 symfony/polyfill-php80

    symfony/polyfill-php81 symfony/polyfill-php82 symfony/polyfill-php83 symfony/polyfill-php84 symfony/polyfill-apcu symfony/polyfill-ctype symfony/polyfill-iconv symfony/polyfill-intl-grapheme symfony/polyfill-intl-idn symfony/polyfill-intl-icu symfony/polyfill-intl-messageformatter symfony/polyfill-mbstring symfony/polyfill-util symfony/polyfill-uuid
  26. Symfony Polyfills $text = <<<TEXT PHP is a popular general-purpose

    scripting language that is especially suited to web development. Fast, flexible and pragmatic, PHP powers everything from your blog & to the most popular websites in the world. TEXT; if (strncmp($text, 'PHP is a popular', strlen('PHP is a popular')) === 0) { echo 'Text starts with "PHP is a popular scripting language" string.'; } if (strpos($text, 'from your blog & to the most') !== false) { echo 'Text contains "from your blog & to the most" string.'; } $needle = 'in the world.'; $needleLength = strlen($needle); if ($needleLength <= strlen($text) && 0 === substr_compare($text, $needle, -$needleLength)) { echo 'Text ends with "in the world." string.'; }
  27. Symfony Polyfills { "require": { "php": ">=7.1", "symfony/polyfill-php72": "^1.31.0", "symfony/polyfill-php73":

    "^1.31.0", "symfony/polyfill-php74": "^1.31.0", "symfony/polyfill-php80": "^1.31.0", "symfony/polyfill-php81": "^1.31.0", "symfony/polyfill-php82": "^1.31.0", "symfony/polyfill-php83": "^1.31.0", "symfony/polyfill-php84": "^1.31.0" } }
  28. Symfony Polyfills if (str_starts_with($text, 'PHP is a popular')) { echo

    'Text starts with "PHP is a popular scripting language"…'; } if (str_contains($text, 'from your blog & to the most')) { echo 'Text contains "from your blog & to the most" string.'; } if (str_ends_with($text, 'in the world.')) { echo 'Text ends with "in the world." string.'; } $ php71 polyfill-php80.php Text starts with "PHP is a popular scripting language" string. Text contains "from your blog & to the most" string. Text ends with "in the world." string.
  29. “ Quelle pagaille ! Il n’y a aucune cohérence d’un

    fichier à l’autre ! ” https://unsplash.com/@xavi_cabrera “ Harmonisons le style de codage une bonne fois pour toutes ”
  30. Configuration (1/3) return ECSConfig::configure() ->withPaths([ __DIR__ . '/config', __DIR__ .

    '/src', __DIR__ . '/tests', ]) ->withSkip([ __DIR__ . '/config/secrets/', ArrayOpenerAndCloserNewlineFixer::class, MethodChainingNewlineFixer::class, ]) ; ecs.php
  31. return ECSConfig::configure() ->withPreparedSets( psr12: true, symplify: true, arrays: true, comments:

    true, docblocks: true, spaces: true, namespaces: true, controlStructures: true, phpunit: true, strict: true, cleanCode: true, ) ; ecs.php Configuration (2/3)
  32. return ECSConfig::configure() ->withRules([ NoUnusedImportsFixer::class, ]) ->withSkip([ DeclareStrictTypesFixer::class, ]) ->withConfiguredRule(GlobalNamespaceImportFixer::class, [

    'import_classes' => true, ]) ->withConfiguredRule(MethodArgumentSpaceFixer::class,[ 'on_multiline' => 'ensure_fully_multiline', 'attribute_placement' => 'same_line', ]) ; ecs.php Configuration (3/3)
  33. “ J’aimerais bien améliorer mon code legacy mais par où

    je commence ? ” https://unsplash.com/@brookecagle “ Si seulement je pouvais avoir une liste de problèmes à corriger… ”
  34. PHP Stan { ... "require-dev": { ... "phpstan/phpstan": "^1.12.3", "phpstan/extension-installer":

    "^1.4.3", "phpstan/phpstan-deprecation-rules": "^1.2.1", "phpstan/phpstan-doctrine": "^1.5.3", "phpstan/phpstan-phpunit": "^1.4.0", "phpstan/phpstan-strict-rules": "^1.6.0", "phpstan/phpstan-symfony": "^1.4.9" }, "config": { "allow-plugins": { "phpstan/extension-installer": true } } }
  35. PHP Stan includes: - phpstan-baseline.neon parameters: level: max paths: -

    bin/ - config/ - public/ - src/ excludePaths: - src/Migrations/ checkAlwaysTrueInstanceof: true checkAlwaysTrueStrictComparison: true checkExplicitMixedMissingReturn: true reportWrongPhpDocTypeInVarTag: true treatPhpDocTypesAsCertain: false phpstan.neon
  36. PHP Stan parameters: ignoreErrors: - message: "#^Method App\\\\Controller\\\\Authenticator\\:\\:login\\(\\) has no

    return type specified\\.$#" count: 1 path: src/Controller/Authenticator.php - message: "#^Method App\\\\Controller\\\\Authenticator\\:\\:logout\\(\\) has no return type specified\\.$#" count: 1 path: src/Controller/Authenticator.php - message: "#^Method App\\\\Controller\\\\Customer\\:\\:edit\\(\\) has no return type specified\\.$#" count: 1 path: src/Controller/Customer.php - message: "#^Method App\\\\Controller\\\\Customer\\:\\:edit\\(\\) has parameter \\$id with no type specified\\.$#" count: 1 path: src/Controller/Customer.php - message: "#^Method App\\\\Controller\\\\Customer\\:\\:handleCreationFormSubmission\\(\\) has no return type specified\\.$#" count: 1 path: src/Controller/Customer.php - message: "#^Method App\\\\Controller\\\\Customer\\:\\:handleCreationFormSubmission\\(\\) has parameter \\$form with ...” count: 1 path: src/Controller/Customer.php - message: "#^Method App\\\\Controller\\\\Customer\\:\\:handleEditFormSubmission\\(\\) has no return type specified\\.$#" count: 1 path: src/Controller/Customer.php phpstan-baseline.neon
  37. “ Ça va prendre un temps fou pour tout fixer

    ! ” https://unsplash.com/@odissei “ Si seulement on pouvait automatiser certains changements… ”
  38. Rector Ajout des typages Indentation Annotations -> Attributs Retrait de

    code mort Constructor property promotion Syntaxe moderne (array, closures, etc.) “ Early returns ” Privatisation des attributs et méthodes Renommage de fonctions et méthodes Migration pour Symfony, Twig, PHPUnit, Laravel, etc. Extensibilité : création / import de règles tierces …
  39. Rector PHP declare(strict_types=1); use Rector\Config\RectorConfig; return RectorConfig::configure() ->withParallel() ->withPaths([ __DIR__

    . '/src', ]) ->withSkip([ __DIR__ . '/src/Migrations', ]) ->withAttributesSets(symfony: true, doctrine: true) ->withPhp74Sets() ->withPreparedSets(typeDeclarations: true); rector.php
  40. Refactoring Remaniement Organiser le code par métier / domaine Employer

    la nomenclature du métier Exposer des constructeurs sémantiques Favoriser l’usage des ENUM Favoriser l’inversion des dépendances Définir des interfaces claires Eviter les modèles anémiques Contrôler les paramètres d’entrée Intégrer des librairies éprouvées …
  41. Domaines src "## Controller $ "## Authenticator.php $ "## ...

    $ &## User.php "## Entity $ "## Customer.php $ "## Helper.php $ "## Installment.php $ "## InstallmentPeriod.php $ "## InstallmentStatus.php $ "## Loan.php $ "## Role.php $ &## User.php "## Repository $ "## CustomersRepository.php $ "## HelperRepository.php $ "## InstallmentPeriodsRepository.php $ "## InstallmentStatusRepository.php $ "## InstallmentsRepository.php $ "## LoansRepository.php $ "## RolesRepository.php $ &## UserRepository.php &## Service "## Calculator.php "## Customer.php "## Helper.php "## Installment.php "## Loan.php "## Profit.php &## User.php
  42. Une banque qui vend des services financiers de crédit à

    des clients ? Quels types de clients ? Quels types de crédits ? Une compagnie d’assurance qui vend des produits d’assurance de crédit à des clients souscripteurs ? Une société de rachat de crédit ? Une société de vente de crédits à la consommation à des clients particuliers ? Une application de suivi des états financiers d’un client particulier ou professionnel ? Une application qui permet de prêter de l’argent entre amis proches ou membres d’une même famille ? Quel est le métier de cette base de code ?
  43. Domaines src "## Application $ "## Controller $ $ "##

    Authenticator.php $ $ "## Customer.php $ $ "## CustomerHistoric.php $ $ "## Installment.php $ $ "## Loan.php $ $ "## Profile.php $ $ "## Profit.php $ $ &## User.php $ "## DataFixtures $ $ &## ORM $ $ &## AppFixtures.php $ "## Kernel.php $ &## Migrations $ "## Version20190103002150.php $ "## Version20190113221724.php $ "## Version20190124120652.php $ "## Version20190212012115.php $ &## Version20190212013033.php "## BankingServices "## FinancialReporting &## User
  44. Domaines src "## FinancialServices $ "## Calculator.php $ "## Customer.php

    $ "## Entity $ $ "## Customer.php $ $ "## Helper.php $ $ "## Installment.php $ $ "## InstallmentPeriod.php $ $ "## InstallmentStatus.php $ $ &## Loan.php $ "## Helper.php $ "## Installment.php $ "## Loan.php $ &## Repository $ "## CustomersRepository.php $ "## HelperRepository.php $ "## InstallmentPeriodsRepository.php $ "## InstallmentStatusRepository.php $ "## InstallmentsRepository.php $ &## LoansRepository.php "## FinancialReporting $ &## Profit.php &## User "## Entity $ "## Role.php $ &## User.php "## Repository $ "## RolesRepository.php $ &## UserRepository.php &## User.php
  45. Nommage // Financial Reporting class Profit { // ... public

    function findAll(): array { $installmentsPerMonth = []; foreach ($this->installmentRepository->findAllPaid() as $installment) { $month = date('n', $installment->getDueDate()->getTimestamp()); if (! isset($installmentsPerMonth[$month])) { $installmentsPerMonth[$month] = []; } $installmentsPerMonth[$month][] = $installment; } $formattedData = []; foreach ($installmentsPerMonth as $month => $installments) { $formattedData[] = [ 'month' => $month, 'year' => date('Y', $installments[0]->getDueDate()->getTimestamp()), 'profit' => $this->sumInstallments($installments), ]; } return $formattedData; } }
  46. Nommage class ComputeYearlyLoanInstallmentReporting { // ... /** * @return array{month:

    string, year: string, profit: float}[] */ public function __invoke(int $year): array { // ... } }
  47. Nommage class ComputeYearlyLoanInstallmentReporting { // ... /** * @return array{month:

    string, year: string, profit: float}[] */ public function __invoke(string $year): array { // ... } }
  48. Nommage final readonly class YearlyLoanInstallmentReporting { /** @var array<string, float>

    */ private array $revenues = [ '01' => 0, // ... '12' => 0, ]; public function __construct(private string $year) {} public function increaseMonthlyRevenue(string $month, float $amount): void { $this->revenues[$month] += $amount; } /** * @return array<int, array{month: string, year: string, profit: float}> */ public function toArray(): array { $results = []; foreach ($this->revenues as $month => $revenue) { $results[] = [ 'year' => $this->year, 'month' => $month, 'profit' => $revenue, ]; } return $results; } }
  49. Nommage class ComputeYearlyLoanInstallmentReporting { // ... /** * @return array<int,

    array{month: string, year: string, profit: float}> */ public function __invoke(int $year): array { $reporting = new YearlyLoanInstallmentReporting($year); foreach ($this->installmentRepository->findYearlyPaid($year) as $installment) { $reporting->increaseMonthlyRevenue( $installment->getDueDate()->format('m'), $installment->getValue(), // to be renamed to getAmount? ); } return $reporting->toArray(); } }
  50. Modèle Anémique // Customer has subscribed to a loan $loan

    = new Loan(...); $installmentAmount = $this->loanService->getInstallmentAmount($loan); // On loan settlement, all its installments are calculated $installment = (new Installment()) ->setLoan($loan) ->setDueDate(new DateTimeImmutable('2024-10-05')) ->setAmount($installmentAmount) ->setStatus(InstallmentStatus::DUE); // Each day, a cronjob runs to update the status of the due/overdue installments $receivedPaymentDate = ...; if ($receivedPaymentDate instanceof DateTimeImmutable) { $installment->setPaymentDate($receivedPaymentDate); $installment->setStatus(InstallmentStatus::PAID); } // If payment is still unpaid after due date, its marked overdue $today = new DateTimeImmutable('today'); if ($installment->getStatus() === InstallmentStatus::DUE && $today > $installment->getDueDate()) { $installment->setStatus(InstallmentStatus::OVERDUE); }
  51. Modèle Anémique // Customer has subscribed to a loan $loan

    = new Loan(...); $installmentAmount = $this->loanService->getInstallmentAmount($loan); // On loan settlement, all its installments are calculated $installment = (new Installment()) ->setLoan($loan) ->setDueDate(new DateTimeImmutable('2024-10-05')) ->setAmount($installmentAmount) ->setStatus(InstallmentStatus::DUE);
  52. Modèle Métier class Installment { public function __construct( private readonly

    Loan $loan, private readonly InstallmentStatus $status, private readonly DateTimeImmutable $dueDate, private readonly float $amount, ) { } }
  53. Modèle Métier class Installment { public static function due( Loan

    $loan, DateTimeImmutable $dueDate, float $amount ): self { return new self( loan: $loan, status: InstallmentStatus::DUE, dueDate: $dueDate, Amount: $amount, ); } }
  54. Modèle Métier class Installment { public static function due( Loan

    $loan, string $dueDate, float $amount, ): self { return new self( loan: $loan, status: InstallmentStatus::DUE, dueDate: new DateTimeImmutable($dueDate), amount: $amount, ); } }
  55. Modèle Métier class Installment { private function __construct( private readonly

    Loan $loan, private readonly InstallmentStatus $status, private readonly DateTimeImmutable $dueDate, private readonly float $amount, ) { } }
  56. Modèle Métier // Customer has subscribed to a loan $loan

    = new Loan(...); $amount = $this->loanService->getInstallmentAmount($loan); // On loan settlement, all its installments are calculated $installment = Installment::due($loan, '2024-10-05', $amount);
  57. Modèle Anémique // Customer has subscribed to a loan $loan

    = new Loan(...); $amount = $this->loanService->getInstallmentAmount($loan); // On loan settlement, all its installments are calculated $installment = Installment::due($loan, '2024-10-05', $amount); // Each day, a cronjob runs to update the status of the due/overdue installments $receivedPaymentDate = ...; if ($receivedPaymentDate instanceof DateTimeImmutable) { $installment->setPaymentDate($receivedPaymentDate); $installment->setStatus(InstallmentStatus::PAID); } // If payment is still unpaid after due date, its marked overdue $today = new DateTimeImmutable('today'); if ($installment->getStatus() === InstallmentStatus::DUE && $today > $installment->getDueDate()) { $installment->setStatus(InstallmentStatus::OVERDUE); }
  58. Modèle Anémique // Each day, a cronjob runs to update

    the status… $receivedPaymentDate = ...; if ($receivedPaymentDate instanceof DateTimeImmutable) { $installment->setPaymentDate($receivedPaymentDate); $installment->setStatus(InstallmentStatus::PAID); }
  59. Modèle Métier class Installment { // ... public function isPaid():

    bool { return $this->status === InstallmentStatus::PAID; } public function pay(DateTimeImmutable $paymentDate): void { if ($this->isPaid()) { throw new DomainException('Already paid!'); } $this->status = InstallmentStatus::PAID; $this->paymentDate = $paymentDate; } }
  60. Modèle Anémique // Customer has subscribed to a loan $loan

    = new Loan(...); $amount = $this->loanService->getInstallmentAmount($loan); // On loan settlement, all its installments are calculated $installment = Installment::due($loan, '2024-10-05', $amount); // Each day, a cronjob runs to update the status // of the due/overdue installments $receivedPaymentDate = new DateTimeImmutable('2024-10-03'); if ($receivedPaymentDate instanceof DateTimeImmutable) { $installment->pay($receivedPaymentDate); } // If payment is still unpaid after due date, its marked overdue $today = new DateTimeImmutable('today'); if ($installment->getStatus() === InstallmentStatus::DUE && $today > $installment->getDueDate()) { $installment->setStatus(InstallmentStatus::OVERDUE); }
  61. Modèle Métier // Each day, a cronjob runs to update

    the status // of the due/overdue installments $receivedPaymentDate = new DateTimeImmutable('2024-10-03'); try { if ($receivedPaymentDate instanceof DateTimeImmutable) { $installment->pay($receivedPaymentDate); } } catch (DomainException $e) { $this->logger->warning($e->getMessage()); }
  62. Modèle Anémique // Customer has subscribed to a loan $loan

    = new Loan(...); $amount = $this->loanService->getInstallmentAmount($loan); // On loan settlement, all its installments are calculated $installment = Installment::due($loan, '2024-10-05', $amount); // Each day, a cronjob runs to update the status // of the due/overdue installments $receivedPaymentDate = new DateTimeImmutable('2024-10-03'); if ($receivedPaymentDate instanceof DateTimeImmutable) { $installment->pay($receivedPaymentDate); } // If payment is still unpaid after due date, its marked overdue $today = new DateTimeImmutable('today'); if ($installment->getStatus() === InstallmentStatus::DUE && $today > $installment->getDueDate()) { $installment->setStatus(InstallmentStatus::OVERDUE); }
  63. Modèle Anémique // If payment is still unpaid after due

    date, its marked overdue $today = new DateTimeImmutable(‘today'); if ($installment->getStatus() === InstallmentStatus::DUE && $today > $installment->getDueDate()) { $installment->setStatus(InstallmentStatus::OVERDUE); }
  64. Modèle Métier class Installment { // ... public function isDue():

    bool { return $this->status === InstallmentStatus::DUE; } public function isOverdue(): bool { return $this->status === InstallmentStatus::OVERDUE; } public function overdue(): void { if (!$this->isDue()) { throw new DomainException('Installment must be due!'); } $this->status = InstallmentStatus::OVERDUE; } }
  65. Modèle Anémique // Customer has subscribed to a loan $loan

    = new Loan(...); $amount = $this->loanService->getInstallmentAmount($loan); // On loan settlement, all its installments are calculated $installment = Installment::due($loan, '2024-10-05', $amount); // Each day, a cronjob runs to update the status // of the due/overdue installments $receivedPaymentDate = new DateTimeImmutable('2024-10-03'); if ($receivedPaymentDate instanceof DateTimeImmutable) { $installment->pay($receivedPaymentDate); } // If payment is still unpaid after due date, // it is marked overdue $today = new DateTimeImmutable('today'); if ($installment->isDue() && $today > $installment->getDueDate()) { $installment->overdue(); }
  66. Modèle Métier // If payment is still unpaid after due

    date, // it is marked overdue $today = new DateTimeImmutable('today'); try { if ($installment->isDue() && $today > $installment->getDueDate()) { $installment->overdue(); } } catch (DomainException $e) { $this->logger->warning($e->getMessage()); }
  67. Modèle Métier class Installment { // ... public function wasPaidOnTime():

    bool { $this->ensureIsPaid(); return $this->getPaymentDate() <= $this->getDueDate(); } public function wasPaidLate(): bool { $this->ensureIsPaid(); return $this->getPaymentDate() > $this->getDueDate(); } private function ensureIsPaid(): void { if (!$this->isPaid()) { throw new DomainException('Installment not paid yet!'); } } }
  68. Enumérations enum InstallmentStatus: string { case DUE = 'due'; case

    OVERDUE = 'overdue'; case PAID = 'paid'; public function isPaid(): bool { return self::PAID->value === $this->value; } public function getLabel(): string { return match ($this) { self::DUE => 'Waiting for payment', self::OVERDUE => 'Payment delay exceeded', self::PAID => 'Payment received', }; } }
  69. Les modules de haut niveau ne doivent pas dépendre des

    modules de niveau inférieur. Les abstractions ne doivent pas dépendre des détails d’implémentation ; les implémentations dépendent des abstractions. Inversion des dépendances
  70. Inversion des dépendances // ... use App\FinancialServices\Repository\DoctrineInstallmentRepository; use Doctrine\ORM\EntityManager; class

    ComputeYearlyLoanInstallmentReporting { private DoctrineInstallmentRepository $installmentRepository; public function __construct(EntityManager $entityManager) { /** @var DoctrineInstallmentRepository $installmentRepository */ $installmentRepository = $entityManager->getRepository(Installment::class); $this->installmentRepository = $installmentRepository; } // ... } detail detail detail
  71. Fort couplage de la classe à la couche de persistence

    (ORM) Testabilité du code plus difficile Problématiques Impossibilité d’adapter facilement la couche de persistence / d’accès aux données
  72. Inversion des dépendances namespace App\FinancialServices\Repository; use App\FinancialServices\Entity\Installment; use App\FinancialServices\Entity\Loan; interface

    InstallmentRepository { public function byId(string $id): Installement; /** * @return Installment[] */ public function findByLoan(Loan $loan): array; /** * @return Installment[] */ public function findYearlyPaid(string $year): array; public function save(Installment $installment): void; }
  73. Inversion des dépendances // ... use App\FinancialServices\Repository\InstallmentRepository; class ComputeYearlyLoanInstallmentReporting {

    public function __construct( private readonly InstallmentRepository $installmentRepository, ) { } // ... } detail abstraction
  74. Faible couplage de la classe à la couche de persistence

    Testabilité du code aisée Bénéfices Capacité d’adapter facilement la couche de persistence / d’accès aux données
  75. Inversion des dépendances final class InMemoryInstallmentRepository implements InstallmentRepository { /**

    @var array<string, Installment> */ private array $store = []; public function byId(string $id): Installement { $installment = $this->store[$id] ?? null; if (!$installment instanceof Installment) { throw new DomainException('Installment not found!'); } return $installment; } public function save(Installment $installment): void { $this->store[(string) $installment->getId()] = $installment; } } detail abstraction
  76. Inversion des dépendances final class InMemoryInstallmentRepository implements InstallmentRepository { //

    ... public function findByLoan(Loan $loan): array { return \array_values(\array_filter( $this->store, static fn (Installment $installment): bool => $installment->getLoan()->equals($loan), )); } public function findYearlyPaid(string $year): array { return \array_values(\array_filter( $this->store, static fn (Installment $installment): bool => $installment->getDueDate()->format('Y') === $year && $installment->isPaid(), )); } }
  77. Inversion des dépendances final class DoctrineInstallmentRepository implements InstallmentRepository { public

    function __construct( private readonly EntityManagerInterface $entityManager, ) { } public function byId(string $id): Installement { $installment = $this->getRepository()->find($id); if (!$installment instanceof Installment) { throw new DomainException('Installment not found!'); } return $installment; } public function save(Installment $installment): void { $this->entityManager->persist($installment); $this->entityManager->flush(); } private function getRepository(): EntityRepository { return $this->entityManager->getRepository(Installment::class); } } detail abstraction abstraction
  78. Inversion des dépendances final class DoctrineInstallmentRepository implements InstallmentRepository { //

    ... public function findByLoan(Loan $loan): array { return $this->getRepository()->findBy(['loan' => $loan]); } public function findYearlyPaid(string $year): array { $qb = $this->getRepository()->createQueryBuilder('i'); $qb ->andWhere($qb->expr()->between('i.paymentDate', ':from', ':to')) ->andWhere($qb->expr()->eq('i.status', ':status')) ->setParameter('from', $year . '-01-01') ->setParameter('to', $year . '-12-31') ->setParameter('status', InstallmentStatus::PAID); /** @var Installment[] */ return $qb->getQuery()->getResult(); } }
  79. Inversion des dépendances // Unit testing case $computer = new

    ComputeYearlyLoanInstallmentReporting( new InMemoryInstallmentRepository(), ); // General use case $computer = new ComputeYearlyLoanInstallmentReporting( new DoctrineInstallmentRepository( new EntityManager(...) ), );
  80. Bibliothèques de code de la communauté PHP https://unsplash.com/@tomas_yates Pour ne

    pas réinventer la roue et s’appuyer sur du code de qualité.
  81. Bibliothèques Tierces ★ beberlei/assert ★ league/csv ★ league/commonmark ★ league/flysystem

    ★ moneyphp/money ★ monolog/monolog ★ nesbot/carbon ★ symfony/clock ★ symfony/console ★ symfony/event-dispatcher ★ symfony/http-client ★ symfony/string ★ …