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

The absolute beginner’s guide to DDD with Symfony

nealio82
November 20, 2020

The absolute beginner’s guide to DDD with Symfony

DDD is one of the most effective ways to address business problems, but it can be overwhelming to get started with. Even armed with the theory, beginning the journey is mysterious and finding answers to specific implementation details can be tough and scary.

In this talk we'll demystify the terminology and work out the most important aspects of DDD you'll need to get you going.

Then, by exploring and implementing a sample business domain, we'll discover such things as:
- How DDD entities differ from Doctrine entities
- What an aggregate root actually is
- How to organise your code
- How to define a bounded context
- How a domain service differs from an application service

Finally, we'll wire it all together with Symfony 5. So if you've been wanting to get started with DDD for a while but never knew how or where to start then this talk is for you.

nealio82

November 20, 2020
Tweet

More Decks by nealio82

Other Decks in Programming

Transcript

  1. The absolute beginner’s
    guide
    to Domain-Driven Design with Symfony

    View Slide

  2. What’s the point?

    View Slide

  3. What’s the point?

    View Slide

  4. What’s the point?
    Tackling complexity in the
    heart of software

    View Slide

  5. What’s the point?

    View Slide

  6. What’s the point?

    View Slide

  7. What’s the point?

    View Slide

  8. View Slide

  9. Domain-Driven
    Design
    Beginner’s Guide

    View Slide

  10. View Slide

  11. View Slide

  12. What is Domain-Driven Design?

    View Slide

  13. DDD is an approach to
    understanding the business in
    order to represent it in software,
    and also an architectural pattern
    for simplifying business problems

    View Slide

  14. Definitions & revision

    View Slide

  15. Domain

    View Slide

  16. Domain Experts

    View Slide

  17. Ubiquitous Language

    View Slide

  18. The software must use the
    same words as the business

    View Slide

  19. Property

    View Slide

  20. Instruction

    View Slide

  21. Instruction
    Property

    View Slide

  22. Bounded Contexts

    View Slide

  23. View Slide

  24. View Slide

  25. View Slide

  26. A Bounded Context is a logical
    boundary around some
    behaviour within the business

    View Slide

  27. Entities

    View Slide

  28. A DDD entity is a class which
    has some sort of identity,
    making it unique from other
    identical objects

    View Slide

  29. Aggregates

    View Slide

  30. An aggregate is a collection of
    objects that represent one
    concept in the domain

    View Slide

  31. Aggregate Roots

    View Slide

  32. The aggregate root is the
    entry-point into the aggregate

    View Slide

  33. Invariants

    View Slide

  34. Invariants are business rules
    which are enforced in the
    domain code.
    They are separate to
    application-level rules.

    View Slide

  35. Domain Services and Application Services

    View Slide

  36. Domain services contain domain
    logic which doesn’t naturally fit
    within an aggregate, entity, or
    value object

    View Slide

  37. Application services orchestrate
    the execution of domain logic
    according to outside-world
    input, but don’t contain any
    domain logic themselves

    View Slide

  38. Architecture

    View Slide

  39. 4 layers of a DDD architecture
    Each bounded context contains:
    ● Domain / Model
    ● Infrastructure
    ● Application
    ● UI

    View Slide

  40. 4 layers of a DDD architecture
    Each bounded context contains:
    ● Domain / Model
    ● Infrastructure
    ● Application
    ● UI
    Contains all business logic / invariants, entities,
    domain services, and interfaces to allow
    communication with the outside world.
    Must not depend on anything outside of the
    domain layer.

    View Slide

  41. 4 layers of a DDD architecture
    Each bounded context contains:
    ● Domain / Model
    ● Infrastructure
    ● Application
    ● UI
    Contains concretions of the interfaces defined in
    the Domain layer.
    Can only depend on items in the domain layer.

    View Slide

  42. 4 layers of a DDD architecture
    Each bounded context contains:
    ● Domain / Model
    ● Infrastructure
    ● Application
    ● UI
    Provides ‘application services’ to facilitate
    communication to the Domain layer from the UI.
    May call domain services. Cannot contain
    business logic, but can perform validation.
    Can only depend on the domain layer and
    infrastructure layer

    View Slide

  43. 4 layers of a DDD architecture
    Each bounded context contains:
    ● Domain / Model
    ● Infrastructure
    ● Application
    ● UI
    UI layer (controllers, console commands, etc)
    Can only talk to the application layer

    View Slide

  44. Domain-Driven
    Design
    Getting started

    View Slide

  45. 4 layers of a DDD architecture
    Each bounded context contains:
    ● Domain / Model
    ● Infrastructure
    ● Application
    ● UI
    Let’s simplify things by sharing the UI layer across
    contexts
    Symfony’s App namespace is a pre-made UI layer
    we can take advantage of
    We can use Symfony’s controllers and commands
    to keep things feeling familiar

    View Slide

  46. View Slide

  47. View Slide

  48. View Slide

  49. Preparation

    View Slide

  50. $ symfony new --full \
    absolute-beginners-guide-to-ddd-with-symfony

    View Slide

  51. public
    src
    config
    templates
    tests
    composer.json

    View Slide

  52. public
    src
    config
    templates
    tests
    composer.json
    Kernel.php
    Controller
    Entity
    Repository

    View Slide

  53. public
    src
    config
    App
    templates
    tests
    composer.json
    Kernel.php
    Controller
    Entity
    Repository

    View Slide

  54. protected function configureContainer
    (ContainerConfigurator $container
    ): void
    {
    $container
    ->import('../../config/{packages}/*.yaml'
    );
    $container
    ->import('../../config/{packages}/'
    .$this->environment
    .'/*.yaml');
    if (is_file(\dirname(__DIR__).'/../config/services.yaml'
    )) {
    $container
    ->import('../../config/services.yaml'
    );
    $container
    ->import('../../config/{services}_'
    .$this->environment
    .'.yaml');
    } elseif (is_file($path = \dirname(__DIR__).'/../config/services.php'
    )) {
    (require $path)($container
    ->withPath($path), $this);
    }
    }
    protected function configureRoutes
    (RoutingConfigurator $routes): void
    {
    $routes->import('../../config/{routes}/'
    .$this->environment
    .'/*.yaml');
    $routes->import('../../config/{routes}/*.yaml'
    );
    if (is_file(\dirname(__DIR__).'/../config/routes.yaml'
    )) {
    $routes->import('../../config/routes.yaml'
    );
    } elseif (is_file($path = \dirname(__DIR__).'/../config/routes.php'
    )) {
    (require $path)($routes->withPath($path), $this);
    }
    }
    public
    src
    config
    App
    templates
    tests
    composer.json
    Kernel.php
    Controller
    Entity
    Repository

    View Slide

  55. "autoload"
    : {
    "psr-4": {
    "App\\": "src/App/"
    }
    },
    public
    src
    config
    App
    templates
    tests
    Kernel.php
    Controller
    Entity
    Repository
    composer.json

    View Slide

  56. services:
    # default configuration for services in *this* file
    _defaults:
    autowire: true # Automatically injects dependencies in your services.
    autoconfigure
    : true # Automatically registers your services as commands, event subscribers, etc.
    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
    resource: '../src/
    App/'
    exclude:
    - '../src/
    App/DependencyInjection/'
    - '../src/
    App/Entity/'
    - '../src/
    App/Kernel.php'
    - '../src/
    App/Tests/'
    # controllers are imported separately to make sure services can be injected
    # as action arguments even if you don't extend any base controller class
    App\Controller\
    :
    resource: '../src/
    App/Controller/'
    tags: ['controller.service_arguments']
    public
    src
    config
    App
    templates
    tests
    Kernel.php
    Controller
    Entity
    Repository
    services.yaml
    composer.json

    View Slide

  57. doctrine:
    dbal:
    url: '%env(resolve:DATABASE_URL)%'
    # IMPORTANT: You MUST configure your server version,
    # either here or in the DATABASE_URL env var (see .env file)
    #server_version: '5.7'
    orm:
    auto_generate_proxy_classes
    : true
    naming_strategy
    : doctrine.orm.naming_strategy.underscore_number_aware
    auto_mapping
    : true
    mappings:
    App:
    is_bundle: false
    type: annotation
    dir: '%kernel.project_dir%/src/
    App/Entity'
    prefix: 'App\Entity'
    alias: App
    public
    src
    config
    App
    templates
    tests
    Kernel.php
    Controller
    Entity
    Repository
    doctrine.yaml
    composer.json
    packages

    View Slide

  58. controllers
    :
    resource: ../../src/
    App/Controller/
    type: annotation
    kernel:
    resource: ../../src/
    App/Kernel.php
    type: annotation
    public
    src
    config
    App
    templates
    tests
    Kernel.php
    Controller
    Entity
    Repository
    annotations.yaml
    composer.json
    routes
    packages

    View Slide

  59. The First Bounded Context

    View Slide

  60. "autoload"
    : {
    "psr-4": {
    "App\\": "src/App/"
    ,
    "AcmeVet\\"
    : "src/AcmeVet/"
    }
    },
    "autoload-dev"
    : {
    "psr-4": {
    "App\\Tests\\": "tests/"
    }
    },
    public
    src
    config
    AcmeVet
    templates
    tests
    composer.json
    App

    View Slide

  61. public
    src
    AcmeVet
    config
    Scheduling
    App
    composer.json
    templates
    tests
    Bounded Context

    View Slide

  62. public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    App
    composer.json
    templates
    tests
    Bounded Context

    View Slide

  63. Build the aggregate

    View Slide

  64. public
    src
    AcmeVet
    config
    Scheduling
    Domain
    composer.json
    AppointmentTest.php
    Appointment
    templates
    tests

    View Slide

  65. namespace Tests\AcmeVet\Scheduling
    \Domain\Appointment
    ;
    class AppointmentTest extends TestCase
    {
    public function test_an_appointment_can_be_created
    (): void
    {
    }
    public function test_a_double_appointment_can_be_created
    (): void
    {
    }
    public function test_an_appointment_cannot_be_longer_than_a_double_slot
    (): void
    {
    }
    public function test_an_appointment_must_be_longer_than_zero_minutes_long
    (): void
    {
    }
    public function test_an_appointment_must_be_a_multiple_of_fifteen_minutes
    (): void
    {
    }
    }
    public
    src
    AcmeVet
    config
    Scheduling
    Domain
    composer.json
    AppointmentTest.php
    Appointment
    templates
    tests

    View Slide

  66. namespace Tests\AcmeVet\Scheduling
    \Domain\Appointment
    ;
    class AppointmentTest extends TestCase
    {
    public function test_an_appointment_can_be_created
    (): void
    {
    $appointmentId = AppointmentId
    ::generate();
    $pet = Pet::create(
    'Luna',
    'David Smith'
    ,
    '07007771234'
    );
    $startTime = new \DateTimeImmutable
    ();
    $appointmentLength = 15;
    $appointment = Appointment
    ::create(
    $appointmentId,
    $pet,
    $startTime,
    $appointmentLength
    );
    static::assertSame
    ($appointmentId, $appointment
    ->getId());
    static::assertSame
    ($pet, $appointment
    ->getPet());
    static::assertSame
    ($startTime, $appointment
    ->getStartTime
    ());
    static::assertSame
    ($appointmentLength, $appointment
    ->getLengthInMinutes
    ());
    }
    }
    public
    src
    AcmeVet
    config
    Scheduling
    Domain
    composer.json
    AppointmentTest.php
    Appointment
    templates
    tests

    View Slide

  67. namespace Tests\AcmeVet\Scheduling
    \Domain\Appointment
    ;
    class AppointmentTest extends TestCase
    {
    public function test_an_appointment_can_be_created
    (): void
    {
    $appointmentId = AppointmentId
    ::generate();
    $pet = Pet::create(
    'Luna',
    'David Smith'
    ,
    '07007771234'
    );
    $startTime = new \DateTimeImmutable
    ();
    $appointmentLength = 15;
    $appointment = Appointment
    ::create(
    $appointmentId,
    $pet,
    $startTime,
    $appointmentLength
    );
    static::assertSame
    ($appointmentId, $appointment
    ->getId());
    static::assertSame
    ($pet, $appointment
    ->getPet());
    static::assertSame
    ($startTime, $appointment
    ->getStartTime
    ());
    static::assertSame
    ($appointmentLength, $appointment
    ->getLengthInMinutes
    ());
    }
    }
    public
    src
    AcmeVet
    config
    Scheduling
    Domain
    composer.json
    AppointmentTest.php
    Appointment
    templates
    tests

    View Slide

  68. namespace Tests\AcmeVet\Scheduling
    \Domain\Appointment
    ;
    class AppointmentTest extends TestCase
    {
    public function test_an_appointment_can_be_created
    (): void
    {
    $appointmentId = AppointmentId
    ::generate();
    $pet = Pet::create(
    'Luna',
    'David Smith'
    ,
    '07007771234'
    );
    $startTime = new \DateTimeImmutable
    ();
    $appointmentLength = 15;
    $appointment = Appointment
    ::create(
    $appointmentId,
    $pet,
    $startTime,
    $appointmentLength
    );
    static::assertSame
    ($appointmentId, $appointment
    ->getId());
    static::assertSame
    ($pet, $appointment
    ->getPet());
    static::assertSame
    ($startTime, $appointment
    ->getStartTime
    ());
    static::assertSame
    ($appointmentLength, $appointment
    ->getLengthInMinutes
    ());
    }
    }
    public
    src
    AcmeVet
    config
    Scheduling
    Domain
    composer.json
    AppointmentTest.php
    Appointment
    templates
    tests

    View Slide

  69. namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    use AcmeVet\Appointment
    \Domain\Exception\AppointmentLengthInvalid
    ;
    class Appointment
    {
    private function __construct(
    AppointmentId $appointmentId
    ,
    Pet $pet,
    \DateTimeImmutable $appointmentTime
    ,
    int $appointmentLengthInMinutes
    )
    {
    $this
    ->appointmentId = $appointmentId
    ;
    $this
    ->pet = $pet;
    $this
    ->appointmentTime = $appointmentTime
    ;
    $this
    ->appointmentLengthInMinutes = $appointmentLengthInMinutes
    ;
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Appointment.php
    Scheduling
    Application
    Domain
    Appointment
    composer.json
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  70. public
    src
    AcmeVet
    config
    Infrastructure
    Appointment.php
    namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    use AcmeVet\Appointment
    \Domain\Exception\AppointmentLengthInvalid
    ;
    class Appointment
    {
    private function __construct
    (
    AppointmentId $appointmentId
    ,
    Pet $pet,
    \DateTimeImmutable $appointmentTime
    ,
    int $appointmentLengthInMinutes
    )
    {
    $this
    ->appointmentId = $appointmentId
    ;
    $this
    ->pet = $pet;
    $this
    ->appointmentTime = $appointmentTime
    ;
    $this
    ->appointmentLengthInMinutes = $appointmentLengthInMinutes
    ;
    }
    }
    Scheduling
    Application
    Domain
    Appointment
    App
    composer.json
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  71. public static function create(
    AppointmentId $appointmentId
    ,
    Pet $pet,
    \DateTimeImmutable $appointmentTime
    ,
    int $appointmentLength
    ): self
    {
    if (self::STANDARD_APPOINTMENT_LENGTH > $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The minimum appointment length is %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    $maxLength = self::STANDARD_APPOINTMENT_LENGTH * 2;
    if ($maxLength < $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The maximum appointment length is %s minutes"
    , $maxLength)
    );
    }
    if (0 !== $appointmentLength % self::STANDARD_APPOINTMENT_LENGTH
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The appointment length must be a multiple of %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    return new self
    ($appointmentId
    , $pet, $appointmentTime
    , $appointmentLength
    );
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Appointment.php
    Scheduling
    Application
    Domain
    Appointment
    composer.json
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  72. public static function create(
    AppointmentId $appointmentId
    ,
    Pet $pet,
    \DateTimeImmutable $appointmentTime
    ,
    int $appointmentLength
    ): self
    {
    if (self::STANDARD_APPOINTMENT_LENGTH > $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The minimum appointment length is %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    $maxLength = self::STANDARD_APPOINTMENT_LENGTH * 2;
    if ($maxLength < $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The maximum appointment length is %s minutes"
    , $maxLength)
    );
    }
    if (0 !== $appointmentLength % self::STANDARD_APPOINTMENT_LENGTH
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The appointment length must be a multiple of %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    return new self
    ($appointmentId
    , $pet, $appointmentTime
    , $appointmentLength
    );
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Appointment.php
    Scheduling
    Application
    Domain
    Appointment
    composer.json
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  73. public static function create(
    AppointmentId $appointmentId
    ,
    Pet $pet,
    \DateTimeImmutable $appointmentTime
    ,
    int $appointmentLength
    ): self
    {
    if (self::STANDARD_APPOINTMENT_LENGTH > $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The minimum appointment length is %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    $maxLength = self::STANDARD_APPOINTMENT_LENGTH * 2;
    if ($maxLength < $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The maximum appointment length is %s minutes"
    , $maxLength)
    );
    }
    if (0 !== $appointmentLength % self::STANDARD_APPOINTMENT_LENGTH
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The appointment length must be a multiple of %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    return new self
    ($appointmentId
    , $pet, $appointmentTime
    , $appointmentLength
    );
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Appointment.php
    Scheduling
    Application
    Domain
    Appointment
    composer.json
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  74. public static function create(
    AppointmentId $appointmentId
    ,
    Pet $pet,
    \DateTimeImmutable $appointmentTime
    ,
    int $appointmentLength
    ): self
    {
    if (self::STANDARD_APPOINTMENT_LENGTH > $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The minimum appointment length is %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    $maxLength = self::STANDARD_APPOINTMENT_LENGTH * 2;
    if ($maxLength < $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The maximum appointment length is %s minutes"
    , $maxLength)
    );
    }
    if (0 !== $appointmentLength % self::STANDARD_APPOINTMENT_LENGTH
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The appointment length must be a multiple of %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    return new self
    ($appointmentId
    , $pet, $appointmentTime
    , $appointmentLength
    );
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Appointment.php
    Scheduling
    Application
    Domain
    Appointment
    composer.json
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  75. public static function create(
    AppointmentId $appointmentId
    ,
    Pet $pet,
    \DateTimeImmutable $appointmentTime
    ,
    int $appointmentLength
    ): self
    {
    if (self::STANDARD_APPOINTMENT_LENGTH > $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The minimum appointment length is %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    $maxLength = self::STANDARD_APPOINTMENT_LENGTH * 2;
    if ($maxLength < $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The maximum appointment length is %s minutes"
    , $maxLength)
    );
    }
    if (0 !== $appointmentLength % self::STANDARD_APPOINTMENT_LENGTH
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The appointment length must be a multiple of %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    return new self
    ($appointmentId
    , $pet, $appointmentTime
    , $appointmentLength
    );
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Appointment.php
    Scheduling
    Application
    Domain
    Appointment
    composer.json
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  76. public static function create(
    AppointmentId $appointmentId
    ,
    Pet $pet,
    \DateTimeImmutable $appointmentTime
    ,
    int $appointmentLength
    ): self
    {
    if (self::STANDARD_APPOINTMENT_LENGTH > $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The minimum appointment length is %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    $maxLength = self::STANDARD_APPOINTMENT_LENGTH * 2;
    if ($maxLength < $appointmentLength
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The maximum appointment length is %s minutes"
    , $maxLength)
    );
    }
    if (0 !== $appointmentLength % self::STANDARD_APPOINTMENT_LENGTH
    ) {
    throw new AppointmentLengthInvalid
    (
    sprintf("The appointment length must be a multiple of %s minutes"
    , self::STANDARD_APPOINTMENT_LENGTH
    )
    );
    }
    return new self
    ($appointmentId
    , $pet, $appointmentTime
    , $appointmentLength
    );
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Appointment.php
    Scheduling
    Application
    Domain
    Appointment
    composer.json
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  77. namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    use Ramsey\Uuid\Uuid;
    class AppointmentId
    {
    private string $id;
    private function __construct
    (string $id)
    {
    $this
    ->id = $id;
    }
    public static function generate(): self
    {
    return new self
    (Uuid::uuid4()->toString());
    }
    public static function fromString
    (string $id): self
    {
    if (false === Uuid::isValid($id)) {
    throw new \DomainException
    (
    \sprintf("AppointmentId '%s' is not valid"
    , $id)
    );
    }
    return new self
    ($id);
    }
    public function toString(): string
    {
    return $this->id;
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    Appointment
    composer.json
    AppointmentId.php
    Appointment.php
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  78. namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    use Ramsey\Uuid\Uuid;
    class AppointmentId
    {
    private string $id;
    private function __construct
    (string $id)
    {
    $this
    ->id = $id;
    }
    public static function generate(): self
    {
    return new self
    (Uuid::uuid4()->toString());
    }
    public static function fromString
    (string $id): self
    {
    if (false === Uuid::isValid($id)) {
    throw new \DomainException
    (
    \sprintf("AppointmentId '%s' is not valid"
    , $id)
    );
    }
    return new self
    ($id);
    }
    public function toString(): string
    {
    return $this->id;
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    Appointment
    composer.json
    AppointmentId.php
    Appointment.php
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  79. class Pet
    {
    private string $name;
    private string $ownerName
    ;
    private string $contactNumber
    ;
    private function __construct
    (
    string $name,
    string $ownerName
    ,
    string $contactNumber
    ) {
    $this
    ->name = $name;
    $this
    ->ownerName = $ownerName
    ;
    $this
    ->contactNumber = $contactNumber
    ;
    }
    public static function create(
    string $name,
    string $ownerName
    ,
    string $contactNumber
    ) {
    return new self
    ($name, $ownerName
    , $contactNumber
    );
    }
    public function getName(): string
    {
    return $this->name;
    }
    public function getOwnerName
    (): string
    {
    return $this->ownerName;
    }
    public function getContactNumber
    (): string
    {
    return $this->contactNumber
    ;
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    composer.json
    AppointmentId.php
    Pet.php
    Appointment.php
    App
    templates
    tests
    Appointment
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  80. namespace AcmeVet\Scheduling
    \Domain\Appointment
    \Exception;
    class AppointmentLengthInvalid extends \DomainException
    {
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    Exception
    composer.json
    AppointmentId.php
    Pet.php
    Appointment.php
    AppointmentLengthInvalid.php
    App
    templates
    tests
    Appointment
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  81. Create a domain service

    View Slide

  82. namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    namespace AcmeVet\Scheduling
    \Domain\Appointment
    \Exception\CouldNotConfirmSlotException
    ;
    class SlotConfirmationService
    {
    private AppointmentRepository $appointmentRepository
    ;
    public function __construct
    (
    AppointmentRepository $appointmentRepository
    ) {
    $this
    ->appointmentRepository = $appointmentRepository
    ;
    }
    public function confirmSlot
    (Appointment $appointment
    ): void
    {
    $this
    ->checkStartTimeIsNotInThePast
    ($appointment
    );
    $this
    ->checkSlotStartTimeIsNotTaken
    ($appointment
    );
    $this
    ->checkSlotIsNotTakenByADoubleLengthBooking
    ($appointment
    );
    $this
    ->checkSlotDoesNotSpanAnotherBooking
    ($appointment
    );
    $this
    ->appointmentRepository
    ->store($appointment
    );
    }
    private function checkStartTimeIsNotInThePast
    (Appointment $appointment
    ): void
    {
    $dateDiff = $appointment
    ->getStartTime
    ()->diff(new \DateTimeImmutable
    ());
    if (
    $appointment
    ->getStartTime
    () < new \DateTimeImmutable
    ()
    && 0 !== (int) $dateDiff->format('%y%m%d%h%i'
    )
    ) {
    throw new CouldNotConfirmSlotException
    ("The slot must not be in the past"
    );
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    Exception
    composer.json
    AppointmentId.php
    Pet.php
    Appointment.php
    SlotConfirmationService.php
    Appointment
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  83. namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    namespace AcmeVet\Scheduling
    \Domain\Appointment
    \Exception\CouldNotConfirmSlotException
    ;
    class SlotConfirmationService
    {
    private AppointmentRepository $appointmentRepository
    ;
    public function __construct
    (
    AppointmentRepository $appointmentRepository
    ) {
    $this
    ->appointmentRepository = $appointmentRepository
    ;
    }
    public function confirmSlot
    (Appointment $appointment
    ): void
    {
    $this
    ->checkStartTimeIsNotInThePast
    ($appointment
    );
    $this
    ->checkSlotStartTimeIsNotTaken
    ($appointment
    );
    $this
    ->checkSlotIsNotTakenByADoubleLengthBooking
    ($appointment
    );
    $this
    ->checkSlotDoesNotSpanAnotherBooking
    ($appointment
    );
    $this
    ->appointmentRepository
    ->store($appointment
    );
    }
    private function checkStartTimeIsNotInThePast
    (Appointment $appointment
    ): void
    {
    $dateDiff = $appointment
    ->getStartTime
    ()->diff(new \DateTimeImmutable
    ());
    if (
    $appointment
    ->getStartTime
    () < new \DateTimeImmutable
    ()
    && 0 !== (int) $dateDiff->format('%y%m%d%h%i'
    )
    ) {
    throw new CouldNotConfirmSlotException
    ("The slot must not be in the past"
    );
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    Exception
    composer.json
    AppointmentId.php
    Pet.php
    Appointment.php
    SlotConfirmationService.php
    Appointment
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  84. namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    namespace AcmeVet\Scheduling
    \Domain\Appointment
    \Exception\CouldNotConfirmSlotException
    ;
    class SlotConfirmationService
    {
    private AppointmentRepository $appointmentRepository
    ;
    public function __construct
    (
    AppointmentRepository $appointmentRepository
    ) {
    $this
    ->appointmentRepository = $appointmentRepository
    ;
    }
    public function confirmSlot
    (Appointment $appointment
    ): void
    {
    $this
    ->checkStartTimeIsNotInThePast
    ($appointment
    );
    $this
    ->checkSlotStartTimeIsNotTaken
    ($appointment
    );
    $this
    ->checkSlotIsNotTakenByADoubleLengthBooking
    ($appointment
    );
    $this
    ->checkSlotDoesNotSpanAnotherBooking
    ($appointment
    );
    $this
    ->appointmentRepository
    ->store($appointment
    );
    }
    private function checkStartTimeIsNotInThePast
    (Appointment $appointment
    ): void
    {
    $dateDiff = $appointment
    ->getStartTime
    ()->diff(new \DateTimeImmutable
    ());
    if (
    $appointment
    ->getStartTime
    () < new \DateTimeImmutable
    ()
    && 0 !== (int) $dateDiff->format('%y%m%d%h%i'
    )
    ) {
    throw new CouldNotConfirmSlotException
    ("The slot must not be in the past"
    );
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    Exception
    composer.json
    AppointmentId.php
    Pet.php
    Appointment.php
    SlotConfirmationService.php
    Appointment
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  85. namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    namespace AcmeVet\Scheduling
    \Domain\Appointment
    \Exception\CouldNotConfirmSlotException
    ;
    class SlotConfirmationService
    {
    private AppointmentRepository $appointmentRepository
    ;
    public function __construct
    (
    AppointmentRepository $appointmentRepository
    ) {
    $this
    ->appointmentRepository = $appointmentRepository
    ;
    }
    public function confirmSlot
    (Appointment $appointment
    ): void
    {
    $this
    ->checkStartTimeIsNotInThePast
    ($appointment
    );
    $this
    ->checkSlotStartTimeIsNotTaken
    ($appointment
    );
    $this
    ->checkSlotIsNotTakenByADoubleLengthBooking
    ($appointment
    );
    $this
    ->checkSlotDoesNotSpanAnotherBooking
    ($appointment
    );
    $this
    ->appointmentRepository
    ->store($appointment
    );
    }
    private function checkStartTimeIsNotInThePast
    (Appointment $appointment
    ): void
    {
    $dateDiff = $appointment
    ->getStartTime
    ()->diff(new \DateTimeImmutable
    ());
    if (
    $appointment
    ->getStartTime
    () < new \DateTimeImmutable
    ()
    && 0 !== (int) $dateDiff->format('%y%m%d%h%i'
    )
    ) {
    throw new CouldNotConfirmSlotException
    ("The slot must not be in the past"
    );
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    Exception
    composer.json
    AppointmentId.php
    Pet.php
    Appointment.php
    SlotConfirmationService.php
    Appointment
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  86. namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    interface AppointmentRepository
    {
    public function getAppointmentAtTime
    (\DateTimeImmutable $appointmentTime
    ): ?Appointment
    ;
    public function store(Appointment $appointment
    ): void;
    /**
    * @return Appointment[]
    */
    public function getAll(): array;
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    Exception
    composer.json
    AppointmentId.php
    Pet.php
    Appointment.php
    AppointmentRepository.php
    Appointment
    SlotConfirmationService.php
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  87. namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    interface AppointmentRepository
    {
    public function getAppointmentAtTime
    (\DateTimeImmutable $appointmentTime
    ): ?Appointment
    ;
    public function store(Appointment $appointment
    ): void;
    /**
    * @return Appointment[]
    */
    public function getAll(): array;
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    Exception
    composer.json
    AppointmentId.php
    Pet.php
    Appointment.php
    AppointmentRepository.php
    Appointment
    SlotConfirmationService.php
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  88. namespace AcmeVet\Scheduling
    \Domain\Appointment
    ;
    interface AppointmentRepository
    {
    public function getAppointmentAtTime
    (\DateTimeImmutable $appointmentTime
    ): ?Appointment
    ;
    public function store(Appointment $appointment
    ): void;
    /**
    * @return Appointment[]
    */
    public function getAll(): array;
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    Exception
    composer.json
    AppointmentId.php
    Pet.php
    Appointment.php
    AppointmentRepository.php
    Appointment
    SlotConfirmationService.php
    App
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root

    View Slide

  89. namespace Tests\AcmeVet\Scheduling
    \Domain\Appointment
    ;
    class SlotConfirmationServiceTest extends TestCase
    {
    public function test_a_slot_can_be_confirmed
    (): void
    {
    $repository = new InMemoryAppointmentRepository
    ();
    $service = new SlotConfirmationService
    ($repository);
    $dateTime = new \DateTimeImmutable
    ();
    $appointment = Appointment
    ::create(
    AppointmentId
    ::generate(),
    Pet::create('Luna', 'David Smith'
    , '07500777123'
    ),
    $dateTime,
    15
    );
    $service
    ->confirmSlot
    ($appointment);
    static::assertSame
    (
    $appointment,
    $repository
    ->getAppointmentAtTime
    ($dateTime)
    );
    }
    public function test_an_appointment_cannot_be_made_in_the_past
    (): void
    {
    }
    public
    src
    AcmeVet
    config
    Scheduling
    Domain
    templates
    tests
    composer.json
    SlotConfirmationServiceTest.php
    Appointment
    AppointmentTest.php

    View Slide

  90. namespace Tests\AcmeVet\Scheduling
    \Domain\Appointment
    ;
    class SlotConfirmationServiceTest extends TestCase
    {
    public function test_a_slot_can_be_confirmed
    (): void
    {
    $repository = new InMemoryAppointmentRepository
    ();
    $service = new SlotConfirmationService
    ($repository);
    $dateTime = new \DateTimeImmutable
    ();
    $appointment = Appointment
    ::create(
    AppointmentId
    ::generate(),
    Pet::create('Luna', 'David Smith'
    , '07500777123'
    ),
    $dateTime,
    15
    );
    $service
    ->confirmSlot
    ($appointment);
    static::assertSame
    (
    $appointment,
    $repository
    ->getAppointmentAtTime
    ($dateTime)
    );
    }
    public function test_an_appointment_cannot_be_made_in_the_past
    (): void
    {
    }
    public
    src
    AcmeVet
    config
    Scheduling
    Domain
    composer.json
    SlotConfirmationServiceTest.php
    Appointment
    AppointmentTest.php
    templates
    tests

    View Slide

  91. namespace Tests\AcmeVet\Scheduling
    \Doubles;
    use AcmeVet\Appointment
    \Domain\Appointment
    ;
    use AcmeVet\Appointment
    \Domain\AppointmentRepository
    ;
    class InMemoryAppointmentRepository implements AppointmentRepository
    {
    private array $appointments = [];
    public function getAppointmentAtTime
    (
    \DateTimeImmutable $appointmentTime
    ): ?Appointment {
    // …
    }
    public function store(Appointment $appointment
    ): void
    {
    $this
    ->appointments
    [] = $appointment
    ;
    }
    public function getAll(): array
    {
    return $this->appointments
    ;
    }
    }
    public
    src
    AcmeVet
    config
    Scheduling
    Domain
    composer.json
    InMemoryAppointmentRepository.php
    Appointment
    AppointmentTest.php
    Doubles
    SlotConfirmationService.php
    templates
    tests

    View Slide

  92. Infrastructure

    View Slide

  93. namespace AcmeVet\Scheduling
    \Infrastructure
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \Appointment
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \AppointmentId
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \AppointmentRepository
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \Pet;
    use Doctrine\DBAL\Connection
    ;
    class DbalAppointmentRepository implements AppointmentRepository
    {
    private const DATE_FORMAT = "Y-m-d\TH:i"
    ;
    private Connection $connection
    ;
    public function __construct
    (Connection $connection
    )
    {
    $this
    ->connection = $connection
    ;
    }
    public function getAppointmentAtTime
    (
    \DateTimeImmutable $appointmentTime
    ): ?Appointment {
    $stmt = $this->connection
    ->prepare('
    SELECT * FROM appointments WHERE start_time = :start_time
    ');
    $stmt
    ->bindValue('start_time'
    , $appointmentTime
    ->format(self::DATE_FORMAT
    ));
    $stmt
    ->execute();
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    composer.json
    DbalAppointmentRepository.php
    App
    templates
    tests
    Bounded Context

    View Slide

  94. namespace AcmeVet\Scheduling
    \Infrastructure
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \Appointment
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \AppointmentId
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \AppointmentRepository
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \Pet;
    use Doctrine\DBAL\Connection
    ;
    class DbalAppointmentRepository implements AppointmentRepository
    {
    private const DATE_FORMAT = "Y-m-d\TH:i"
    ;
    private Connection $connection
    ;
    public function __construct
    (Connection $connection
    )
    {
    $this
    ->connection = $connection
    ;
    }
    public function getAppointmentAtTime
    (
    \DateTimeImmutable $appointmentTime
    ): ?Appointment {
    $stmt = $this->connection
    ->prepare('
    SELECT * FROM appointments WHERE start_time = :start_time
    ');
    $stmt
    ->bindValue('start_time'
    , $appointmentTime
    ->format(self::DATE_FORMAT
    ));
    $stmt
    ->execute();
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    DbalAppointmentRepository.php
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  95. namespace AcmeVet\Scheduling
    \Infrastructure
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \Appointment
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \AppointmentId
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \AppointmentRepository
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \Pet;
    use Doctrine\DBAL\Connection
    ;
    class DbalAppointmentRepository implements AppointmentRepository
    {
    private const DATE_FORMAT = "Y-m-d\TH:i"
    ;
    private Connection $connection
    ;
    public function __construct
    (Connection $connection
    )
    {
    $this
    ->connection = $connection
    ;
    }
    public function getAppointmentAtTime
    (
    \DateTimeImmutable $appointmentTime
    ): ?Appointment {
    $stmt = $this->connection
    ->prepare('
    SELECT * FROM appointments WHERE start_time = :start_time
    ');
    $stmt
    ->bindValue('start_time'
    , $appointmentTime
    ->format(self::DATE_FORMAT
    ));
    $stmt
    ->execute();
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    DbalAppointmentRepository.php
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  96. namespace AcmeVet\Scheduling
    \Infrastructure
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \Appointment
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \AppointmentId
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \AppointmentRepository
    ;
    use AcmeVet\Scheduling
    \Domain\Appointment
    \Pet;
    use Doctrine\DBAL\Connection
    ;
    class DbalAppointmentRepository implements AppointmentRepository
    {
    private const DATE_FORMAT = "Y-m-d\TH:i"
    ;
    private Connection $connection
    ;
    public function __construct
    (Connection $connection
    )
    {
    $this
    ->connection = $connection
    ;
    }
    public function getAppointmentAtTime
    (
    \DateTimeImmutable $appointmentTime
    ): ?Appointment {
    $stmt = $this->connection
    ->prepare('
    SELECT * FROM appointments WHERE start_time = :start_time
    ');
    $stmt
    ->bindValue('start_time'
    , $appointmentTime
    ->format(self::DATE_FORMAT
    ));
    $stmt
    ->execute();
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    DbalAppointmentRepository.php
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  97. public function getAppointmentAtTime
    (
    \DateTimeImmutable $appointmentTime
    ): ?Appointment {
    $stmt = $this->connection
    ->prepare('
    SELECT * FROM appointments WHERE start_time = :start_time
    ');
    $stmt
    ->bindValue('start_time'
    , $appointmentTime
    ->format(self::DATE_FORMAT
    ));
    $stmt
    ->execute();
    $result = $stmt->fetchAssociative
    ();
    if (!$result) {
    return null
    ;
    }
    return Appointment
    ::create(
    AppointmentId
    ::fromString
    ($result['id']),
    Pet::create(
    $result[
    'pet_name'
    ],
    $result[
    'owner_name'
    ],
    $result[
    'contact_number'
    ]
    ),
    \DateTimeImmutable
    ::createFromFormat
    (
    self::DATE_FORMAT
    ,
    $result['start_time'
    ]
    ),
    $result[
    'length']
    );
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    DbalAppointmentRepository.php
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  98. services:
    # ...
    # Tell Symfony to auto-wire everything that it finds
    # within the AcmeVet namespace
    AcmeVet\
    :
    resource: '../src/AcmeVet/'
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    DbalAppointmentRepository.php
    services.yaml
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  99. $ php bin/console doctrine:migrations:generate

    View Slide

  100. namespace DoctrineMigrations
    ;
    use Doctrine\DBAL\Schema\Schema;
    use Doctrine\Migrations
    \AbstractMigration
    ;
    final class Version20201107141910 extends AbstractMigration
    {
    public function getDescription
    () : string
    {
    return 'Add the table for appointments in the
    AcmeVet\Appointment context'
    ;
    }
    public function up(Schema $schema) : void
    {
    $this
    ->addSql('
    CREATE TABLE appointments (
    id CHARACTER(36) PRIMARY KEY,
    start_time VARCHAR,
    length CHARACTER(2),
    pet_name VARCHAR,
    owner_name VARCHAR,
    contact_number VARCHAR
    )
    ');
    }
    public function down(Schema $schema): void
    {
    $this
    ->addSql('DROP TABLE appointments'
    );
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    DbalAppointmentRepository.php
    Version2020110714910.php
    migrations
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  101. Application services

    View Slide

  102. View Slide

  103. class AppointmentBookingCommand implements Command
    {
    private \DateTimeImmutable $appointmentTime
    ;
    private string $petName;
    private string $ownerName
    ;
    private string $contactNumber
    ;
    private int $appointmentLengthInMinutes
    ;
    public function __construct
    (
    \DateTimeImmutable $appointmentTime
    ,
    string $petName,
    string $ownerName
    ,
    string $contactNumber
    ,
    int $appointmentLengthInMinutes
    )
    {
    $this
    ->appointmentTime = $appointmentTime
    ;
    $this
    ->petName = $petName;
    $this
    ->ownerName = $ownerName
    ;
    $this
    ->contactNumber = $contactNumber
    ;
    $this
    ->appointmentLengthInMinutes = $appointmentLengthInMinutes
    ;
    }
    public function getAppointmentTime
    (): \DateTimeImmutable
    {
    return $this->appointmentTime
    ;
    }
    public function getPetName
    (): string
    {
    return $this->petName;
    }
    public function getOwnerName
    (): string
    {
    return $this->ownerName;
    }
    public function getContactNumber
    (): string
    {
    return $this->contactNumber
    ;
    }
    public function getAppointmentLengthInMinutes
    (): int
    {
    return $this->appointmentLengthInMinutes
    ;
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    AppointmentBookingCommand.php
    Command
    Booking
    migrations
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  104. class AppointmentBookingCommand implements Command
    {
    private \DateTimeImmutable $appointmentTime
    ;
    private string $petName;
    private string $ownerName
    ;
    private string $contactNumber
    ;
    private int $appointmentLengthInMinutes
    ;
    public function __construct
    (
    \DateTimeImmutable $appointmentTime
    ,
    string $petName,
    string $ownerName
    ,
    string $contactNumber
    ,
    int $appointmentLengthInMinutes
    )
    {
    $this
    ->appointmentTime = $appointmentTime
    ;
    $this
    ->petName = $petName;
    $this
    ->ownerName = $ownerName
    ;
    $this
    ->contactNumber = $contactNumber
    ;
    $this
    ->appointmentLengthInMinutes = $appointmentLengthInMinutes
    ;
    }
    public function getAppointmentTime
    (): \DateTimeImmutable
    {
    return $this->appointmentTime
    ;
    }
    public function getPetName
    (): string
    {
    return $this->petName;
    }
    public function getOwnerName
    (): string
    {
    return $this->ownerName;
    }
    public function getContactNumber
    (): string
    {
    return $this->contactNumber
    ;
    }
    public function getAppointmentLengthInMinutes
    (): int
    {
    return $this->appointmentLengthInMinutes
    ;
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    AppointmentBookingCommand.php
    Command
    Booking
    migrations
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  105. class AppointmentBookingHandler implements Handler
    {
    private SlotConfirmationService $slotConfirmationService
    ;
    public function __construct
    (
    SlotConfirmationService $slotConfirmationService
    ) {
    $this
    ->slotConfirmationService = $slotConfirmationService
    ;
    }
    public function __invoke(Command $command): void
    {
    $appointment = Appointment
    ::create(
    AppointmentId
    ::generate(),
    Pet::create(
    $command->getPetName
    (),
    $command->getOwnerName
    (),
    $command->getContactNumber
    ()
    ),
    $command->getAppointmentTime
    (),
    $command->getAppointmentLengthInMinutes
    ()
    );
    try {
    $this
    ->slotConfirmationService
    ->confirmSlot
    ($appointment);
    } catch (CouldNotConfirmSlotException $exception) {
    throw new \RuntimeException
    ("The slot could not be booked"
    );
    }
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    AppointmentBookingCommand.php
    Command
    Booking
    AppointmentBookingHandler.php
    migrations
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  106. class AppointmentBookingHandler implements Handler
    {
    private SlotConfirmationService $slotConfirmationService
    ;
    public function __construct
    (
    SlotConfirmationService $slotConfirmationService
    ) {
    $this
    ->slotConfirmationService = $slotConfirmationService
    ;
    }
    public function __invoke(Command $command): void
    {
    $appointment = Appointment
    ::create(
    AppointmentId
    ::generate(),
    Pet::create(
    $command->getPetName
    (),
    $command->getOwnerName
    (),
    $command->getContactNumber
    ()
    ),
    $command->getAppointmentTime
    (),
    $command->getAppointmentLengthInMinutes
    ()
    );
    try {
    $this
    ->slotConfirmationService
    ->confirmSlot
    ($appointment);
    } catch (CouldNotConfirmSlotException $exception) {
    throw new \RuntimeException
    ("The slot could not be booked"
    );
    }
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    AppointmentBookingCommand.php
    Command
    Booking
    AppointmentBookingHandler.php
    migrations
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  107. class AppointmentBookingHandler implements Handler
    {
    private SlotConfirmationService $slotConfirmationService
    ;
    public function __construct
    (
    SlotConfirmationService $slotConfirmationService
    ) {
    $this
    ->slotConfirmationService = $slotConfirmationService
    ;
    }
    public function __invoke(Command $command): void
    {
    $appointment = Appointment
    ::create(
    AppointmentId
    ::generate(),
    Pet::create(
    $command->getPetName
    (),
    $command->getOwnerName
    (),
    $command->getContactNumber
    ()
    ),
    $command->getAppointmentTime
    (),
    $command->getAppointmentLengthInMinutes
    ()
    );
    try {
    $this
    ->slotConfirmationService
    ->confirmSlot
    ($appointment);
    } catch (CouldNotConfirmSlotException $exception) {
    throw new \RuntimeException
    ("The slot could not be booked"
    );
    }
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    AppointmentBookingCommand.php
    Command
    Booking
    AppointmentBookingHandler.php
    migrations
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  108. class AppointmentBookingHandler implements Handler
    {
    private SlotConfirmationService $slotConfirmationService
    ;
    public function __construct
    (
    SlotConfirmationService $slotConfirmationService
    ) {
    $this
    ->slotConfirmationService = $slotConfirmationService
    ;
    }
    public function __invoke(Command $command): void
    {
    $appointment = Appointment
    ::create(
    AppointmentId
    ::generate(),
    Pet::create(
    $command->getPetName
    (),
    $command->getOwnerName
    (),
    $command->getContactNumber
    ()
    ),
    $command->getAppointmentTime
    (),
    $command->getAppointmentLengthInMinutes
    ()
    );
    try {
    $this
    ->slotConfirmationService
    ->confirmSlot
    ($appointment);
    } catch (CouldNotConfirmSlotException $exception) {
    throw new \RuntimeException
    ("The slot could not be booked"
    );
    }
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    AppointmentBookingCommand.php
    Command
    Booking
    AppointmentBookingHandler.php
    migrations
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  109. class AppointmentBookingHandler implements Handler
    {
    private SlotConfirmationService $slotConfirmationService
    ;
    public function __construct
    (
    SlotConfirmationService $slotConfirmationService
    ) {
    $this
    ->slotConfirmationService = $slotConfirmationService
    ;
    }
    public function __invoke(Command $command): void
    {
    $appointment = Appointment
    ::create(
    AppointmentId
    ::generate(),
    Pet::create(
    $command->getPetName
    (),
    $command->getOwnerName
    (),
    $command->getContactNumber
    ()
    ),
    $command->getAppointmentTime
    (),
    $command->getAppointmentLengthInMinutes
    ()
    );
    try {
    $this
    ->slotConfirmationService
    ->confirmSlot
    ($appointment);
    } catch (CouldNotConfirmSlotException $exception) {
    throw new \RuntimeException
    ("The slot could not be booked"
    );
    }
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    AppointmentBookingCommand.php
    Command
    Booking
    AppointmentBookingHandler.php
    migrations
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  110. Services:
    # ...
    # Tell Symfony to auto-wire everything that it finds
    # within the AcmeVet namespace
    AcmeVet\
    :
    resource: '../src/AcmeVet/'
    AcmeVet\Scheduling\Application\Command\Booking\AppointmentBookingHandler
    :
    tags: [messenger.message_handler
    ]
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    AppointmentBookingCommand.php
    Command
    Booking
    AppointmentBookingHandler.php
    services.yaml
    migrations
    composer.json
    App
    templates
    tests
    Bounded Context

    View Slide

  111. The UI

    View Slide

  112. class AppointmentController extends AbstractController
    {
    private MessageBusInterface $messageBus
    ;
    public function __construct
    (MessageBusInterface $messageBus
    )
    {
    $this
    ->messageBus = $messageBus
    ;
    }
    /**
    * @Route("/appointments", name="appointments");
    */
    public function list(Request $request): Response
    {
    $form = $this->createForm
    (AppointmentType
    ::class);
    $form
    ->handleRequest
    ($request);
    if ($form->isSubmitted
    () && $form->isValid()) {
    $command = new AppointmentBookingCommand
    (
    $form
    ->get('appointmentTime'
    )->getData(),
    $form
    ->get('petName')->getData(),
    $form
    ->get('ownerName'
    )->getData(),
    $form
    ->get('contactNumber'
    )->getData(),
    true === $form->get('appointmentLength'
    )->getData() ? 30 : 15
    );
    $this
    ->messageBus
    ->dispatch($command);
    }
    return $this->render('appointment/list.html.twig'
    , [
    'form' => $form->createView
    ()
    ]);
    }
    }
    public
    src
    AcmeVet
    config
    App
    templates
    tests
    composer.json
    Kernel.php
    AppointmentController.php
    migrations
    Controller
    Entity
    Form
    Repository

    View Slide

  113. class AppointmentController extends AbstractController
    {
    private MessageBusInterface $messageBus
    ;
    public function __construct
    (MessageBusInterface $messageBus
    )
    {
    $this
    ->messageBus = $messageBus
    ;
    }
    /**
    * @Route("/appointments", name="appointments");
    */
    public function list(Request $request): Response
    {
    $form = $this->createForm
    (AppointmentType
    ::class);
    $form
    ->handleRequest
    ($request);
    if ($form->isSubmitted
    () && $form->isValid()) {
    $command = new AppointmentBookingCommand
    (
    $form
    ->get('appointmentTime'
    )->getData(),
    $form
    ->get('petName')->getData(),
    $form
    ->get('ownerName'
    )->getData(),
    $form
    ->get('contactNumber'
    )->getData(),
    true === $form->get('appointmentLength'
    )->getData() ? 30 : 15
    );
    $this
    ->messageBus
    ->dispatch($command);
    }
    return $this->render('appointment/list.html.twig'
    , [
    'form' => $form->createView
    ()
    ]);
    }
    }
    public
    src
    AcmeVet
    config
    App
    composer.json
    AppointmentController.php
    migrations
    Controller
    Entity
    Form
    Repository
    templates
    tests
    Kernel.php

    View Slide

  114. class AppointmentController extends AbstractController
    {
    private MessageBusInterface $messageBus
    ;
    public function __construct
    (MessageBusInterface $messageBus
    )
    {
    $this
    ->messageBus = $messageBus
    ;
    }
    /**
    * @Route("/appointments", name="appointments");
    */
    public function list(Request $request): Response
    {
    $form = $this->createForm
    (AppointmentType
    ::class);
    $form
    ->handleRequest
    ($request);
    if ($form->isSubmitted
    () && $form->isValid()) {
    $command = new AppointmentBookingCommand
    (
    $form
    ->get('appointmentTime'
    )->getData(),
    $form
    ->get('petName')->getData(),
    $form
    ->get('ownerName'
    )->getData(),
    $form
    ->get('contactNumber'
    )->getData(),
    true === $form->get('appointmentLength'
    )->getData() ? 30 : 15
    );
    $this
    ->messageBus
    ->dispatch($command);
    }
    return $this->render('appointment/list.html.twig'
    , [
    'form' => $form->createView
    ()
    ]);
    }
    }
    public
    src
    AcmeVet
    config
    App
    composer.json
    Kernel.php
    AppointmentController.php
    migrations
    Controller
    Entity
    Form
    Repository
    templates
    tests

    View Slide

  115. class AppointmentController extends AbstractController
    {
    private MessageBusInterface $messageBus
    ;
    public function __construct
    (MessageBusInterface $messageBus
    )
    {
    $this
    ->messageBus = $messageBus
    ;
    }
    /**
    * @Route("/appointments", name="appointments");
    */
    public function list(Request $request): Response
    {
    $form = $this->createForm
    (AppointmentType
    ::class);
    $form
    ->handleRequest
    ($request);
    if ($form->isSubmitted
    () && $form->isValid()) {
    $command = new AppointmentBookingCommand
    (
    $form
    ->get('appointmentTime'
    )->getData(),
    $form
    ->get('petName')->getData(),
    $form
    ->get('ownerName'
    )->getData(),
    $form
    ->get('contactNumber'
    )->getData(),
    true === $form->get('appointmentLength'
    )->getData() ? 30 : 15
    );
    $this
    ->messageBus
    ->dispatch($command);
    }
    return $this->render('appointment/list.html.twig'
    , [
    'form' => $form->createView
    ()
    ]);
    }
    }
    public
    src
    AcmeVet
    config
    App
    composer.json
    Kernel.php
    AppointmentController.php
    migrations
    Controller
    Entity
    Form
    Repository
    templates
    tests

    View Slide

  116. namespace App\Form\Type;
    class AppointmentType extends AbstractType
    {
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
    ->add('appointmentTime'
    , DateTimeType
    ::class, [
    'input' => 'datetime_immutable'
    ,
    'widget' => 'single_text'
    ,
    'attr' => [
    'step' => 900,
    'min' => (new \DateTimeImmutable
    ())->format('Y-m-d')
    ]
    ])
    ->add('petName', TextType::class)
    ->add('ownerName'
    , TextType::class)
    ->add('contactNumber'
    , TextType::class)
    ->add('appointmentLength'
    , ChoiceType
    ::class, [
    'choices' => [
    'single' => false,
    'double' => true
    ]
    ])
    ->add('submit', SubmitType
    ::class);
    }
    }
    public
    src
    AcmeVet
    config
    App
    composer.json
    Kernel.php
    AppointmentController.php
    migrations
    Controller
    Entity
    Form
    Repository
    Type
    templates
    tests

    View Slide

  117. View Slide

  118. View Slide

  119. namespace AcmeVet\Scheduling
    \Application
    \Query;
    use AcmeVet\Appointment
    \Domain\Appointment
    ;
    use AcmeVet\Appointment
    \Domain\AppointmentRepository
    ;
    class AppointmentQuery
    {
    private AppointmentRepository $repository
    ;
    public function __construct
    (AppointmentRepository $repository
    )
    {
    $this
    ->repository = $repository
    ;
    }
    public function fetchAll(): array
    {
    $results = $this->repository
    ->getAll();
    return \array_map(function (Appointment $item) {
    return new AppointmentDTO
    (
    $item->getStartTime
    (),
    15 === $item->getLengthInMinutes
    () ? false : true,
    $item->getPet()->getName(),
    $item->getPet()->getOwnerName
    (),
    $item->getPet()->getContactNumber
    ()
    );
    }, $results);
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    App
    templates
    tests
    composer.json
    Command
    Query
    AppointmentQuery.php
    migrations
    Bounded Context

    View Slide

  120. namespace AcmeVet\Scheduling
    \Application
    \Query;
    use AcmeVet\Appointment
    \Domain\Appointment
    ;
    use AcmeVet\Appointment
    \Domain\AppointmentRepository
    ;
    class AppointmentQuery
    {
    private AppointmentRepository $repository
    ;
    public function __construct
    (AppointmentRepository $repository
    )
    {
    $this
    ->repository = $repository
    ;
    }
    public function fetchAll(): array
    {
    $results = $this->repository
    ->getAll();
    return \array_map(function (Appointment $item) {
    return new AppointmentDTO
    (
    $item->getStartTime
    (),
    15 === $item->getLengthInMinutes
    () ? false : true,
    $item->getPet()->getName(),
    $item->getPet()->getOwnerName
    (),
    $item->getPet()->getContactNumber
    ()
    );
    }, $results);
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    App
    composer.json
    Command
    Query
    AppointmentQuery.php
    migrations
    templates
    tests
    Bounded Context

    View Slide

  121. namespace AcmeVet\Scheduling
    \Application
    \Query;
    use AcmeVet\Appointment
    \Domain\Appointment
    ;
    use AcmeVet\Appointment
    \Domain\AppointmentRepository
    ;
    class AppointmentQuery
    {
    private AppointmentRepository $repository
    ;
    public function __construct
    (AppointmentRepository $repository
    )
    {
    $this
    ->repository = $repository
    ;
    }
    public function fetchAll(): array
    {
    $results = $this->repository
    ->getAll();
    return \array_map(function (Appointment $item) {
    return new AppointmentDTO
    (
    $item->getStartTime
    (),
    15 === $item->getLengthInMinutes
    () ? false : true,
    $item->getPet()->getName(),
    $item->getPet()->getOwnerName
    (),
    $item->getPet()->getContactNumber
    ()
    );
    }, $results);
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    App
    composer.json
    Command
    Query
    AppointmentQuery.php
    migrations
    templates
    tests
    Bounded Context

    View Slide

  122. namespace AcmeVet\Scheduling
    \Application
    \Query;
    use AcmeVet\Appointment
    \Domain\Appointment
    ;
    use AcmeVet\Appointment
    \Domain\AppointmentRepository
    ;
    class AppointmentQuery
    {
    private AppointmentRepository $repository
    ;
    public function __construct
    (AppointmentRepository $repository
    )
    {
    $this
    ->repository = $repository
    ;
    }
    public function fetchAll(): array
    {
    $results = $this->repository
    ->getAll();
    return \array_map(function (Appointment $item) {
    return new AppointmentDTO
    (
    $item->getStartTime
    (),
    15 === $item->getLengthInMinutes
    () ? false : true,
    $item->getPet()->getName(),
    $item->getPet()->getOwnerName
    (),
    $item->getPet()->getContactNumber
    ()
    );
    }, $results);
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    App
    composer.json
    Command
    Query
    AppointmentQuery.php
    migrations
    templates
    tests
    Bounded Context

    View Slide

  123. namespace AcmeVet\Scheduling
    \Application
    \Query;
    use AcmeVet\Appointment
    \Domain\Appointment
    ;
    use AcmeVet\Appointment
    \Domain\AppointmentRepository
    ;
    class AppointmentQuery
    {
    private AppointmentRepository $repository
    ;
    public function __construct
    (AppointmentRepository $repository
    )
    {
    $this
    ->repository = $repository
    ;
    }
    public function fetchAll(): array
    {
    $results = $this->repository
    ->getAll();
    return \array_map(function (Appointment $item) {
    return new AppointmentDTO
    (
    $item->getStartTime
    (),
    15 === $item->getLengthInMinutes
    () ? false : true,
    $item->getPet()->getName(),
    $item->getPet()->getOwnerName
    (),
    $item->getPet()->getContactNumber
    ()
    );
    }, $results);
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    App
    composer.json
    Command
    Query
    AppointmentQuery.php
    migrations
    templates
    tests
    Bounded Context

    View Slide

  124. class AppointmentDTO
    {
    private \DateTimeImmutable $startTime
    ;
    private bool $isDoubleAppointment
    ;
    private string $petName;
    private string $ownerName
    ;
    private string $contactNumber
    ;
    public function __construct
    (
    \DateTimeImmutable $startTime
    ,
    bool $isDoubleAppointment
    ,
    string $petName,
    string $ownerName
    ,
    string $contactNumber
    )
    {
    $this
    ->startTime = $startTime
    ;
    $this
    ->isDoubleAppointment = $isDoubleAppointment
    ;
    $this
    ->petName = $petName;
    $this
    ->ownerName = $ownerName
    ;
    $this
    ->contactNumber = $contactNumber
    ;
    }
    public function getStartTime
    (): \DateTimeImmutable
    {
    return $this->startTime;
    }
    public function getPetName
    (): string
    {
    return $this->petName;
    }
    public function getOwnerName
    (): string
    {
    return $this->ownerName;
    }
    public function getContactNumber
    (): string
    {
    return $this->contactNumber
    ;
    }
    public function isDoubleAppointment
    (): bool
    {
    return $this->isDoubleAppointment
    ;
    }
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Scheduling
    Application
    Domain
    App
    composer.json
    Command
    Query
    AppointmentQuery.php
    migrations
    AppointmentDTO.php
    templates
    tests
    Bounded Context

    View Slide

  125. class AppointmentController extends AbstractController
    {
    /**
    * @Route("/appointments", name="appointments");
    */
    public function list(Request $request, AppointmentQuery $appointmentList
    ): Response
    {
    $form = $this->createForm
    (AppointmentType
    ::class);
    $form
    ->handleRequest
    ($request);
    if ($form->isSubmitted
    () && $form->isValid()) {
    $command = new AppointmentBookingCommand
    (
    $form
    ->get('appointmentTime'
    )->getData(),
    $form
    ->get('petName')->getData(),
    $form
    ->get('ownerName'
    )->getData(),
    $form
    ->get('contactNumber'
    )->getData(),
    true === $form->get('appointmentLength'
    )->getData() ? 30 : 15
    );
    $this
    ->messageBus
    ->dispatch($command);
    }
    $appointments = $appointmentList
    ->fetchAll();
    return $this->render('appointment/list.html.twig'
    , [
    'appointments' => $appointments,
    'form' => $form->createView
    ()
    ]);
    }
    }
    public
    src
    AcmeVet
    config
    App
    composer.json
    Kernel.php
    AppointmentController.php
    migrations
    Controller
    Entity
    Form
    Repository
    templates
    tests

    View Slide

  126. class AppointmentController extends AbstractController
    {
    /**
    * @Route("/appointments", name="appointments");
    */
    public function list(Request $request, AppointmentQuery $appointmentList
    ): Response
    {
    $form = $this->createForm
    (AppointmentType
    ::class);
    $form
    ->handleRequest
    ($request);
    if ($form->isSubmitted
    () && $form->isValid()) {
    $command = new AppointmentBookingCommand
    (
    $form
    ->get('appointmentTime'
    )->getData(),
    $form
    ->get('petName')->getData(),
    $form
    ->get('ownerName'
    )->getData(),
    $form
    ->get('contactNumber'
    )->getData(),
    true === $form->get('appointmentLength'
    )->getData() ? 30 : 15
    );
    $this
    ->messageBus
    ->dispatch($command);
    }
    $appointments = $appointmentList
    ->fetchAll();
    return $this->render('appointment/list.html.twig'
    , [
    'appointments' => $appointments,
    'form' => $form->createView
    ()
    ]);
    }
    }
    public
    src
    AcmeVet
    config
    App
    composer.json
    Kernel.php
    AppointmentController.php
    migrations
    Controller
    Entity
    Form
    Repository
    templates
    tests

    View Slide

  127. class AppointmentController extends AbstractController
    {
    /**
    * @Route("/appointments", name="appointments");
    */
    public function list(Request $request, AppointmentQuery $appointmentList
    ): Response
    {
    $form = $this->createForm
    (AppointmentType
    ::class);
    $form
    ->handleRequest
    ($request);
    if ($form->isSubmitted
    () && $form->isValid()) {
    $command = new AppointmentBookingCommand
    (
    $form
    ->get('appointmentTime'
    )->getData(),
    $form
    ->get('petName')->getData(),
    $form
    ->get('ownerName'
    )->getData(),
    $form
    ->get('contactNumber'
    )->getData(),
    true === $form->get('appointmentLength'
    )->getData() ? 30 : 15
    );
    $this
    ->messageBus
    ->dispatch($command);
    }
    $appointments = $appointmentList
    ->fetchAll();
    return $this->render('appointment/list.html.twig'
    , [
    'appointments' => $appointments,
    'form' => $form->createView
    ()
    ]);
    }
    }
    public
    src
    AcmeVet
    config
    App
    composer.json
    Kernel.php
    AppointmentController.php
    migrations
    Controller
    Entity
    Form
    Repository
    templates
    tests

    View Slide

  128. View Slide

  129. View Slide

  130. The second bounded context

    View Slide

  131. public
    src
    AcmeVet
    config
    Infrastructure
    Consultation
    Application
    Domain
    App
    composer.json
    migrations
    Scheduling
    templates
    tests
    Bounded Context
    Bounded Context

    View Slide

  132. namespace AcmeVet\Consultation
    \Domain\Pet;
    class Pet
    {
    private PetId $petId;
    private Animal $animal;
    private string $petName;
    private \DateTimeImmutable $dateOfBirth
    ;
    private array $diagnoses = [];
    private array $consultations = [];
    private function __construct
    (
    PetId $petId,
    Animal $animal,
    string $petName,
    \DateTimeImmutable $dateOfBirth
    ) {
    $this
    ->petId = $petId;
    $this
    ->animal = $animal;
    $this
    ->petName = $petName;
    $this
    ->dateOfBirth = $dateOfBirth
    ;
    }
    public static function create(
    PetId $petId,
    Animal $animal,
    string $petName,
    \DateTimeImmutable $dateOfBirth
    ): self {
    return new self
    ($petId, $animal, $petName, $dateOfBirth
    );
    }
    public function recordConsultation
    (Consultation $consultation
    ): void
    {
    $this
    ->consultations
    [] = $consultation
    ;
    }
    public function getBreed(): string
    {
    return (new \ReflectionClass
    ($this->animal))->getShortName
    () . '/' . $this->animal->getBreedName
    ();
    }
    public function addDiagnosis
    (Diagnosis $diagnosis
    ): void
    {
    $this
    ->diagnoses[] = $diagnosis
    ;
    }
    public
    src
    AcmeVet
    config
    Infrastructure
    Consultation
    Application
    Domain
    App
    composer.json
    migrations
    Scheduling
    Pet
    Pet.php
    templates
    tests
    Bounded Context
    Aggregate
    Aggregate
    Root
    Bounded Context

    View Slide

  133. Application data

    View Slide

  134. namespace App\Entity;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Security\Core\User\UserInterface;
    /**
    * @ORM\Table(name="app_users")
    * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
    */
    class User implements UserInterface, \Serializable
    {
    /**
    * @ORM\Column(type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;
    /**
    * @ORM\Column(type="string", length=25, unique=true)
    */
    private $username;
    /**
    * @ORM\Column(type="string", length=64)
    */
    private $password;
    /**
    * @ORM\Column(type="string", length=254, unique=true)
    */
    private $email;
    /**
    * @ORM\Column(name="is_active", type="boolean")
    */
    private $isActive;
    public function __construct()
    {
    $this->isActive = true;
    // may not be needed, see section on salt below
    // $this->salt = md5(uniqid('', true));
    }
    public function getUsername()
    public
    src
    AcmeVet
    config
    App
    composer.json
    Kernel.php
    User.php
    migrations
    Controller
    Entity
    Form
    Repository
    templates
    tests

    View Slide

  135. Do it yourself ● Think about the words you use in the
    business
    ● Identify Bounded Contexts
    ● Think about who the domain experts are for
    each Bounded Context
    ● Start talking about business processes and
    invariants in each Bounded Context
    ● Identify the aggregates
    ● Build the software

    View Slide

  136. Do it yourself ● Think about the words you use in the
    business
    ● Identify Bounded Contexts
    ● Think about who the domain experts are for
    each Bounded Context
    ● Start talking about business processes and
    invariants in each Bounded Context
    ● Identify the aggregates
    ● Build the software
    https://github.com/nealio82/
    absolute-beginners-guide-to-ddd-with-symfony

    View Slide

  137. Do it yourself
    https://github.com/nealio82/
    absolute-beginners-guide-to-ddd-with-symfony
    ● Think about the words you use in the
    business
    ● Identify Bounded Contexts
    ● Think about who the domain experts are for
    each Bounded Context
    ● Start talking about business processes and
    invariants in each Bounded Context
    ● Identify the aggregates
    ● Build the software
    @nealio82

    View Slide

  138. Do it yourself
    https://github.com/nealio82/
    absolute-beginners-guide-to-ddd-with-symfony
    ● Think about the words you use in the
    business
    ● Identify Bounded Contexts
    ● Think about who the domain experts are for
    each Bounded Context
    ● Start talking about business processes and
    invariants in each Bounded Context
    ● Identify the aggregates
    ● Build the software
    @nealio82
    Thanks to @edd_mann, @lucas_courot, and @jim_jqim

    View Slide

  139. Do it yourself
    https://github.com/nealio82/
    absolute-beginners-guide-to-ddd-with-symfony
    ● Think about the words you use in the
    business
    ● Identify Bounded Contexts
    ● Think about who the domain experts are for
    each Bounded Context
    ● Start talking about business processes and
    invariants in each Bounded Context
    ● Identify the aggregates
    ● Build the software
    @nealio82
    Thanks to @edd_mann, @lucas_courot, and @jim_jqim, and you!

    View Slide