Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Rich domain model with symfony 2.5 and doctrine 2.5

10,159 views

Published on

The slides of my talk at Symfony Day Italy 2014

Published in: Technology
  • Be the first to comment

Rich domain model with symfony 2.5 and doctrine 2.5

  1. 1. Rich Domain Model with Symfony2 and Doctrine2 Leonardo Proietti @_leopro_ Symfony Day Italy Milan, 10 October 2014
  2. 2. twitter.com/_leopro_ github.com/leopro linkedin.com/in/leonardoproietti
  3. 3. 1. Domain Modeling
  4. 4. domain = problem space domain model = solution space
  5. 5. Domain “A Domain [...] is what an organization does and the world it does in.” (Vaughn Vernon, “Implementing Domain-Driven Design”)
  6. 6. 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")
  7. 7. A model
  8. 8. Domain Model “In DDD, domain model refers to a class.” (Julie Lerman, http://msdn.microsoft.com/en-us/magazine/dn385704.aspx)
  9. 9. Domain Model “An object model of the domain that incorporates both behavior and data.” (Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
  10. 10. Domain Model “An object model of the domain that incorporates both behavior and data.” (Martin Fowler, http://martinfowler.com/eaaCatalog/domainModel.html)
  11. 11. Why Rich?
  12. 12. First, because it’s not anemic Martin Fowler http://martinfowler.com/bliki/AnemicDomainModel.html
  13. 13. <?php class Anemic { private $shouldNotChangeAfterCreation; private $shouldBeChangedOnlyOnEdit; private $couldBeChangedAnytime; public function setCouldBeChangedAnytime($couldBeChangedAnytime) {} public function setShouldBeChangedOnlyOnEdit($shouldBeChangedOnlyOnEdit) {} public function setShouldNotChangeAfterCreation($shouldNotChangeAfterCreation {} }
  14. 14. <?php class SomeService { public function doStuffOnCreation() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldNotChangeAfterCreation('def'); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
  15. 15. <?php class SomeService { public function doStuffOnEdit() { $anemic = new Anemic(); $anemic->setCouldBeChangedAnytime('abc'); $anemic->setShouldBeChangedOnlyOnEdit(‘123’); $unitOfWork->persist($anemic); $unitOfWork->flush(); } }
  16. 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. 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. 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. 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. 20. 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")
  21. 21. What does “coffee” mean? Alberto Brandolini AKA @ziobrando
  22. 22. A bit of strategy
  23. 23. Our domain is an online game that simulate the soccer’s world.
  24. 24. Bounded Contexts
  25. 25. What does “player” mean in our domain?
  26. 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. 27. The meaning of “player” Within data import context a model of a real soccer player, but modelled for a simple CRUD.
  28. 28. The meaning of “player” Within user profile context a model of the user of the website, who plays the game.
  29. 29. League Team Calendar Player Lineup Coach Core context
  30. 30. League Team Calendar Player Lineup Coach (the player in “user context”) Core context
  31. 31. Player Game context User context Uuid Name Roles Uuid Email Password
  32. 32. Data and Behaviours
  33. 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. 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. 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. 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. 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. 38. Validation: invariants and input
  39. 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. 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. 41. <?php class League { // ... public function getTeams() { return $this->teams; } }
  42. 42. <?php class League { // ... private function getTeams() { return $this->teams; } }
  43. 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. 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. 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. 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. 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. 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. 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. 50. The Player should have a relation towards the Team?
  51. 51. Be iterative using TDD/BDD
  52. 52. <?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); } } }
  53. 53. <?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); } } }
  54. 54. <?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); } } }
  55. 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. 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. 57. 2. Doctrine (v2.5)
  58. 58. Awareness
  59. 59. We are using the entities of the Persistence Model as entities of our Domain Awareness Model
  60. 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. 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. 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. 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. 64. <?php class TeamRepository implements TeamRepositoryInterface { private $em; public function __construct(EntityManager $em) { $this->em = $em; } }
  65. 65. Persisting entities <?php class TeamRepository implements TeamRepositoryInterface { public function add(Team $team) { $this->em->persist($team); $this->em->flush(); } }
  66. 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. 67. 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(); } }
  68. 68. 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(); } }
  69. 69. 3. Symfony (v2.5)
  70. 70. <?php class FirePlayerCommand implements Command { public $teamId; public $playerId; public function getRequest() { return new Request( [ 'teamId' => $this->teamId, 'playerId' => $this->playerId ] ); } }
  71. 71. <?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; } }
  72. 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. 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. 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. 75. <?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); } }
  76. 76. Commands and Use Cases could be used standalone
  77. 77. <?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) ); } }
  78. 78. <service id="use_case.fire_player" public="false" class="UseCaseFirePlayerUseCase"> <argument type="service" id="repository_team"/> <tag name="use_case" /> </service>
  79. 79. <?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() ); } }
  80. 80. <?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() ); } }
  81. 81. <?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() ); } }
  82. 82. <?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, )); } }
  83. 83. <?php class ModifyLeagueCommand implements Command { public $leagueId; public $name; public function getRequest() { return new Request( [ leagueId => $this->leagueId, name => $this->name ] ); } }
  84. 84. Command validation ModifyLeagueCommand: properties: leagueId: - NotBlank: ~ name: - NotBlank: ~
  85. 85. Using CQRS and Event Sourcing things change deeply
  86. 86. ?
  87. 87. https://joind.in/talk/view/12214
  88. 88. 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/
  89. 89. 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

×