Rich Domain Model with Symfony2 and Doctrine2 Leonardo Proietti @_leopro_ Symfony Day Italy Milan, 10 October 2014
twitter.com/_leopro_ github.com/leopro linkedin.com/in/leonardoproietti
1. Domain Modeling
domain = problem space domain model = solution space
Domain “A Domain [...] is what an organization does and the world it does in.” (Vaughn Vernon, “Implementing Domain-Driven Design”)
Domain Model “A model is a simplification. It is an interpretation of reality that abstracts the aspects relevant to solving problem at hand and ignores extraneous detail.” (Eric Evans, "Domain Driven Design")
A model
Domain Model “In DDD, domain model refers to a class.” (Julie Lerman, http://msdn.microsoft.com/en-us/magazine/dn385704.aspx)
Domain Model “An object model of the domain that incorporates both behavior and data.” (Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
Domain Model “An object model of the domain that incorporates both behavior and data.” (Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
Why Rich?
First, because it’s not anemic Martin Fowler http://martinfowler.com/bliki/AnemicDomainModel.html
<?php class Anemic { private $shouldNotChangeAfterCreation; private $shouldBeChangedOnlyOnEdit; private $couldBeChangedAnytime; public function setCouldBeChangedAnytime($couldBeChangedAnytime) {} public function setShouldBeChangedOnlyOnEdit($shouldBeChangedOnlyOnEdit) {} public function setShouldNotChangeAfterCreation($shouldNotChangeAfterCreation {} }
<?php class SomeService { public function doStuffOnCreation() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldNotChangeAfterCreation('def'); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
<?php class SomeService { public function doStuffOnEdit() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldBeChangedOnlyOnEdit(‘123’); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
<?php class SomeService { public function doStuffOnEdit() { Loss of memory $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldBeChangedOnlyOnEdit(‘123’); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
<?php class SomeService { public function doStuffOnEdit() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldBeChangedOnlyOnEdit(‘123’); $anemic->setShouldNotChangeAfterCreation('def'); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
<?php class BehaviouralClass { private $shouldNotChangeAfterCreation; private $shouldBeChangedOnlyOnEdit; private $couldBeChangedAnytime; public function __construct($shouldNotChangeAfterCreation, $couldBeChangedAnytime) { $this->shouldNotChangeAfterCreation = $shouldNotChangeAfterCreation; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } public function modify($shouldBeChangedOnlyOnEdit, $couldBeChangedAnytime = null) { $this->shouldBeChangedOnlyOnEdit = $shouldBeChangedOnlyOnEdit; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } }
<?php class BehaviouralClass { private $shouldNotChangeAfterCreation; private $shouldBeChangedOnlyOnEdit; private $couldBeChangedAnytime; It's not still rich, lacks of ... public function __construct($shouldNotChangeAfterCreation, $couldBeChangedAnytime) { $this->shouldNotChangeAfterCreation = $shouldNotChangeAfterCreation; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } public function modify($shouldBeChangedOnlyOnEdit, $couldBeChangedAnytime = null) { $this->shouldBeChangedOnlyOnEdit = $shouldBeChangedOnlyOnEdit; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } }
Ubiquitous Language “the domain model can provide the backbone for that common language [...]. The vocabulary of that ubiquitius language includes the names of classes and prominent operations” (Eric Evans, "Domain Driven Design")
What does “coffee” mean? Alberto Brandolini AKA @ziobrando
A bit of strategy
Our domain is an online game that simulate the soccer’s world.
Bounded Contexts
What does “player” mean in our domain?
The meaning of “player” Within game engine context a model of a real soccer player, modelled with behaviours to fit the requirements of the game engine.
The meaning of “player” Within data import context a model of a real soccer player, but modelled for a simple CRUD.
The meaning of “player” Within user profile context a model of the user of the website, who plays the game.
League Team Calendar Player Lineup Coach Core context
League Team Calendar Player Lineup Coach (the player in “user context”) Core context
Player Game context User context Uuid Name Roles Uuid Email Password
Data and Behaviours
<?php class League { private $id; private $name; private $teams; public function __construct(Uuid $uuid, $name) { $this->id = $uuid; $this->name = $name; $this->teams = new ArrayCollection(); } public function registerTeam(Team $team) { $this->teams->add($team); } }
<?php class League { private $id; private $name; private $teams; A team must do a registration public function __construct({ to the league Uuid $uuid, $name) $this->id = $uuid; $this->name = $name; $this->teams = new ArrayCollection(); } public function registerTeam(Team $team) { $this->teams->add($team); } }
<?php class League { private $id; private $genericInfo; private $teams; public function __construct(Uuid $uuid, LeagueGenericInfo $leagueGenericInfo) { $this->id = $uuid; $this->genericInfo = $leagueGenericInfo; $this->teams = new ArrayCollection(); } // ...}
<?php class LeagueGenericInfo { private $name; private $description; private $country; public function __construct($country, $description, $name) { $this->country = $country; $this->description = $description; $this->name = $name; } // … getters and behaviours}
<?php class LeagueGenericInfo { private $name; private $description; private $country; Value object public function __construct($country, $description, $name) { $this->country = $country; $this->description = $description; $this->name = $name; } // … getters and behaviours}
Validation: invariants and input
<?php class League { // ... public function registerTeam(Team $team) { if (!$this->canLeagueAcceptAnotherRegistration()) { throw new DomainException('Not more places available'); } $this->teams->add($team); } private function canLeagueAcceptAnotherRegistration() { if ($this->teams->count() == 8) { return false; } return true; } }
<?php class League { // ... public function registerTeam(Team $team) { if (!$this->canLeagueAcceptAnotherRegistration()) { League protects its invariants throw new DomainException('Not more places available'); } $this->teams->add($team); } private function canLeagueAcceptAnotherRegistration() { if ($this->teams->count() == 8) { return false; } return true; } }
<?php class League { // ... public function getTeams() { return $this->teams; } }
<?php class League { // ... private function getTeams() { return $this->teams; } }
<?php class LeagueGenericInfo { private $name; private $description; private $country; private static $countries; public function __construct($country, $description, $name) { if(!isset(static::$countries)) { static::$countries = require __DIR__.'/countries.php'; } if (!array_key_exists($name, static::$countries)) { throw new UnknownCountryException($country); } $this->country = $country; // .. thanks to Mathias Verraes for “Money” ;-) }}
<?php class LeagueGenericInfo { private $name; private $description; private $country; Input validation private static $countries; public function __construct($country, $description, $name) { if(!isset(static::$countries)) { static::$countries = require __DIR__.'/countries.php'; } if (!array_key_exists($name, static::$countries)) { throw new UnknownCountryException($country); } $this->country = $country; // .. thanks to Mathias Verraes for “Money” ;-) }}
<?php class LeagueGenericInfo { private $name; private $description; private $country; Could be private static also $countries; placed in commands public function __construct($country, $description, $name) { if(!isset(static::$countries)) { static::$countries = require __DIR__.'/countries.php'; } if (!array_key_exists($name, static::$countries)) { throw new UnknownCountryException($country); } $this->country = $country; // .. thanks to Mathias Verraes for “Money” ;-) }}
<?php class League { public function render() { $properties = [ 'id' => $this->id, 'name' => $this->genericInfo->getName(), 'description' => $this->genericInfo->getDescription(), 'country' => $this->genericInfo->getCountry(), ]; $teams = new ArrayCollection(); foreach ($this->teams as $team) { $teams->add($team->render()); } $properties['teams'] = $teams; return new ArrayCollection($properties); } }
<?php class League { public function render() { $properties = [ 'id' => $this->id, 'name' => $this->genericInfo->getName(), 'description' => $this->genericInfo->getDescription(), 'country' => $this->genericInfo->getCountry(), ]; Return a read-only object $teams = new ArrayCollection(); foreach ($this->teams as $team) { $teams->add($team->render()); } $properties['teams'] = $teams; return new ArrayCollection($properties); } }
<?php class Team { private $id; private $players; public function __construct(Uuid $uuid) { $this->id = $uuid; $this->players = new ArrayCollection(); } public function firePlayer($id) { foreach ($this->players as $key => $player) { if ($player->getId() == $id) { $this->players->remove($key); } } } }
<?php class Team { private $id; private $players; Traverse the collections public function __construct(Uuid $uuid) { $this->id = $uuid; $this->players = new ArrayCollection(); } public function firePlayer(Player $playerToFire) { foreach ($this->players as $key => $player) { if ($player->getId() == $playerToFire->getId()) { $this->players->remove($key); } } } }
The Player should have a relation towards the Team?
Be iterative using TDD/BDD
<?php class LeagueTest extends PHPUnit_Framework_TestCase { /** * @test * @expectedException */ public function leagueMustHaveMaximumEightTeams() { // … $genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); $league = new League($uuid, $genericInfo); $team = $this->getMockBuilder('Team') // … for ($x=0; $x<=8; $x++) { $league->registerTeam($team); } } }
<?php class LeagueTest extends PHPUnit_Framework_TestCase { /** * @test * @expectedException */ public function leagueMustHaveMaximumEightTeams() { // … $genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); $league = new League($uuid, $genericInfo); $team = $this->getMockBuilder('Team') // … for ($x=0; $x<=8; $x++) { $league->registerTeam($team); } } }
<?php class LeagueTest extends PHPUnit_Framework_TestCase { /** * @test * @expectedException */ public function leagueMustHaveMaximumEightTeams() { // … The same Team can do more than one registration to the League?!? $genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); $league = new League($uuid, $genericInfo); $team = $this->getMockBuilder('Team') // … for ($x=0; $x<=8; $x++) { $league->registerTeam($team); } } }
<?php class League { //.. public function registerTeam(Team $team) { $this->canLeagueAcceptRegistrationOf($team); $this->teams->add($team); } private function canLeagueAcceptRegistrationOf(Team $applicantTeam) { if (!$this->canLeagueAcceptAnotherRegistration()) { throw new DomainException('Not more places available'); } foreach ($this->teams as $key => $team) { if ($team->getId() == $applicantTeam->getId()) { throw new DomainException('Team already registered'); } } } }
<?php class League { //.. public function registerTeam(Team $team) { $this->canLeagueAcceptRegistrationOf($team); $this->teams->add($team); } And so on ... private function canLeagueAcceptRegistrationOf(Team $applicantTeam) { if (!$this->canLeagueAcceptAnotherRegistration()) { throw new DomainException('Not more places available'); } foreach ($this->teams as $key => $team) { if ($team->getId() == $applicantTeam->getId()) { throw new DomainException('Team already registered'); } } } }
2. Doctrine (v2.5)
Awareness
We are using the entities of the Persistence Model as entities of our Domain Awareness Model
League.orm.yml League: type: entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY
League.orm.yml League: type: entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY
League.orm.yml League: type: entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY Uuid.orm.yml Uuid: type: embeddable id: uuid: type: string length: 36
League.orm.yml League: type: entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY Uuid.orm.yml Uuid: type: embeddable id: uuid: type: string length: 36
<?php class TeamRepository implements TeamRepositoryInterface { private $em; public function __construct(EntityManager $em) { $this->em = $em; } }
Persisting entities <?php class TeamRepository implements TeamRepositoryInterface { public function add(Team $team) { $this->em->persist($team); $this->em->flush(); } }
Avoid collection hydration (foreach, toArray) <?php class TeamRepository implements TeamRepositoryInterface { public function getWithoutPlayers($id) { $qb = $this->em->createQueryBuilder(); $qb ->select('t', 'p') ->from("Team", 't') ->leftJoin('t.players', 'p', Join::WITH, $qb->expr()->andX( $qb->expr()->eq('p.id.uuid', ':pid') )) ->where('c.id.uuid = :id') ->setMaxResults(1); $qb->setParameter('id', $id); $qb->setParameter('pid', null); return $qb->getQuery()->getOneOrNullResult(); } }
Retrieve an object joined with empty collection <?php class TeamRepository implements TeamRepositoryInterface { public function getWithPlayers($id) { $qb = $this->em->createQueryBuilder(); $qb ->select('t', 'p') ->from("Team", 't') ->leftJoin(t.players', 'p', Join::WITH, $qb->expr()->andX( $qb->expr()->eq('p.status', ':status') )) ->where('t.id.uuid = :id'); $qb->setParameter('status', 'on_the_market'); $qb->setParameter('id', $id); return $qb->getQuery()->getOneOrNullResult(); } }
Get paginated list of Teams with Player joined <?php use DoctrineORMToolsPaginationPaginator; class TeamRepository implements TeamRepositoryInterface { public function paginate($first, $max) { $qb = $this->em->createQueryBuilder(); $qb ->select('t', 'p') ->from("Team", 't') ->leftJoin('t.players', 'p') ->setFirstResult($first) ->setMaxResults($max); $paginator = new Paginator($qb->getQuery()); return $paginator->getIterator(); } }
3. Symfony (v2.5)
<?php class FirePlayerCommand implements Command { public $teamId; public $playerId; public function getRequest() { return new Request( [ 'teamId' => $this->teamId, 'playerId' => $this->playerId ] ); } }
<?php class Request extends ArrayCollection implements RequestInterface { public function __construct(array $values) { parent::__construct($values); } public function get($key, $default = null) { if (!parent::containsKey($key)) { throw new DomainException(); } $value = parent::get($key); if (!$value && $default) { return $default; } return $value; } }
<?php class CommandHandler { private $dispatcher; private $useCases; public function __construct(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } public function registerUseCases($useCases) { foreach ($useCases as $useCase) { if ($useCase instanceof UseCase) { $this->useCases[$useCase->getManagedCommand()] = $useCase; } else { throw new LogicException(''); } } } // ...}
<?php class CommandHandler { // ... public function execute(Command $command) { try { $this->dispatcher ->dispatch(Events::PRE_COMMAND, new CommandEvent($command)); $this->useCases[get_class($command)]->run($command); $response = new Response(); $this->dispatcher ->dispatch(Events::POST_COMMAND, new PostCommandEvent($command, $response)); return $response; } catch (DomainException $e) { $this->dispatcher ->dispatch(Events::EXCEPTION, new ExceptionEvent($command, $e)); return new Response($e->getMessage(), Response::STATUS_KO); } } }
<?php class CommandHandler { // ... public function execute(Command $command) { try { $this->dispatcher ->dispatch(Events::PRE_COMMAND, new CommandEvent($command)); $this->useCases[get_class($command)]->run($command); $response = new Response(); $this->dispatcher ->dispatch(Events::POST_COMMAND, new PostCommandEvent($command, $response)); return $response; } catch (DomainException $e) { $this->dispatcher ->dispatch(Events::EXCEPTION, new ExceptionEvent($command, $e)); return new Response($e->getMessage(), Response::STATUS_KO); } } }
<?php class FirePlayerUseCase implements UseCase { private $repository; public function __construct(TeamRepositoryInterface $repository) { $this->repository = $repository; } public function run(Command $command) { $request = $command->getRequest(); $team = $this->repository->get( $request->get('teamId') ); $team->firePlayer( $request->get('playerId') ); $this->repository->add($team); } }
Commands and Use Cases could be used standalone
<?php class CommandHandlerCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { if (!$container->hasDefinition('command_handler')) { return; } $definition = $container->getDefinition('command_handler'); $taggedServices = $container->findTaggedServiceIds('use_case'); $useCases = array(); foreach ($taggedServices as $id => $attributes) { $useCases[] = new Reference($id); } $definition->addMethodCall( 'registerUseCases', array($useCases) ); } }
<service id="use_case.fire_player" public="false" class="UseCaseFirePlayerUseCase"> <argument type="service" id="repository_team"/> <tag name="use_case" /> </service>
<?php class MyController extends Controller { public function modifyLeagueAction(Request $request, $id) { $reader = $this->get('reader'); $league = $reader->getLeague($id); $command = ModifyLeagueCommand::fromArray($league); $form = $this->createForm(new ModifyLeagueType(), $command); $form->handleRequest($request); if ($form->isValid()) { $commandHandler = $this->get('command_handler'); $response = $commandHandler->execute($command); if ($response->isOk()) { //... } } return array( 'form' => $form->createView() ); } }
<?php class MyController extends Controller { public function modifyLeagueAction(Request $request, $id) { $reader = $this->get('reader'); $league = $reader->getLeague($id); $command = ModifyLeagueCommand::fromArray($league); $form = $this->createForm(new ModifyLeagueType(), $command); $form->handleRequest($request); if ($form->isValid()) { $commandHandler = $this->get('command_handler'); $response = $commandHandler->execute($command); if ($response->isOk()) { //... } } return array( 'form' => $form->createView() ); } }
<?php class MyController extends Controller { public function modifyLeagueAction(Request $request, $id) { $reader = $this->get('reader'); Consider using a service for $league = $reader->getLeague($id); reading $command = ModifyLeagueCommand::operations, fromArray($league); instead $form = $this->createForm(new ModifyLeagueType(), $command); use $form->the handleRequest($repository request); directly if ($form->isValid()) { $commandHandler = $this->get('command_handler'); $response = $commandHandler->execute($command); if ($response->isOk()) { //... } } return array( 'form' => $form->createView() ); } }
<?php class ModifyLeagueType extends CreateNewsType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('leagueId’, ‘’hidden') ->add('name') ->add('save', 'submit') ; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => ModifyLeagueCommand::CLASS, )); } }
<?php class ModifyLeagueCommand implements Command { public $leagueId; public $name; public function getRequest() { return new Request( [ leagueId => $this->leagueId, name => $this->name ] ); } }
Command validation ModifyLeagueCommand: properties: leagueId: - NotBlank: ~ name: - NotBlank: ~
Using CQRS and Event Sourcing things change deeply
?
https://joind.in/talk/view/12214
Credits A special thanks to @_orso_ the first who told me about rich models - Eric Evans - "Domain Driven Design" - Vaughn Vernon - “Implementing Domain-Driven Design” - http://www.slideshare.net/ziobrando/gestire-la-complessit-con-domain-driven-design - http://verraes.net/2013/12/related-entities-vs-child-entities/ - http://www.whitewashing.de/2012/08/22/building_an_object_model__no_setters_allowed.html - http://www.infoq.com/articles/ddd-contextmapping - http://nicolopignatelli.me/valueobjects-a-php-immutable-class-library/ - http://welcometothebundle.com/domain-driven-design-and-symfony-for-simple-app/ - http://www.slideshare.net/perprogramming/application-layer-33335917 - http://lostechies.com/jimmybogard/2008/08/21/services-in-domain-driven-design/ - http://www.slideshare.net/thinkddd/practical-domain-driven-design-cqrs-and-messaging-architectures - http://lostechies.com/jimmybogard/2009/02/15/validation-in-a-ddd-world/ - http://gojko.net/2009/09/30/ddd-and-relational-databases-the-value-object-dilemma/
Credits - http://verraes.net/2013/06/unbreakable-domain-models/ - http://www.mehdi-khalili.com/orm-anti-patterns-part-4-persistence-domain-model - http://martinfowler.com/bliki/BoundedContext.html - http://www.substanceofcode.com/2007/01/17/from-anemic-to-rich-domain-model/ - http://gorodinski.com/blog/2012/04/25/read-models-as-a-tactical-pattern-in-domain-driven-design-ddd/ - http://www.sapiensworks.com/blog/post/2013/05/01/DDD-Persisting-Aggregate-Roots-In-A-Unit-Of-Work.aspx - http://simon-says-architecture.com/2011/09/06/ddd-by-the-book/ - http://scaledagileframework.com/domain-modeling/ - http://www.codeproject.com/Articles/555855/Introduction-to-CQRS

Rich domain model with symfony 2.5 and doctrine 2.5

  • 1.
    Rich Domain Modelwith Symfony2 and Doctrine2 Leonardo Proietti @_leopro_ Symfony Day Italy Milan, 10 October 2014
  • 2.
  • 3.
  • 4.
    domain = problemspace domain model = solution space
  • 5.
    Domain “A Domain[...] is what an organization does and the world it does in.” (Vaughn Vernon, “Implementing Domain-Driven Design”)
  • 6.
    Domain Model “Amodel is a simplification. It is an interpretation of reality that abstracts the aspects relevant to solving problem at hand and ignores extraneous detail.” (Eric Evans, "Domain Driven Design")
  • 7.
  • 8.
    Domain Model “InDDD, domain model refers to a class.” (Julie Lerman, http://msdn.microsoft.com/en-us/magazine/dn385704.aspx)
  • 9.
    Domain Model “Anobject model of the domain that incorporates both behavior and data.” (Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
  • 10.
    Domain Model “Anobject model of the domain that incorporates both behavior and data.” (Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
  • 11.
  • 12.
    First, because it’snot anemic Martin Fowler http://martinfowler.com/bliki/AnemicDomainModel.html
  • 13.
    <?php class Anemic { private $shouldNotChangeAfterCreation; private $shouldBeChangedOnlyOnEdit; private $couldBeChangedAnytime; public function setCouldBeChangedAnytime($couldBeChangedAnytime) {} public function setShouldBeChangedOnlyOnEdit($shouldBeChangedOnlyOnEdit) {} public function setShouldNotChangeAfterCreation($shouldNotChangeAfterCreation {} }
  • 14.
    <?php class SomeService { public function doStuffOnCreation() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldNotChangeAfterCreation('def'); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
  • 15.
    <?php class SomeService { public function doStuffOnEdit() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldBeChangedOnlyOnEdit(‘123’); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
  • 16.
    <?php class SomeService { public function doStuffOnEdit() { Loss of memory $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldBeChangedOnlyOnEdit(‘123’); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
  • 17.
    <?php class SomeService { public function doStuffOnEdit() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldBeChangedOnlyOnEdit(‘123’); $anemic->setShouldNotChangeAfterCreation('def'); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
  • 18.
    <?php class BehaviouralClass { private $shouldNotChangeAfterCreation; private $shouldBeChangedOnlyOnEdit; private $couldBeChangedAnytime; public function __construct($shouldNotChangeAfterCreation, $couldBeChangedAnytime) { $this->shouldNotChangeAfterCreation = $shouldNotChangeAfterCreation; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } public function modify($shouldBeChangedOnlyOnEdit, $couldBeChangedAnytime = null) { $this->shouldBeChangedOnlyOnEdit = $shouldBeChangedOnlyOnEdit; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } }
  • 19.
    <?php class BehaviouralClass { private $shouldNotChangeAfterCreation; private $shouldBeChangedOnlyOnEdit; private $couldBeChangedAnytime; It's not still rich, lacks of ... public function __construct($shouldNotChangeAfterCreation, $couldBeChangedAnytime) { $this->shouldNotChangeAfterCreation = $shouldNotChangeAfterCreation; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } public function modify($shouldBeChangedOnlyOnEdit, $couldBeChangedAnytime = null) { $this->shouldBeChangedOnlyOnEdit = $shouldBeChangedOnlyOnEdit; $this->couldBeChangedAnytime = $couldBeChangedAnytime; } }
  • 20.
    Ubiquitous Language “thedomain model can provide the backbone for that common language [...]. The vocabulary of that ubiquitius language includes the names of classes and prominent operations” (Eric Evans, "Domain Driven Design")
  • 21.
    What does “coffee”mean? Alberto Brandolini AKA @ziobrando
  • 22.
    A bit ofstrategy
  • 23.
    Our domain isan online game that simulate the soccer’s world.
  • 24.
  • 25.
    What does “player”mean in our domain?
  • 26.
    The meaning of“player” Within game engine context a model of a real soccer player, modelled with behaviours to fit the requirements of the game engine.
  • 27.
    The meaning of“player” Within data import context a model of a real soccer player, but modelled for a simple CRUD.
  • 28.
    The meaning of“player” Within user profile context a model of the user of the website, who plays the game.
  • 29.
    League Team CalendarPlayer Lineup Coach Core context
  • 30.
    League Team CalendarPlayer Lineup Coach (the player in “user context”) Core context
  • 31.
    Player Game contextUser context Uuid Name Roles Uuid Email Password
  • 32.
  • 33.
    <?php class League { private $id; private $name; private $teams; public function __construct(Uuid $uuid, $name) { $this->id = $uuid; $this->name = $name; $this->teams = new ArrayCollection(); } public function registerTeam(Team $team) { $this->teams->add($team); } }
  • 34.
    <?php class League { private $id; private $name; private $teams; A team must do a registration public function __construct({ to the league Uuid $uuid, $name) $this->id = $uuid; $this->name = $name; $this->teams = new ArrayCollection(); } public function registerTeam(Team $team) { $this->teams->add($team); } }
  • 35.
    <?php class League { private $id; private $genericInfo; private $teams; public function __construct(Uuid $uuid, LeagueGenericInfo $leagueGenericInfo) { $this->id = $uuid; $this->genericInfo = $leagueGenericInfo; $this->teams = new ArrayCollection(); } // ...}
  • 36.
    <?php class LeagueGenericInfo { private $name; private $description; private $country; public function __construct($country, $description, $name) { $this->country = $country; $this->description = $description; $this->name = $name; } // … getters and behaviours}
  • 37.
    <?php class LeagueGenericInfo { private $name; private $description; private $country; Value object public function __construct($country, $description, $name) { $this->country = $country; $this->description = $description; $this->name = $name; } // … getters and behaviours}
  • 38.
  • 39.
    <?php class League { // ... public function registerTeam(Team $team) { if (!$this->canLeagueAcceptAnotherRegistration()) { throw new DomainException('Not more places available'); } $this->teams->add($team); } private function canLeagueAcceptAnotherRegistration() { if ($this->teams->count() == 8) { return false; } return true; } }
  • 40.
    <?php class League { // ... public function registerTeam(Team $team) { if (!$this->canLeagueAcceptAnotherRegistration()) { League protects its invariants throw new DomainException('Not more places available'); } $this->teams->add($team); } private function canLeagueAcceptAnotherRegistration() { if ($this->teams->count() == 8) { return false; } return true; } }
  • 41.
    <?php class League { // ... public function getTeams() { return $this->teams; } }
  • 42.
    <?php class League { // ... private function getTeams() { return $this->teams; } }
  • 43.
    <?php class LeagueGenericInfo { private $name; private $description; private $country; private static $countries; public function __construct($country, $description, $name) { if(!isset(static::$countries)) { static::$countries = require __DIR__.'/countries.php'; } if (!array_key_exists($name, static::$countries)) { throw new UnknownCountryException($country); } $this->country = $country; // .. thanks to Mathias Verraes for “Money” ;-) }}
  • 44.
    <?php class LeagueGenericInfo { private $name; private $description; private $country; Input validation private static $countries; public function __construct($country, $description, $name) { if(!isset(static::$countries)) { static::$countries = require __DIR__.'/countries.php'; } if (!array_key_exists($name, static::$countries)) { throw new UnknownCountryException($country); } $this->country = $country; // .. thanks to Mathias Verraes for “Money” ;-) }}
  • 45.
    <?php class LeagueGenericInfo { private $name; private $description; private $country; Could be private static also $countries; placed in commands public function __construct($country, $description, $name) { if(!isset(static::$countries)) { static::$countries = require __DIR__.'/countries.php'; } if (!array_key_exists($name, static::$countries)) { throw new UnknownCountryException($country); } $this->country = $country; // .. thanks to Mathias Verraes for “Money” ;-) }}
  • 46.
    <?php class League { public function render() { $properties = [ 'id' => $this->id, 'name' => $this->genericInfo->getName(), 'description' => $this->genericInfo->getDescription(), 'country' => $this->genericInfo->getCountry(), ]; $teams = new ArrayCollection(); foreach ($this->teams as $team) { $teams->add($team->render()); } $properties['teams'] = $teams; return new ArrayCollection($properties); } }
  • 47.
    <?php class League { public function render() { $properties = [ 'id' => $this->id, 'name' => $this->genericInfo->getName(), 'description' => $this->genericInfo->getDescription(), 'country' => $this->genericInfo->getCountry(), ]; Return a read-only object $teams = new ArrayCollection(); foreach ($this->teams as $team) { $teams->add($team->render()); } $properties['teams'] = $teams; return new ArrayCollection($properties); } }
  • 48.
    <?php class Team { private $id; private $players; public function __construct(Uuid $uuid) { $this->id = $uuid; $this->players = new ArrayCollection(); } public function firePlayer($id) { foreach ($this->players as $key => $player) { if ($player->getId() == $id) { $this->players->remove($key); } } } }
  • 49.
    <?php class Team { private $id; private $players; Traverse the collections public function __construct(Uuid $uuid) { $this->id = $uuid; $this->players = new ArrayCollection(); } public function firePlayer(Player $playerToFire) { foreach ($this->players as $key => $player) { if ($player->getId() == $playerToFire->getId()) { $this->players->remove($key); } } } }
  • 50.
    The Player shouldhave a relation towards the Team?
  • 51.
  • 52.
    <?php class LeagueTestextends PHPUnit_Framework_TestCase { /** * @test * @expectedException */ public function leagueMustHaveMaximumEightTeams() { // … $genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); $league = new League($uuid, $genericInfo); $team = $this->getMockBuilder('Team') // … for ($x=0; $x<=8; $x++) { $league->registerTeam($team); } } }
  • 53.
    <?php class LeagueTestextends PHPUnit_Framework_TestCase { /** * @test * @expectedException */ public function leagueMustHaveMaximumEightTeams() { // … $genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); $league = new League($uuid, $genericInfo); $team = $this->getMockBuilder('Team') // … for ($x=0; $x<=8; $x++) { $league->registerTeam($team); } } }
  • 54.
    <?php class LeagueTestextends PHPUnit_Framework_TestCase { /** * @test * @expectedException */ public function leagueMustHaveMaximumEightTeams() { // … The same Team can do more than one registration to the League?!? $genericInfo = new LeagueGenericInfo('it', 'my league', 'awesome league'); $league = new League($uuid, $genericInfo); $team = $this->getMockBuilder('Team') // … for ($x=0; $x<=8; $x++) { $league->registerTeam($team); } } }
  • 55.
    <?php class League { //.. public function registerTeam(Team $team) { $this->canLeagueAcceptRegistrationOf($team); $this->teams->add($team); } private function canLeagueAcceptRegistrationOf(Team $applicantTeam) { if (!$this->canLeagueAcceptAnotherRegistration()) { throw new DomainException('Not more places available'); } foreach ($this->teams as $key => $team) { if ($team->getId() == $applicantTeam->getId()) { throw new DomainException('Team already registered'); } } } }
  • 56.
    <?php class League { //.. public function registerTeam(Team $team) { $this->canLeagueAcceptRegistrationOf($team); $this->teams->add($team); } And so on ... private function canLeagueAcceptRegistrationOf(Team $applicantTeam) { if (!$this->canLeagueAcceptAnotherRegistration()) { throw new DomainException('Not more places available'); } foreach ($this->teams as $key => $team) { if ($team->getId() == $applicantTeam->getId()) { throw new DomainException('Team already registered'); } } } }
  • 57.
  • 58.
  • 59.
    We are usingthe entities of the Persistence Model as entities of our Domain Awareness Model
  • 60.
    League.orm.yml League: type:entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY
  • 61.
    League.orm.yml League: type:entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY
  • 62.
    League.orm.yml League: type:entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY Uuid.orm.yml Uuid: type: embeddable id: uuid: type: string length: 36
  • 63.
    League.orm.yml League: type:entity table: league embedded: id: class: ValueObjectUuid genericInfo: class: ValueObjectLeagueGenericInfo oneToMany: contratti: targetEntity: Team mappedBy: league fetch: EXTRA_LAZY Uuid.orm.yml Uuid: type: embeddable id: uuid: type: string length: 36
  • 64.
    <?php class TeamRepositoryimplements TeamRepositoryInterface { private $em; public function __construct(EntityManager $em) { $this->em = $em; } }
  • 65.
    Persisting entities <?php class TeamRepository implements TeamRepositoryInterface { public function add(Team $team) { $this->em->persist($team); $this->em->flush(); } }
  • 66.
    Avoid collection hydration (foreach, toArray) <?php class TeamRepository implements TeamRepositoryInterface { public function getWithoutPlayers($id) { $qb = $this->em->createQueryBuilder(); $qb ->select('t', 'p') ->from("Team", 't') ->leftJoin('t.players', 'p', Join::WITH, $qb->expr()->andX( $qb->expr()->eq('p.id.uuid', ':pid') )) ->where('c.id.uuid = :id') ->setMaxResults(1); $qb->setParameter('id', $id); $qb->setParameter('pid', null); return $qb->getQuery()->getOneOrNullResult(); } }
  • 67.
    Retrieve an objectjoined with empty collection <?php class TeamRepository implements TeamRepositoryInterface { public function getWithPlayers($id) { $qb = $this->em->createQueryBuilder(); $qb ->select('t', 'p') ->from("Team", 't') ->leftJoin(t.players', 'p', Join::WITH, $qb->expr()->andX( $qb->expr()->eq('p.status', ':status') )) ->where('t.id.uuid = :id'); $qb->setParameter('status', 'on_the_market'); $qb->setParameter('id', $id); return $qb->getQuery()->getOneOrNullResult(); } }
  • 68.
    Get paginated listof Teams with Player joined <?php use DoctrineORMToolsPaginationPaginator; class TeamRepository implements TeamRepositoryInterface { public function paginate($first, $max) { $qb = $this->em->createQueryBuilder(); $qb ->select('t', 'p') ->from("Team", 't') ->leftJoin('t.players', 'p') ->setFirstResult($first) ->setMaxResults($max); $paginator = new Paginator($qb->getQuery()); return $paginator->getIterator(); } }
  • 69.
  • 70.
    <?php class FirePlayerCommandimplements Command { public $teamId; public $playerId; public function getRequest() { return new Request( [ 'teamId' => $this->teamId, 'playerId' => $this->playerId ] ); } }
  • 71.
    <?php class Requestextends ArrayCollection implements RequestInterface { public function __construct(array $values) { parent::__construct($values); } public function get($key, $default = null) { if (!parent::containsKey($key)) { throw new DomainException(); } $value = parent::get($key); if (!$value && $default) { return $default; } return $value; } }
  • 72.
    <?php class CommandHandler { private $dispatcher; private $useCases; public function __construct(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } public function registerUseCases($useCases) { foreach ($useCases as $useCase) { if ($useCase instanceof UseCase) { $this->useCases[$useCase->getManagedCommand()] = $useCase; } else { throw new LogicException(''); } } } // ...}
  • 73.
    <?php class CommandHandler { // ... public function execute(Command $command) { try { $this->dispatcher ->dispatch(Events::PRE_COMMAND, new CommandEvent($command)); $this->useCases[get_class($command)]->run($command); $response = new Response(); $this->dispatcher ->dispatch(Events::POST_COMMAND, new PostCommandEvent($command, $response)); return $response; } catch (DomainException $e) { $this->dispatcher ->dispatch(Events::EXCEPTION, new ExceptionEvent($command, $e)); return new Response($e->getMessage(), Response::STATUS_KO); } } }
  • 74.
    <?php class CommandHandler { // ... public function execute(Command $command) { try { $this->dispatcher ->dispatch(Events::PRE_COMMAND, new CommandEvent($command)); $this->useCases[get_class($command)]->run($command); $response = new Response(); $this->dispatcher ->dispatch(Events::POST_COMMAND, new PostCommandEvent($command, $response)); return $response; } catch (DomainException $e) { $this->dispatcher ->dispatch(Events::EXCEPTION, new ExceptionEvent($command, $e)); return new Response($e->getMessage(), Response::STATUS_KO); } } }
  • 75.
    <?php class FirePlayerUseCaseimplements UseCase { private $repository; public function __construct(TeamRepositoryInterface $repository) { $this->repository = $repository; } public function run(Command $command) { $request = $command->getRequest(); $team = $this->repository->get( $request->get('teamId') ); $team->firePlayer( $request->get('playerId') ); $this->repository->add($team); } }
  • 76.
    Commands and UseCases could be used standalone
  • 77.
    <?php class CommandHandlerCompilerPassimplements CompilerPassInterface { public function process(ContainerBuilder $container) { if (!$container->hasDefinition('command_handler')) { return; } $definition = $container->getDefinition('command_handler'); $taggedServices = $container->findTaggedServiceIds('use_case'); $useCases = array(); foreach ($taggedServices as $id => $attributes) { $useCases[] = new Reference($id); } $definition->addMethodCall( 'registerUseCases', array($useCases) ); } }
  • 78.
    <service id="use_case.fire_player" public="false"class="UseCaseFirePlayerUseCase"> <argument type="service" id="repository_team"/> <tag name="use_case" /> </service>
  • 79.
    <?php class MyControllerextends Controller { public function modifyLeagueAction(Request $request, $id) { $reader = $this->get('reader'); $league = $reader->getLeague($id); $command = ModifyLeagueCommand::fromArray($league); $form = $this->createForm(new ModifyLeagueType(), $command); $form->handleRequest($request); if ($form->isValid()) { $commandHandler = $this->get('command_handler'); $response = $commandHandler->execute($command); if ($response->isOk()) { //... } } return array( 'form' => $form->createView() ); } }
  • 80.
    <?php class MyControllerextends Controller { public function modifyLeagueAction(Request $request, $id) { $reader = $this->get('reader'); $league = $reader->getLeague($id); $command = ModifyLeagueCommand::fromArray($league); $form = $this->createForm(new ModifyLeagueType(), $command); $form->handleRequest($request); if ($form->isValid()) { $commandHandler = $this->get('command_handler'); $response = $commandHandler->execute($command); if ($response->isOk()) { //... } } return array( 'form' => $form->createView() ); } }
  • 81.
    <?php class MyControllerextends Controller { public function modifyLeagueAction(Request $request, $id) { $reader = $this->get('reader'); Consider using a service for $league = $reader->getLeague($id); reading $command = ModifyLeagueCommand::operations, fromArray($league); instead $form = $this->createForm(new ModifyLeagueType(), $command); use $form->the handleRequest($repository request); directly if ($form->isValid()) { $commandHandler = $this->get('command_handler'); $response = $commandHandler->execute($command); if ($response->isOk()) { //... } } return array( 'form' => $form->createView() ); } }
  • 82.
    <?php class ModifyLeagueTypeextends CreateNewsType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('leagueId’, ‘’hidden') ->add('name') ->add('save', 'submit') ; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => ModifyLeagueCommand::CLASS, )); } }
  • 83.
    <?php class ModifyLeagueCommandimplements Command { public $leagueId; public $name; public function getRequest() { return new Request( [ leagueId => $this->leagueId, name => $this->name ] ); } }
  • 84.
    Command validation ModifyLeagueCommand: properties: leagueId: - NotBlank: ~ name: - NotBlank: ~
  • 85.
    Using CQRS andEvent Sourcing things change deeply
  • 87.
  • 88.
  • 89.
    Credits A specialthanks to @_orso_ the first who told me about rich models - Eric Evans - "Domain Driven Design" - Vaughn Vernon - “Implementing Domain-Driven Design” - http://www.slideshare.net/ziobrando/gestire-la-complessit-con-domain-driven-design - http://verraes.net/2013/12/related-entities-vs-child-entities/ - http://www.whitewashing.de/2012/08/22/building_an_object_model__no_setters_allowed.html - http://www.infoq.com/articles/ddd-contextmapping - http://nicolopignatelli.me/valueobjects-a-php-immutable-class-library/ - http://welcometothebundle.com/domain-driven-design-and-symfony-for-simple-app/ - http://www.slideshare.net/perprogramming/application-layer-33335917 - http://lostechies.com/jimmybogard/2008/08/21/services-in-domain-driven-design/ - http://www.slideshare.net/thinkddd/practical-domain-driven-design-cqrs-and-messaging-architectures - http://lostechies.com/jimmybogard/2009/02/15/validation-in-a-ddd-world/ - http://gojko.net/2009/09/30/ddd-and-relational-databases-the-value-object-dilemma/
  • 90.
    Credits - http://verraes.net/2013/06/unbreakable-domain-models/ - http://www.mehdi-khalili.com/orm-anti-patterns-part-4-persistence-domain-model - http://martinfowler.com/bliki/BoundedContext.html - http://www.substanceofcode.com/2007/01/17/from-anemic-to-rich-domain-model/ - http://gorodinski.com/blog/2012/04/25/read-models-as-a-tactical-pattern-in-domain-driven-design-ddd/ - http://www.sapiensworks.com/blog/post/2013/05/01/DDD-Persisting-Aggregate-Roots-In-A-Unit-Of-Work.aspx - http://simon-says-architecture.com/2011/09/06/ddd-by-the-book/ - http://scaledagileframework.com/domain-modeling/ - http://www.codeproject.com/Articles/555855/Introduction-to-CQRS

Editor's Notes

  • #7 A model is a simplification. It is an interpretation of reality that abstracts the aspects relevant to solving problem at hand and ignores extraneous detail.
  • #25 il matching uno a uno tra sottodominio e bounded context è una condizione desiderabile, non un vincolo
  • #52 il modello cambia spesso, tanto quanto la conoscenza che acquisiamo e che ci porta a comprendere come risolvere i problemi; TDD si sposa bene
  • #61 non accoppiare con le annotations
  • #65 non accoppiare con le annotations
  • #66 non accoppiare con le annotations
  • #67 non accoppiare con le annotations
  • #68 non accoppiare con le annotations
  • #69 non accoppiare con le annotations
  • #71 non accoppiare con le annotations
  • #72 non accoppiare con le annotations
  • #73 non accoppiare con le annotations
  • #74 non accoppiare con le annotations
  • #75 non accoppiare con le annotations
  • #76 non accoppiare con le annotations
  • #78 non accoppiare con le annotations
  • #79 non accoppiare con le annotations
  • #80 non accoppiare con le annotations
  • #81 non accoppiare con le annotations
  • #82 non accoppiare con le annotations
  • #83 non accoppiare con le annotations
  • #84 non accoppiare con le annotations
  • #85 non accoppiare con le annotations