Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

What’s the point?

Slide 3

Slide 3 text

What’s the point?

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

What’s the point?

Slide 6

Slide 6 text

What’s the point?

Slide 7

Slide 7 text

What’s the point?

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

Domain-Driven Design Beginner’s Guide

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

What is Domain-Driven Design?

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Definitions & revision

Slide 15

Slide 15 text

Domain

Slide 16

Slide 16 text

Domain Experts

Slide 17

Slide 17 text

Ubiquitous Language

Slide 18

Slide 18 text

The software must use the same words as the business

Slide 19

Slide 19 text

Property

Slide 20

Slide 20 text

Instruction

Slide 21

Slide 21 text

Instruction Property

Slide 22

Slide 22 text

Bounded Contexts

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Entities

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Aggregates

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Aggregate Roots

Slide 32

Slide 32 text

The aggregate root is the entry-point into the aggregate

Slide 33

Slide 33 text

Invariants

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Domain Services and Application Services

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Architecture

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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.

Slide 41

Slide 41 text

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.

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Domain-Driven Design Getting started

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

Preparation

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

public src config templates tests composer.json

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

The First Bounded Context

Slide 60

Slide 60 text

"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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Build the aggregate

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

public src AcmeVet config Infrastructure Appointment.php 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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

Slide 81

Slide 81 text

Create a domain service

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

Slide 87

Slide 87 text

Slide 88

Slide 88 text

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

Infrastructure

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

$ php bin/console doctrine:migrations:generate

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Application services

Slide 102

Slide 102 text

No content

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

The UI

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

No content

Slide 118

Slide 118 text

No content

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

No content

Slide 129

Slide 129 text

No content

Slide 130

Slide 130 text

The second bounded context

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

Application data

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

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

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

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!