Slide 1

Slide 1 text

Keeping your Legacy Codebase Alive! March, 21st 2025 - Dutch PHP Conference - Amsterdam, The Netherlands

Slide 2

Slide 2 text

@hhamon / [email protected] Hugo HAMON Consultant PHP / Symfony @ KODERO Symfony Certi fi ed Expert Developer

Slide 3

Slide 3 text

Why modernizing a “legacy” codebase?

Slide 4

Slide 4 text

What’s “legacy code” ? The one you haven’t written in first place 🫣 The one you’ve just written (and goes to production) 🥳 The one that is not tested (and goes to production) 😭 The one that is based on no longer supported dependencies 😞 The one that “works” on prod but no one wants to change 😰

Slide 5

Slide 5 text

Reasons for operating a change Validating a new added feature Fixing a persistent or business impacting bug Improving or simplifying the overall “shape” of the code Detecting and optimizing performance issues Maintaining your application for the years to come Replacing an old piece of software for a modern one Easing infrastructure & dependencies upgrades … https://unsplash.com/@clemono

Slide 6

Slide 6 text

Leaving the code in a better shape than it was before changing it What are the benefits for the tech team? Easing the upcoming evolutions and changes to the code base Lowering the risk of introducing new regressions and bugs when changing the code Keeping focusing on bringing value to the business and end users on the long run https://unsplash.com/@orrbarone 🏆 🥇 🥈 🥉

Slide 7

Slide 7 text

What kind of change to operate? Purely syntactic change (“code style”) Bug fix Duplicated code extraction No longer supported dependency replacement New feature integration Components abstraction / encapsulation Automated tests addition Infrastructure level change

Slide 8

Slide 8 text

Impactful changes! Impacts on third party services https://unsplash.com/@vinic_ Impacts on the data model Changes that introduce a breaking change for others Impacts on the business

Slide 9

Slide 9 text

Keeping delivery value for the business https://unsplash.com/@nathan_cima Define a roadmap with the stake holders Plan an action plan ahead Communicate with all involved parties Dedicate a testing / preprod environnement Use feature flags for beta testers Integrate tools to automate complex tasks

Slide 10

Slide 10 text

Sentry https://sentry.io

Slide 11

Slide 11 text

Automated Tests at the Heart of Software Development

Slide 12

Slide 12 text

“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 veri fi ably. 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

Slide 13

Slide 13 text

Different types of tests https://unsplash.com/@imchenyf Unit Tests Integration Tests E2E Tests Others Application Tests

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

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()); } }

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

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'); Panther Library

Slide 18

Slide 18 text

