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 full-size slide

  2. -1-
    Object Oriented
    Design

    View full-size slide

  3. Dependency
    Injection

    View full-size slide

  4. 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 full-size slide

  5. 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 full-size slide

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

    View full-size slide

  7. 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 full-size slide

  8. 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 full-size slide

  9. 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 full-size slide

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

    View full-size slide

  11. 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 full-size slide

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

    View full-size slide

  13. Object
    Composition

    View full-size slide

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

    View full-size slide

  15. 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 full-size slide

  16. 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 full-size slide

  17. 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 full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

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

    View full-size slide

  21. SOLID
    Principles

    View full-size slide

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

    View full-size slide

  23. 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 full-size 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;
    }
    }
    Persistence
    Object Construction

    View full-size slide

  25. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  29. 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 full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. 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 full-size slide

  36. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  39. 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 full-size slide

  40. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  43. Object
    Calisthenics

    View full-size slide

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

    View full-size slide

  45. 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 full-size slide

  46. Wrap Primitive
    Types and
    Strings

    View full-size slide

  47. 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 full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

  50. 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 full-size slide

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

    View full-size slide

  52. One Level of
    Indentation per
    Method

    View full-size slide

  53. 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 full-size slide

  54. 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 full-size slide

  55. 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 full-size slide

  56. Avoid the ELSE
    Keyword

    View full-size slide

  57. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  61. Two Instance
    Operators per
    Line

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  64. Avoid public
    accessors
    methods

    View full-size slide

  65. /!\
    Beware of
    Anemic Models

    View full-size slide

  66. $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 full-size slide

  67. 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 full-size slide

  68. 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 full-size slide

  69. 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 full-size slide

  70. 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 full-size slide

  71. 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 full-size slide

  72. Treat lists as
    custom collection
    objects

    View full-size slide

  73. 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 full-size slide

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

    View full-size slide

  75. 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 full-size slide

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

    View full-size slide

  77. Assertions
    Library

    View full-size slide

  78. 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 full-size slide

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

    View full-size slide

  80. 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 full-size slide

  81. 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 full-size slide

  82. 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 full-size slide

  83. -2-
    Domain Model
    Main Structures

    View full-size slide

  84. Ubiquitous
    Language

    View full-size slide

  85. 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 full-size slide

  86. 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 full-size slide

  87. Value Objects

    View full-size slide

  88. 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 full-size slide

  89. 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 full-size slide

  90. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  93. 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 full-size slide

  94. $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 full-size slide

  95. 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 full-size slide

  96. Beware of
    Anemic Models

    View full-size slide

  97. $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 full-size slide

  98. 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 full-size slide

  99. 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 full-size slide

  100. 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 full-size slide

  101. 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 full-size slide

  102. 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 full-size slide

  103. Named
    Constructors

    View full-size slide

  104. 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 full-size slide

  105. $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 full-size slide

  106. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  109. 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 full-size slide

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

    View full-size slide

  111. 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 full-size slide

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

    View full-size slide

  113. 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 full-size slide

  114. 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 full-size slide

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

    View full-size slide

  116. 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 full-size slide

  117. 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 full-size slide

  118. Data Transfer
    Objects

    View full-size slide

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

    View full-size slide

  120. 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 full-size slide

  121. 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 full-size slide

  122. 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 full-size slide

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

    View full-size slide

  124. 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 full-size slide

  125. 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 full-size slide



  126. {{ invoice.number }}

    Total due: {{ invoice.dueAmount}}

    Total paid: {{ invoice.paidAmount }}

    Total remaining: {{ invoice.remainingAmount }}



    View full-size slide

  127. -4-
    Thank you for
    attending!

    View full-size slide