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

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.

Hugo Hamon

May 31, 2018
Tweet

More Decks by Hugo Hamon

Other Decks in Programming

Transcript

  1. AFUP 2018 / May. 31st / Poitiers / France Hugo Hamon
    Designing Better
    Object Oriented
    Software

    View Slide

  2. Hugo Hamon

    View Slide

  3. -1-
    Object Oriented
    Design

    View Slide

  4. Dependency
    Injection

    View Slide

  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

    View Slide

  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
    {
    // …
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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'

    View Slide

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

    View Slide

  14. Object
    Composition

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  22. SOLID
    Principles

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  44. Object
    Calisthenics

    View Slide

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

    View Slide

  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

    View Slide

  47. Wrap Primitive
    Types and
    Strings

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  53. One Level of
    Indentation per
    Method

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  57. Avoid the ELSE
    Keyword

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  62. Two Instance
    Operators per
    Line

    View Slide

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

    View Slide

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

    View Slide

  65. Avoid public
    accessors
    methods

    View Slide

  66. /!\
    Beware of
    Anemic Models

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  73. Treat lists as
    custom collection
    objects

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  78. Assertions
    Library

    View Slide

  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.

    View Slide

  80. https://github.com/beberlei/assert

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  84. -2-
    Domain Model
    Main Structures

    View Slide

  85. Ubiquitous
    Language

    View Slide

  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

    View Slide

  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

    View Slide

  88. Value Objects

    View Slide

  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.

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  96. Entities

    View Slide

  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.

    View Slide

  98. Beware of
    Anemic Models

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  105. Named
    Constructors

    View Slide

  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.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  110. Collections

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  116. Services

    View Slide

  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

    View Slide

  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.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  122. Data Transfer
    Objects

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  126. View Model

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide



  131. {{ invoice.number }}

    Total due: {{ invoice.dueAmount}}

    Total paid: {{ invoice.paidAmount }}

    Total remaining: {{ invoice.remainingAmount }}



    View Slide

  132. -4-
    Thank you for
    attending!

    View Slide