Pro Yearly is on sale from $80 to $50! »

Designing Better Object Oriented Software with PHP

Designing Better Object Oriented Software with PHP

Designing software with an object oriented approach is hard... really hard! In fact, making good object oriented design (aka OOD) is very difficult for many developers as it goes far beyond basic concepts like classes, objects, inheritance and interfaces. This talk will provide tips and techniques to help you design better object oriented code. We'll cover topics like SOLID principles, composition vs inheritance, value objects, entities, etc.

E2ed7c278c8c49bb3e7fe0b7de039997?s=128

Hugo Hamon

May 31, 2018
Tweet

Transcript

  1. AFUP 2018 / May. 31st / Poitiers / France Hugo

    Hamon Designing Better Object Oriented Software
  2. Hugo Hamon

  3. -1- Object Oriented Design

  4. Dependency Injection

  5. 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
  6. 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 { // … } }
  7. Problems • Tight coupling • Concretions instead of abstractions •

    Not testable • Not flexible
  8. 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; } }
  9. 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
  10. 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
  11. $loader = new ChessGameLoader( new InMemoryChessGameRepository(), new RedisCache(new Predis\Client('tcp://...')), new

    Serializer(new JsonDecoder()) ); $game = $loader->load(Uuid::fromString('1f809a73-...')); Complex Construction
  12. 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'
  13. $loader = $container ->get(ChessGameLoader::class) ;

  14. Object Composition

  15. Object Composition In computer science, object composition is a way

    to combine simple objects or data types into more complex ones. — wikipedia.com
  16. 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
  17. 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) ); } }
  18. 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); } }
  19. 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; } }
  20. 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); } }
  21. $repository = new ChainChessGameRepository(); $repository->add(new RedisChessGameRepository(...)); $repository->add(new InMemoryChessGameRepository()); $loader =

    new ChessGameLoader( $repository, new NullLogger() ); $game = $loder->load(Uuid::fromString('1f809a73-...'));
  22. SOLID Principles

  23. Single Responsibility A class should have one, and only one,

    reason to change. — Robert C. Martin
  24. 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)); } }
  25. 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
  26. 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)) ); } }
  27. class ChessGameRunner { private $gameRepository; private $gameFactory; public function __construct(

    ChessGameRepository $repository, ChessGameFactory $factory ) { $this->gameRepository = $repository; $this->gameFactory = $factory; } }
  28. class ChessGameRunner { // ... public function startNewGame(ChessGameContext $context): ChessGame

    { $game = $this->gameFactory->create( $context->getPlayerOne(), $context->getPlayerTwo() ); $this->gameRepository->save($game); return $game; } }
  29. Open Closed Principle You should be able to extend a

    classes behavior, without modifying it. — Robert C. Martin
  30. 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)) ); } }
  31. interface GameIdGenerator { public function generate(): UuidInterface; } class FixedGameIdGenerator

    implements GameIdGenerator { public function generate(): UuidInterface { return new Uuid::fromString('1f809a73-63d5-40dd-9bc0-f7bc6813a4bc'); } } class RandomGameIdGenerator implements GameIdGenerator { public function generate(): UuidInterface { return new Uuid::uuid4(); } }
  32. class ChessGameFactory { private $userRepository; private $identityGenerator; public function __construct(

    UserAccountRepository $repository, GameIdGenerator $identityGenerator ) { $this->userRepository = $repository; $this->identityGenerator = $identityGenerator; } public function create(string $player1, string $player2): ChessGame { return new ChessGame( $this->identityGenerator->generate(), $this->loadPlayer($player1), $this->loadPlayer($player2) ); } }
  33. Liskov Substitution Principle Derived classes must be substitutable for their

    base classes. — Robert C. Martin
  34. interface ChessGameRepository { /** * @throws ChessGameNotFound */ public function

    byId(UuidInterface $uuid): ChessGame; }
  35. class ChessGameRunner { // ... public function loadGame(UuidInterface $id): ChessGame

    { try { return $this->gameRepository->byId($id); } catch (ChessGameNotFound $e) { throw ChessGameUnavailable::gameNotFound($id, $e); } } }
  36. 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]; } }
  37. 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; } }
  38. $runner = new ChessGameRunner( new InMemoryChessGameRepository(), new ChessGameFactory(...) ); $runner

    = new ChessGameRunner( new DoctrineChessGameRepository(...), new ChessGameFactory(...) ); $runner->loadGame(Uuid::fromString('1f809a73-...'));
  39. Interface Segregation Principle Make fine grained interfaces that are client

    specific. — Robert C. Martin
  40. 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; }
  41. interface UrlMatcherInterface { public function match(string $pathinfo): array; } interface

    UrlGeneratorInterface { public function generate(string $name, array $params = []): string; } interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface { public function getRouteCollection(): RouteCollection; }
  42. Dependency Inversion Principle Depend on abstractions, not on concretions. —

    Robert C. Martin
  43. class ChessGameRunner { private $gameRepository; private $gameFactory; public function __construct(

    ChessGameRepository $repository, ChessGameFactory $factory ) { $this->gameRepository = $repository; $this->gameFactory = $factory; } } Interfaces
  44. Object Calisthenics

  45. Object Calisthenics Calisthenics are gymnastic exercises designed to develop physical

    health and vigor, usually performed with little or no special apparatus. — dictionary.com
  46. 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
  47. Wrap Primitive Types and Strings

  48. 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
  49. 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; } // ... }
  50. 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
  51. 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; } // ... }
  52. class ChessGame { /** @var ChessBoard */ private $board; public

    function makeMove(Move $move): void { $this->ensureValidMove($move); $this ->board ->getSquare($move->getTargetSquare()) ->add($pawn); } }
  53. One Level of Indentation per Method

  54. class ChessGame { private $finished = false; public function makeMove(Move

    $move): void { if (!$this->finished) { if ($this->isValidMove($move)) { $this->performMove($move); } } } } 0 1 2
  55. 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
  56. 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
  57. Avoid the ELSE Keyword

  58. class ChessGame { // ... private $finished = false; public

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

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

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

    function makeMove(Move $move): void { $this->ensureGameNotFinished(); $this->ensureValidMove($move); $this->performMove($move); } }
  62. Two Instance Operators per Line

  63. class ChessGame { /** @var ChessBoard */ private $board; public

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

    function makeMove(Move $move): void { $this->ensureValidMove($move); $this->board->placePawnOnSquare( $move->getTargetSquare(), $move->getPawn() ); } } 2
  65. Avoid public accessors methods

  66. /!\ Beware of Anemic Models

  67. $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
  68. class Invoice { private $number; private $billingEntity; private $issueDate; private

    $dueDate; private $dueAmount; private $remainingDueAmount; public function __construct( InvoiceId $number, BillingEntity $billingEntity, Money $dueAmount, \DateTimeImmutable $dueDate ) { $this->number = $number; $this->billingEntity = $billingEntity; $this->issueDate = new \DateTimeImmutable('today', new \DateTimeZone('UTC')); $this->dueDate = $dueDate; $this->dueAmount = $dueAmount; $this->remainingDueAmount = clone $dueAmount; } }
  69. Issue an Invoice $invoice = new Invoice( new InvoiceId('INV-20180306-66'), new

    BillingEntity('3429234'), new Money(9990, new Currency('EUR')), new \DateTimeImmutable('+30 days') );
  70. 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. ); } }
  71. 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); } }
  72. 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') ));
  73. Treat lists as custom collection objects

  74. 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. )); } }
  75. Filtering the collection class Invoice { // ... public function

    countPaymentsReceivedAfterDueDate(): int { return $this ->payments ->filter(function (CollectedPayment $payment) { return $payment->getReceptionDate() > $this->dueDate; }) ->count(); } }
  76. 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); } }
  77. class Invoice { // … public function __construct(…) { //

    ... $this->payments = new CollectedPaymentCollection(); } public function countPaymentsReceivedAfterDueDate(): int { return $this ->payments ->receivedAfter($this->dueDate) ->count(); } }
  78. Assertions Library

  79. 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.
  80. https://github.com/beberlei/assert

  81. 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); // ...
  82. 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.'); } // ... } }
  83. 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; } // ... }
  84. -2- Domain Model Main Structures

  85. Ubiquitous Language

  86. 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
  87. 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
  88. Value Objects

  89. 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.
  90. 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
  91. 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; } }
  92. new Currency('EUR'); // OK new Currency('USD'); // OK new Currency('CAD');

    // OK new Currency('BTC'); // Error
  93. final class Money { private $amount; private $currency; public function

    __construct(int $amount, Currency $currency) { $this->amount = $amount; $this->currency = $currency; } // ... }
  94. 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'); } } }
  95. $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
  96. Entities

  97. 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.
  98. Beware of Anemic Models

  99. $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
  100. 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; } }
  101. Issue an Invoice $invoice = new Invoice( new InvoiceId('INV-20180306-66'), new

    CustomerId('3429234'), new Money(9990, new Currency('EUR')), new \DateTimeImmutable('+30 days') );
  102. 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. ); } }
  103. 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); } }
  104. 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') ));
  105. Named Constructors

  106. 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.
  107. $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);
  108. 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'), ); } }
  109. Issue an Invoice $invoice = Invoice::issue( 'INV-20180306-66', '3429234', new Money(9990,

    new Currency('EUR')) );
  110. Collections

  111. Collection Objects Simple lists structures should be encapsulated into Collection

    objects to offer dedicated manipulation operations.
  112. 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. )); } }
  113. Filtering the collection class Invoice { // ... public function

    countPaymentsReceivedAfterDueDate(): int { return $this ->payments ->filter(function (CollectedPayment $payment) { return $payment->getReceptionDate() > $this->dueDate; }) ->count(); } }
  114. 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); } }
  115. class Invoice { // … public function __construct(…) { //

    ... $this->payments = new CollectedPaymentCollection(); } public function countPaymentsReceivedAfterDueDate(): int { return $this ->payments ->receivedAfter($this->dueDate) ->count(); } }
  116. Services

  117. 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
  118. 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.
  119. https://martinfowler.com/eaaCatalog/serviceLayer.html

  120. 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)); } } }
  121. 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(), ]); } }
  122. Data Transfer Objects

  123. Data Transfer Objects An object that carries data between processes

    in order to reduce the number of method calls. — Martin Fowler
  124. 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; } }
  125. 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); } }
  126. View Model

  127. 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
  128. class InvoiceView { public $invoiceNumber; public $customerName; public $customerAddress; public

    $dueDate; public $dueAmount; public $paidAmount; public $remainingAmount; }
  129. 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; } }
  130. 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, ]); } }
  131. <html> <body> <h1>{{ invoice.number }}</h1> <p> Total due: {{ invoice.dueAmount}}<br/>

    Total paid: {{ invoice.paidAmount }}<br/> Total remaining: {{ invoice.remainingAmount }} </p> </body> </html>
  132. -4- Thank you for attending!