Slide 1

Slide 1 text

Confoo 2018 / Mar. 8th / Montréal / Canada Hugo Hamon Designing Better Object Oriented Softwares

Slide 2

Slide 2 text

Hugo Hamon

Slide 3

Slide 3 text

-1- Object Oriented Design

Slide 4

Slide 4 text

Dependency Injection

Slide 5

Slide 5 text

Dependency Injection Dependency Injection is where components are given their dependencies through their constructors, methods, or directly into fields. Those components do not get their dependencies themselves, or instantiate them directly. — picocontainer.com/injection.html

Slide 6

Slide 6 text

class ChessGameLoader { private $repository; private $cache; private $serializer; public function __construct() { $this->repository = new InMemoryChessGameRepository(); $this->cache = new RedisCache(); $this->serializer = new Serializer(); } public function load(UuidInterface $id): ?ChessGame { // … } }

Slide 7

Slide 7 text

Problems • Tight coupling • Concretions instead of abstractions • Not testable • Not flexible

Slide 8

Slide 8 text

class ChessGameLoader { private $repository; private $cache; private $serializer; public function __construct( InMemoryChessGameRepository $repository, RedisCache $cache, Serializer $serializer ) { $this->repository = $repository; $this->cache = $cache; $this->serializer = $serializer; } }

Slide 9

Slide 9 text

Pros • Code is unit testable • Dependencies can be mocked • Dependencies can be changed Cons • Client code is still tightly coupled • Client code doesn’t rely on abstractions

Slide 10

Slide 10 text

Dependency Injection Container A dependency injection container is an object that enables to standardize and centralize the way objects are constructed and configured in an application. — symfony.com

Slide 11

Slide 11 text

$loader = new ChessGameLoader( new InMemoryChessGameRepository(), new RedisCache(new Predis\Client('tcp://...')), new Serializer(new JsonDecoder()) ); $game = $loader->load(Uuid::fromString('1f809a73-...')); Complex Construction

Slide 12

Slide 12 text

parameters: env(REDIS_DSN): 'tcp://10.0.0.1:6379' services: App\Serializer\Serializer: arguments: ['@App\Serializer\Decoder\JsonDecoder'] Predis\Client: arguments: ['%env(resolve:REDIS_DSN)%'] App\Cache\RedisCache: arguments: ['@Predis\Client'] App\ChessGame\InMemoryChessGameRepository: ~ App\ChessGame\ChessGameLoader: arguments: - '@App\ChessGame\InMemoryChessGameRepository' - '@App\Cache\RedisCache' - '@App\Serializer\Serializer'

Slide 13

Slide 13 text

$loader = $container ->get(ChessGameLoader::class) ;

Slide 14

Slide 14 text

Object Composition

Slide 15

Slide 15 text

Object Composition In computer science, object composition is a way to combine simple objects or data types into more complex ones. — wikipedia.com

Slide 16

Slide 16 text

class ChessGameLoader { // ... public function __construct( InMemoryChessGameRepository $repository, RedisCache $cache, Serializer $serializer ) { // ... } public function load(UuidInterface $id): ?ChessGame { if ($this->cache->has($key = sprintf('game/%s', $id))) { return $this->serializer->deserialize( ChessGame::class, $this->cache->get($key) ); } return $this->repository->byId($id); } } RedisRepository

Slide 17

Slide 17 text

class RedisChessGameRepository implements ChessGameRepository { public function byId(UuidInterface $id): ?ChessGame { $key = sprintf('game/%s', $id); if (!$this->cache->has($key)) { return null; } return $this->serializer->deserialize( ChessGame::class, $this->cache->get($key) ); } }

Slide 18

Slide 18 text

class ChessGameLoader { // ... public function __construct( InMemoryChessGameRepository $inMemoryRepository, RedisChessGameRepository $redisRepository ) { // ... } public function load(UuidInterface $id): ?ChessGame { if ($game = $this->redisRepository->byId($id)) { return $game; } return $this->repository->byId($id); } }

Slide 19

Slide 19 text

class ChainChessGameRepository implements ChessGameRepository { private $repositories = []; public function add(ChessGameRepository $repository): void { $this->repositories[] = $repository; } public function byId(UuidInterface $uuid): ?ChessGame { foreach ($this->repositories as $repository) { if ($game = $repository->byUuid($uuid)) { return $game; } } return null; } }

Slide 20

Slide 20 text

class ChessGameLoader { // ... public function __construct( ChessGameRepository $repository, LoggerInterface $logger ) { // ... } public function load(UuidInterface $id): ?ChessGame { $this->logger->log(sprinf('Load game %s', $id)); return $this->repository->byId($id); } }

Slide 21

Slide 21 text

$repository = new ChainChessGameRepository(); $repository->add(new RedisChessGameRepository(...)); $repository->add(new InMemoryChessGameRepository()); $loader = new ChessGameLoader( $repository, new NullLogger() ); $game = $loder->load(Uuid::fromString('1f809a73-...'));

Slide 22

Slide 22 text

SOLID Principles

Slide 23

Slide 23 text

Single Responsibility A class should have one, and only one, reason to change. — Robert C. Martin

Slide 24

Slide 24 text

class ChessGameRunner { // ... public function startNewGame(ChessGameContext $context): ChessGame { $game = new ChessGame( Uuid::uuid4(), $this->loadPlayer($context->getPlayerOne()), $this->loadPlayer($context->getPlayerTwo()) ); $this->gameRepository->save($game); return $game; } private function loadPlayer(string $player): Player { return Player::fromUserAccount($this->userRepository->byUsername($player)); } }

Slide 25

Slide 25 text

class ChessGameRunner { public function startNewGame(ChessGameContext $context): ChessGame { $game = new ChessGame( Uuid::uuid4(), $this->loadPlayer($context->getPlayerOne()), $this->loadPlayer($context->getPlayerTwo()) ); $this->gameRepository->save($game); return $game; } } Persistence Object Construction

Slide 26

Slide 26 text

class ChessGameFactory { private $userRepository; public function __construct(UserAccountRepository $repository) { $this->userRepository = $repository; } public function create(string $player1, string $player2): ChessGame { return new ChessGame( Uuid::uuid4(), Player::fromUserAccount($this->userRepository->byUsername($player1)), Player::fromUserAccount($this->userRepository->byUsername($player2)) ); } }

Slide 27

Slide 27 text

class ChessGameRunner { private $gameRepository; private $gameFactory; public function __construct( ChessGameRepository $repository, ChessGameFactory $factory ) { $this->gameRepository = $repository; $this->gameFactory = $factory; } }

Slide 28

Slide 28 text

class ChessGameRunner { // ... public function startNewGame(ChessGameContext $context): ChessGame { $game = $this->gameFactory->create( $context->getPlayerOne(), $context->getPlayerTwo() ); $this->gameRepository->save($game); return $game; } }

Slide 29

Slide 29 text

Open Closed Principle You should be able to extend a classes behavior, without modifying it. — Robert C. Martin

Slide 30

Slide 30 text

class ChessGameFactory { private $userRepository; public function __construct(UserAccountRepository $repository) { $this->userRepository = $repository; } public function create(string $player1, string $player2): ChessGame { return new ChessGame( Uuid::uuid4(), Player::fromUserAccount($this->userRepository->byUsername($player1)), Player::fromUserAccount($this->userRepository->byUsername($player2)) ); } }

Slide 31

Slide 31 text

interface SerialGenerator { public function nextIdentity(): UuidInterface; } class FixedSerialGenerator implements SerialGenerator { public function nextIdentity(): UuidInterface { return new Uuid::fromString('1f809a73-63d5-40dd-9bc0-f7bc6813a4bc'); } } class DefaultSerialGenerator { public function nextIdentity(): UuidInterface { return new Uuid::uuid4(); } }

Slide 32

Slide 32 text

class ChessGameFactory { private $userRepository; private $identityGenerator; public function __construct( UserAccountRepository $repository, SerialGenerator $identityGenerator ) { $this->userRepository = $repository; $this->identityGenerator = $identityGenerator; } public function create(string $player1, string $player2): ChessGame { return new ChessGame( $this->identityGenerator->nextIdentity(), $this->loadPlayer($player1), $this->loadPlayer($player2) ); } }

Slide 33

Slide 33 text

Liskov Substitution Principle Derived classes must be substitutable for their base classes. — Robert C. Martin

Slide 34

Slide 34 text

interface ChessGameRepository { /** * @throws ChessGameNotFound */ public function byId(UuidInterface $uuid): ChessGame; }

Slide 35

Slide 35 text

class ChessGameRunner { // ... public function loadGame(UuidInterface $id): ChessGame { try { return $this->gameRepository->byId($id); } catch (ChessGameNotFound $e) { throw ChessGameUnavailable::gameNotFound($id, $e); } } }

Slide 36

Slide 36 text

class InMemoryChessGameRepository implements ChessGameRepository { private $games = []; // ... public function byId(UuidInterface $uuid): ChessGame { $uuid = $uuid->toString(); if (!isset($this->games[$uuid])) { throw new ChessGameNotFound($uuid); } return $this->games[$uuid]; } }

Slide 37

Slide 37 text

class DoctrineChessGameRepository implements ChessGameRepository { private $repository; public function __construct(ManagerRegistry $registry) { $this->repository = $registry->getRepository(ChessGame::class); } public function byId(UuidInterface $uuid): ChessGame { if (!$game = $this->repository->find($uuid->toString())) { throw new ChessGameNotFound($uuid->toString()); } return $game; } }

Slide 38

Slide 38 text

$runner = new ChessGameRunner( new InMemoryChessGameRepository(), new ChessGameFactory(...) ); $runner = new ChessGameRunner( new DoctrineChessGameRepository(...), new ChessGameFactory(...) ); $runner->loadGame(Uuid::fromString('1f809a73-...'));

Slide 39

Slide 39 text

Interface Segregation Principle Make fine grained interfaces that are client specific. — Robert C. Martin

Slide 40

Slide 40 text

interface ChessGameRepository { public function byId(UuidInterface $uuid): ChessGame; } interface UserAccountRepository { public function byUsername(string $username): User; } interface ChessGameFactory { public function create(string $player1, string $player2): ChessGame; }

Slide 41

Slide 41 text

Dependency Inversion Principle Depend on abstractions, not on concretions. — Robert C. Martin

Slide 42

Slide 42 text

class ChessGameRunner { private $gameRepository; private $gameFactory; public function __construct( ChessGameRepository $repository, ChessGameFactory $factory ) { $this->gameRepository = $repository; $this->gameFactory = $factory; } } Interfaces

Slide 43

Slide 43 text

Object Calisthenics

Slide 44

Slide 44 text

Object Calisthenics Calisthenics are gymnastic exercises designed to develop physical health and vigor, usually performed with little or no special apparatus. — dictionary.com

Slide 45

Slide 45 text

1. One level of indentation per method 2.Don’t use the ELSE keyword 3.Wrap primitive types and strings 4.Two instance operators per line 5.Don’t abbreviate 6.Make short and focused classes 7. Keep number of instance properties low 8.Treat lists as custom collection objects 9.Avoid public accessors and mutators

Slide 46

Slide 46 text

Wrap Primitive Types and Strings

Slide 47

Slide 47 text

class ChessGame { /** @var ChessBoard */ private $board; public function makeMove( Pawn $pawn, int $originRow, int $originCol, int $targetRow, int $targetCol ): void { $this->ensureValidMove( $pawn, $originRow, $originCol, $targetRow, $targetCol ); $this->board->getSquare($targetRow, $targetCol)->add($pawn); } } Square

Slide 48

Slide 48 text

class Square { private $row; private $col; public function __construct(int $row, int $col) { $range = range(1, 8); if (!in_array($row, $range, true)) { throw new \InvalidArgumentException('Invalid row.'); } if (!in_array($col, $range, true)) { throw new \InvalidArgumentException('Invalid col.'); } $this->row = $row; $this->col = $col; } // ... }

Slide 49

Slide 49 text

class ChessGame { /** @var ChessBoard */ private $board; public function makeMove( Pawn $pawn, Square $origin, Square $target ) { $this->ensureValidMove($pawn, $origin, $target); $this ->board ->getSquare($target->row(), $target->col()) ->add($pawn); } } Move

Slide 50

Slide 50 text

class Move { private $pawn; private $originSquare; private $targetSquare; public function __construct(Pawn $pawn, Square $from, Square $to) { $this->pawn = $pawn; $this->originSquare = $from; $this->targetSquare = $to; } // ... }

Slide 51

Slide 51 text

class ChessGame { /** @var ChessBoard */ private $board; public function makeMove(Move $move): void { $this->ensureValidMove($move); $this ->board ->getSquare($move->getTargetSquare()) ->add($pawn); } }

Slide 52

Slide 52 text

One Level of Indentation per Method

Slide 53

Slide 53 text

class ChessGame { private $finished = false; public function makeMove(Move $move): void { if (!$this->finished) { if ($this->isValidMove($move)) { $this->performMove($move); } } } } 0 1 2

Slide 54

Slide 54 text

class ChessGame { private $finished = false; public function makeMove(Move $move): void { if ($this->finished) { throw new GameAlreadyFinished(); } if (!$this->isValidMove($move)) { throw new InvalidGameMove(); } $this->performMove($move); } } 0 1 0 1 0

Slide 55

Slide 55 text

class ChessGame { private $finished = false; public function makeMove(Move $move): void { $this->ensureGameNotFinished(); $this->ensureValidMove($move); $this->performMove($move); } private function ensureGameNotFinished(): void { if ($this->finished) { throw new GameAlreadyFinished(); } } private function ensureValidMove(Move $move): void { if (!$this->isValidMove($move)) { throw new InvalidGameMove(); } } } 0 1 1

Slide 56

Slide 56 text

Avoid the ELSE Keyword

Slide 57

Slide 57 text

class ChessGame { // ... private $finished = false; public function makeMove(Move $move): void { if (!$this->finished) { $this->ensureValidMove($move); $this->performMove($move); } else { throw new GameAlreadyFinished(); } } }

Slide 58

Slide 58 text

class ChessGame { // ... private $finished = false; public function makeMove(Move $move): void { if ($this->finished) { return; } $this->ensureValidMove($move); $this->performMove($move); } }

Slide 59

Slide 59 text

class ChessGame { // ... private $finished = false; public function makeMove(Move $move): void { if ($this->finished) { throw new GameAlreadyFinished(); } $this->ensureValidMove($move); $this->performMove($move); } }

Slide 60

Slide 60 text

class ChessGame { // ... private $finished = false; public function makeMove(Move $move): void { $this->ensureGameNotFinished(); $this->ensureValidMove($move); $this->performMove($move); } }

Slide 61

Slide 61 text

Two Instance Operators per Line

Slide 62

Slide 62 text

class ChessGame { /** @var ChessBoard */ private $board; public function makeMove(Move $move): void { $this->ensureValidMove($move); $this ->board ->getSquare($move->getTargetSquare()) ->add($pawn); } } 3

Slide 63

Slide 63 text

class ChessGame { /** @var ChessBoard */ private $board; public function makeMove(Move $move): void { $this->ensureValidMove($move); $this->board->placePawnOnSquare( $move->getTargetSquare(), $move->getPawn() ); } } 2

Slide 64

Slide 64 text

Assertions Library

Slide 65

Slide 65 text

Assertions Library Assertions libraries provide useful set of assertions and guard methods for input validation in business model. They help reducing the number of code execution paths, thus reducing methods complexity by having a linear code.

Slide 66

Slide 66 text

https://github.com/beberlei/assert

Slide 67

Slide 67 text

Assertion::alnum(mixed $value); Assertion::base64(string $value); Assertion::between(mixed $value, mixed $lowerLimit, mixed $upperLimit); Assertion::betweenLength(mixed $value, int $minLength, int $maxLength); Assertion::boolean(mixed $value); Assertion::choice(mixed $value, array $choices); Assertion::choicesNotEmpty(array $values, array $choices); Assertion::classExists(mixed $value); Assertion::contains(mixed $string, string $needle); Assertion::count(array|\Countable $countable, int $count); Assertion::date(string $value, string $format); Assertion::defined(mixed $constant); Assertion::digit(mixed $value); Assertion::directory(string $value); Assertion::e164(string $value); Assertion::email(mixed $value); Assertion::endsWith(mixed $string, string $needle); // ...

Slide 68

Slide 68 text

class Square { public function __construct(int $row, int $col) { $range = range(1, 8); if (!in_array($row, $range, true)) { throw new \InvalidArgumentException('Invalid row.'); } if (!in_array($col, $range, true)) { throw new \InvalidArgumentException('Invalid col.'); } // ... } }

Slide 69

Slide 69 text

class Square { private $row; private $col; public function __construct(int $row, int $col) { Assertion::between($row, 1, 8); Assertion::between($col, 1, 8); $this->row = $row; $this->col = $col; } // ... }

Slide 70

Slide 70 text

-2- Domain Model Main Structures

Slide 71

Slide 71 text

Ubiquitous Language

Slide 72

Slide 72 text

Ubiquitous Language Ubiquitous Language is the term Eric Evans uses in Domain Driven Design for the practice of building up a common, rigorous language between developers and users. This language should be based on the Domain Model used in the software - hence the need for it to be rigorous, since software doesn't cope well with ambiguity. — Martin Fowler

Slide 73

Slide 73 text

Domain Example •An invoice is issued with a unique number •The invoice is due within X days •The invoice is associated to a customer account •A payment is recorded for this invoice •The invoice is partially paid •Waiting for the remaining amount to be collected •The invoice is fully paid •The invoice is closed •The invoice is overpaid •Overdue amount must be refunded

Slide 74

Slide 74 text

Value Objects

Slide 75

Slide 75 text

Value Objects A value object is an object representing an atomic value or concept. The value object is responsible for validating the consistency of its own state. It’s designed to always be in a valid, consistent and immutable state.

Slide 76

Slide 76 text

Value Object Properties •They don’t have an identity •They’re responsible for validating their state •They are immutable by design •They are always valid by design •Equality is based on their fields •They are interchangeable without side effects

Slide 77

Slide 77 text

final class Currency { private $code; public function __construct(string $code) { if (!in_array($code, ['EUR', 'USD', 'CAD'], true)) { throw new \InvalidArgumentException('Unsupported currency.'); } $this->code = $code; } public function equals(self $other): bool { return $this->code === $other->code; } }

Slide 78

Slide 78 text

new Currency('EUR'); // OK new Currency('USD'); // OK new Currency('CAD'); // OK new Currency('BTC'); // Error

Slide 79

Slide 79 text

final class Money { private $amount; private $currency; public function __construct(int $amount, Currency $currency) { $this->amount = $amount; $this->currency = $currency; } // ... }

Slide 80

Slide 80 text

final class Money { // ... public function add(self $other): self { $this->ensureSameCurrency($other->currency); return new self($this->amount + $other->amount, $this->currency); } private function ensureSameCurrency(Currency $other): void { if (!$this->currency->equals($other)) { throw new \RuntimeException('Currency mismatch'); } } }

Slide 81

Slide 81 text

$a = new Money(100, new Currency('EUR')); // 1€ $b = new Money(500, new Currency('EUR')); // 5€ $c = $a->add($b); // 6€ $c->add(new Money(300, new Currency('USD'))); // Error

Slide 82

Slide 82 text

Entities

Slide 83

Slide 83 text

Entities An entity is an in-memory representation of a business object. It’s uniquely identified and must always be in a valid state at every step of its lifecycle. Thus, its methods convey the ubiquitous language operations.

Slide 84

Slide 84 text

Beware of Anemic Models

Slide 85

Slide 85 text

$invoice = new Invoice(); $invoice->setNumber('INV-20180306-66'); $invoice->setIssueDate('2018-03-10'); $invoice->setDueDate('2018-04-10'); $invoice->setDueAmount(1350.90); $invoice->setDueAmountCurrency('USD'); $invoice->setStatus('issued'); // + all the getter methods

Slide 86

Slide 86 text

class Invoice { private $number; private $customerId; private $issueDate; private $dueDate; private $dueAmount; private $remainingDueAmount; public function __construct( InvoiceId $number, CustomerId $customerId, Money $dueAmount, \DateTimeImmutable $dueDate ) { $this->number = $number; $this->customerId = $customerId; $this->issueDate = new \DateTimeImmutable('today', new \DateTimeZone('UTC')); $this->dueDate = $dueDate; $this->dueAmount = $dueAmount; $this->remainingDueAmount = clone $dueAmount; } }

Slide 87

Slide 87 text

Issue an Invoice $invoice = new Invoice( new InvoiceId('INV-20180306-66'), new CustomerId('3429234'), new Money(9990, new Currency('EUR')), new \DateTimeImmutable('+30 days') );

Slide 88

Slide 88 text

class Invoice { // ... private $overdueAmount; private $closingDate; private $payments = []; public function collectPayment(Payment $payment): void { $amount = $payment->getAmount(); $this->remainingDueAmount = $this->remainingDueAmount->subtract($amount); $this->overdueAmount = $this->remainingDueAmount->absolute(); $zero = new Money(0, $this->remainingDueAmount->getCurrency()); if ($this->remainingDueAmount->lessThanOrEqual($zero)) { $this->closingDate = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); } $this->payments[] = new CollectedPayment( $payment->getReceptionDate(), $amount, $payment->getSource() // wire, check, cash, etc. ); } }

Slide 89

Slide 89 text

class Invoice { public function isClosed(): bool { return $this->closingDate instanceof \DateTimeImmutable; } public function isPaid(): bool { $zero = new Money(0, $this->remainingDueAmount->getCurrency()); return $this->remainingDueAmount->lessThanOrEqual($zero); } public function isOverpaid(): bool { $zero = new Money(0, $this->remainingDueAmount->getCurrency()); return $this->remainingDueAmount->lessThan($zero); } }

Slide 90

Slide 90 text

Collecting Payments $invoice->collectPayment(new Payment( new \DateTimeImmutable('2018-03-04'), new Money(4900, new Currency('EUR')), new WireTransferPayment('450357035') )); $invoice->collectPayment(new Payment( new \DateTimeImmutable('2018-03-08'), new Money(5100, new Currency('EUR')), new WireTransferPayment('248748484') ));

Slide 91

Slide 91 text

Named Constructors

Slide 92

Slide 92 text

Named Constructor Static methods can be used as an alternative to the regular constructor to provide a simplified and semantic way to produce an object.

Slide 93

Slide 93 text

$time = Time::fromString('12:30'); $time = Time::fromSecondsSinceMidnight(21600); // 06:00 $time = Time::fromMinutesSinceMidnight(510); // 08:30 $color = Color::fromRGB(0, 255, 0); $color = Color::fromHex('#00FF00'); $color = Color::black(); $color = Color::white(); $code = PinCode::generate(); $uuid = Uuid::fromString(‘14bc56cc-f23e-4338-a758-d616dc515ea3'); $uuid = Uuid::uuid4(); $temp = Temperature::fromCelsius(-273.15); $temp = Temperature::fromFahrenheit(-459.67); $temp = Temperature::fromKelvin(0);

Slide 94

Slide 94 text

class Invoice { // ... public static function issue( string $number, string $customerId, Money $amount ): self { return new static( new InvoiceId($number), new CustomerId($customerId), $amount, new \DateTimeImmutable('+30 days'), ); } }

Slide 95

Slide 95 text

Issue an Invoice $invoice = Invoice::issue( 'INV-20180306-66', '3429234', new Money(9990, new Currency('EUR')) );

Slide 96

Slide 96 text

Collections

Slide 97

Slide 97 text

Collection Objects Simple lists structures should be encapsulated into Collection objects to offer dedicated manipulation operations.

Slide 98

Slide 98 text

class Invoice { // ... private $payments; public function __construct(…) { // ... $this->payments = new ArrayCollection(); } public function collectPayment(Payment $payment): void { // ... $this->payments->add(new CollectedPayment( $payment->getReceptionDate(), $amount, $payment->getSource() // wire, check, cash, etc. )); } }

Slide 99

Slide 99 text

Filtering the collection class Invoice { // ... public function countPaymentsReceivedAfterDueDate(): int { return $this ->payments ->filter(function (CollectedPayment $payment) { return $payment->getReceptionDate() > $this->dueDate; }) ->count(); } }

Slide 100

Slide 100 text

Custom Collection Type class CollectedPaymentCollection extends ArrayCollection { public function receivedAfter(\DateTimeImmutable $origin): self { $filter = function (CollectedPayment $payment) use ($origin) { return $payment->getReceptionDate() > $origin; }; return $this->filter($filter); } }

Slide 101

Slide 101 text

class Invoice { // … public function __construct(…) { // ... $this->payments = new CollectedPaymentCollection(); } public function countPaymentsReceivedAfterDueDate(): int { return $this ->payments ->receivedAfter($this->dueDate) ->count(); } }

Slide 102

Slide 102 text

Services

Slide 103

Slide 103 text

The Service Layer Defines an application's boundary with a layer of services that establishes a set of available operations and coordinates the application's response in each operation. — Randy Stafford

Slide 104

Slide 104 text

Services are global behavioral objects that encapsulate business logic. They take place between the controller and the domain layers as they manipulate one or several entities at the same time.

Slide 105

Slide 105 text

https://martinfowler.com/eaaCatalog/serviceLayer.html

Slide 106

Slide 106 text

class AccountingDepartment { // ... public function recordPayment(InvoiceId $invoiceId, Payment $payment): void { if (!$invoice = $this->repository->byId($invoiceId)) { throw InvoiceNotFound($invoiceId); } $invoice->collectPayment($payment); $this->repository->save($invoice); if ($invoice->isPaid()) { $this->dispatcher->dispatch('invoice.paid', new InvoiceWasPaid($invoice)); } } }

Slide 107

Slide 107 text

class InvoiceController extends Controller { public function collectPayment( Request $request, AccountingDepartment $service ): Response { $invoiceId = new InvoiceId($request->get('invoiceId')); $form = $this->createForm(CollectPaymentType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $service->recordPayment($invoiceId, $form->getData()); return $this->redirectToRoute('payment_recorded', ['invoiceId' => $invoiceId]); } return $this->render('invoice/payment.html.twig', [ 'form' => $form->createView(), ]); } }

Slide 108

Slide 108 text

Data Transfer Objects

Slide 109

Slide 109 text

Data Transfer Objects An object that carries data between processes in order to reduce the number of method calls. — Martin Fowler

Slide 110

Slide 110 text

class RecordPaymentCommand implements Command { private $invoiceId; private $paymentData; public function __construct(string $invoiceId, array $paymentData) { $this->invoiceId = $invoiceId; $this->paymentData = $paymentData; } public function getInvoiceId(): string { return $this->invoiceId; } public function getPaymentData(): array { return $this->paymentData; } }

Slide 111

Slide 111 text

class RecordPaymentCommandHandler implements CommandHandler { private $repository; public function __construct(InvoiceRepository $repository) { $this->repository = $repository; } public function handle(RecordPaymentCommand $command): void { $invoice = $this->repository->byId(new InvoiceId($command->getInvoiceId())); $invoice->collectPayment(Payment::fromPayload($command->getPaymentData())); $this->repository->save($invoice); } }

Slide 112

Slide 112 text

View Model

Slide 113

Slide 113 text

View Model Objects MVVM facilitates a separation of development of the graphical user interface from development of the business logic. The view model is a value converter, meaning the view model is responsible for exposing the data objects from the model in such a way that objects are easily managed and presented. — Wikipedia.com

Slide 114

Slide 114 text

class InvoiceView { public $invoiceNumber; public $customerName; public $customerAddress; public $dueDate; public $dueAmount; public $paidAmount; public $remainingAmount; }

Slide 115

Slide 115 text

class InvoiceViewBuilder { // ... public function buildView(Invoice $invoice, string $locale): InvoiceView { $customer = $this->customerRepository->byAccountId($invoice->getCustomerId()); $view = new InvoiceView(); $view->invoiceNumber = $invoice->getNumber()->toString(); $view->customerName = $customer->getName(); $view->customerAddress = $customer->getAddress()->toString(); $view->dueAmount = $this->formatMoney($invoice->getDueAmount(), $locale); $view->paidAmount = $this->formatMoney($invoice->getPaidAmount(), $locale); $view->remainingAmount = $this->formatMoney($invoice->getRemainingAmount(), $locale); return $view; } }

Slide 116

Slide 116 text

class InvoiceController extends Controller { public function summary( Request $request, InvoiceViewBuilder $builder ): Response { $invoice = $this->repository->byId($request->get('invoiceId')); $this->denyAccessUnlessGranted('VIEW', $invoice); $view = $builder->buildView($invoice, $request->getLocale(); return $this->render('invoice/summary.html.twig', [ 'invoiceView' => $view), ]); } }

Slide 117

Slide 117 text

{{ invoice.number }}

Total due: {{ invoice.dueAmount}}
Total paid: {{ invoice.paidAmount }}
Total remaining: {{ invoice.remainingAmount }}

Slide 118

Slide 118 text

-4- Thank you for attending!