final class PostAppointmentsCancellationControllerTest extends WebTestCase { public function testCannotCancelPastAppointment(): void { // ... $this->browser() ->post( url: \sprintf('/api/appointments/%s/cancellation', $appointment->getId()), options: HttpOptions::json([ 'referenceNumber' => 'E4N6ST', 'lastName' => 'SMITH', 'reason' => 'I booked another one earlier.', ]), ) ->assertStatus(422) ->assertJson() ->assertJsonMatches('violations[0].title', 'Appointment is no longer cancellable.') ->use(function (MailerComponent $component): void { $component->assertNoEmailSent(); }) ; } } Symfony + Zenstruck Browser

Slide 19

Slide 19 text

Automate Legacy Code Modernization with PHP Tools

Slide 20

Slide 20 text

“ Dependencies management, what a hell!” “ How to get rid of all these include / require statements ?! ” https://unsplash.com/@nate_dumlao “ If I only could execute all these scripts within a one line command line… ”

Slide 21

Slide 21 text

Composer > Dependencies Management { "require": { "php": ">=7.2", "league/flysystem": "^2.3.2", "moneyphp/money": "^3", "pagerfanta/core": "*" } }

Slide 22

Slide 22 text

{ "require": { "php": ">=7.2", "league/flysystem": "^2.5.0", "moneyphp/money": "^3.3.3", "pagerfanta/core": "^3.8.0" } } $ php composer bump

Slide 23

Slide 23 text

Dependabot https://github.com/dependabot Renovate.js https://github.com/renovatebot

Slide 24

Slide 24 text

{ "require": { "php": ">=7.2", "ext-iconv": "*", "ext-imap": "*", "ext-mbstring": "*", "ext-pdo": "*", "ext-xsl": "*", ... } } $ php composer require ext-iconv ext-imap ext-mbstring …

Slide 25

Slide 25 text

{ "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

Slide 26

Slide 26 text

“ WOW ! I love these new functions (or classes) added to PHP 8.x. They would be very helpful for our code base! ” https://unsplash.com/@1nimidiffa_ “ Sadly, we’re still running production with PHP 7… ”

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

$text = <<

Slide 29

Slide 29 text

{ "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" } } Symfony Polyfills

Slide 30

Slide 30 text

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.

Slide 31

Slide 31 text

“There is absolutely no consistency from one file to another! ” https://unsplash.com/@xavi_cabrera “ Let’s make things consistent once and for all for everyone ”

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

Configuration (1/3) return ECSConfig::configure() ->withPaths([ __DIR__ . '/config', __DIR__ . '/src', __DIR__ . '/tests', ]) ->withSkip([ __DIR__ . '/config/secrets/', ArrayOpenerAndCloserNewlineFixer::class, MethodChainingNewlineFixer::class, ]) ; ecs.php

Slide 34

Slide 34 text

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)

Slide 35

Slide 35 text

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)

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

“ I’d love to modernize my codebase but how should I start? ” https://unsplash.com/@brookecagle “ If I only could be able to have a clear view on what is worth being fixed… ”

Slide 38

Slide 38 text

PHPStan https://phpstan.org/

Slide 39

Slide 39 text

{ ... "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 } } } PHPStan & Plugins

Slide 40

Slide 40 text

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 > Configuration > phpstan.neon file

Slide 41

Slide 41 text

$ php vendor/bin/phpstan --generate-baseline

Slide 42

Slide 42 text

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 > Generated Baseline File

Slide 43

Slide 43 text

“ It’s going to take so much time and effort to fix everything! ” https://unsplash.com/@odissei “ Could we have the opportunity to automate some of these changes? ”

Slide 44

Slide 44 text

PHPStorm

Slide 45

Slide 45 text

Rector https://getrector.com/ https://getrector.com

Slide 46

Slide 46 text

Rector ˒ Adding properties & methods type hints ˒ Auto reindentation ˒ Convert PHP annotations to PHP attributes ˒ Detect and remove dead code ˒ Leverage constructor property promotion ˒ Use modern syntax (array, closures, etc.) ˒ Leverage “ early returns ” ˒ Make properties & methods private ˒ Rename functions and methods ˒ Migrate framework based code (Symfony, Twig, PHPUnit, Laravel, etc.) ˒ Extensibility by creating (or importing third party) new rules ˒ …

Slide 47

Slide 47 text

{ ... "require-dev": { ... "rector/rector": "^1.2.0" } } Rector > Installation

Slide 48

Slide 48 text

C declare(strict_types=1); use Rector\Config\RectorConfig; return RectorConfig::configure() ->withPaths([ __DIR__ . '/src', ]) ->withSkip([ __DIR__ . '/src/Migrations', ]) ->withAttributesSets(symfony: true, doctrine: true) ->withPhp74Sets() ->withPreparedSets(typeDeclarations: true); Rector > Configuration File rector.php

Slide 49

Slide 49 text

$ php vendor/bin/rector ——dry—run

Slide 50

Slide 50 text

Code Refactoring

Slide 51

Slide 51 text

Refactoring ˒ Organize code by business / domain / feature ˒ Reuse domain terminology in code ˒ Expose semantic class constructors ˒ Leverage use of native enumeration ˒ Enable dependency inversion principle ˒ Define short and focused interfaces ˒ Avoir anemic domain model for entities ˒ Assert values for arguments / parameters ˒ Reuse trusted third party PHP dependencies ˒ …

Slide 52

Slide 52 text

Organize code by domain https://unsplash.com/@odissei In order to better identify modules that coexist within the codebase.

Slide 53

Slide 53 text

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 🤔

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Reuse domain terminology https://unsplash.com/@waldemarbrandt67w To name code data structures according to their real terminology within the business they belong to.

Slide 57

Slide 57 text

// 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; } }

Slide 58

Slide 58 text

class ComputeYearlyLoanInstallmentReporting { // ... /** * @return array */ public function __invoke(string $year): array { // ... } }

Slide 59

Slide 59 text

class ComputeYearlyLoanInstallmentReporting { // ... /** * @return array */ public function __invoke(string $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(); } }

Slide 60

Slide 60 text

Avoid anemic domain models https://unsplash.com/@pattib Expose explicit domain focused methods from the domain entities

Slide 61

Slide 61 text

// 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); }

Slide 62

Slide 62 text

// 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);

Slide 63

Slide 63 text

class Installment { public function __construct( private readonly Loan $loan, private readonly InstallmentStatus $status, private readonly DateTimeImmutable $dueDate, private readonly float $amount, ) { } }

Slide 64

Slide 64 text

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, ); } }

Slide 65

Slide 65 text

class Installment { private function __construct( private readonly Loan $loan, private readonly InstallmentStatus $status, private readonly DateTimeImmutable $dueDate, private readonly float $amount, ) { } }

Slide 66

Slide 66 text

// 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);

Slide 67

Slide 67 text

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; } }

Slide 68

Slide 68 text

// 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()); }

Slide 69

Slide 69 text

Leverage built-in enumerations https://unsplash.com/@glenncarstenspeters Encapsulate lists of predefined values within the same data structure.

Slide 70

Slide 70 text

enum InstallmentStatus: string { case DUE = 'due'; case OVERDUE = 'overdue'; case PAID = 'paid'; public function isPaid(): bool { return self::PAID->value === $this->value; } public function pay(): self { if ($this->isPaid()) { throw new LogicException('Installment already paid!'); } return self::PAID; } }

Slide 71

Slide 71 text

class Installment { // ... private InstallmentStatus $status = InstallmentStatus::DUE; public function isPaid(): bool { return $this->status->isPaid(); } public function pay(DateTimeImmutable $paymentDate): void { $this->status = $this->status->pay(); $this->paymentDate = $paymentDate; } }

Slide 72

Slide 72 text

Dependency inversion https://unsplash.com/@cmycatistufa200gsugden Reducing coupling between high-level modules and low-level modules.

Slide 73

Slide 73 text

˒ High level modules should not depend on low level modules. ˒ Abstractions should not depend on implementation details ; implementations should depend on abstractions. Dependency Inversion

Slide 74

Slide 74 text

// ... 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

Slide 75

Slide 75 text

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; }

Slide 76

Slide 76 text

// ... use App\FinancialServices\Repository\InstallmentRepository; class ComputeYearlyLoanInstallmentReporting { public function __construct( private readonly InstallmentRepository $installmentRepository, ) { } // ... } detail abstraction

Slide 77

Slide 77 text

final class InMemoryInstallmentRepository implements InstallmentRepository { /** @var array */ 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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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->value); /** @var Installment[] */ return $qb->getQuery()->getResult(); } }

Slide 80

Slide 80 text

// Unit testing case $computer = new ComputeYearlyLoanInstallmentReporting( new InMemoryInstallmentRepository(), ); // General use case $computer = new ComputeYearlyLoanInstallmentReporting( new DoctrineInstallmentRepository( new EntityManager(...) ), );

Slide 81

Slide 81 text

@hhamon / [email protected] Thank You!