Models and Service Layers, Hemoglobin and Hobgoblins

41,737 views

Published on

As presented at ZendCon 2014, AmsterdamPHP, PHPBenelux 2014, Sweetlake PHP and PHP Northwest 2013, an overview of some different patterns for integrating and managing logic throughout your application.

Published in: Technology, Business
0 Comments
91 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
41,737
On SlideShare
0
From Embeds
0
Number of Embeds
2,435
Actions
Shares
0
Downloads
250
Comments
0
Likes
91
Embeds 0
No embeds

No notes for slide

Models and Service Layers, Hemoglobin and Hobgoblins

  1. 1. Surgeon General's Warning This talk is clocked at 1 slide per 12.8 seconds and features unsafe amounts of code. Presenter is a registered Class 3 Fast Talker (equal to 1 Gilmore Girls episode). Viewing is not recommended for those hungover, expected to become hungover or consuming excessive amounts of caffeine. Do not watch and operate motor vehicles. If you accidentally consume this talk, flush brain with kitten pictures and seek emergency help in another talk. No hard feelings, seriously. It's almost the end of the conference, after all. Why are we even here? Notice in accordance with the PHP Disarmament Compact of 1992. Void where prohibited.
  2. 2. Models & Service Layers Hemoglobin & Hobgoblins ZendCon 2014 Ross Tuck
  3. 3. Freerange Codemonkey Know-It-All Hot-Air Balloon
  4. 4. @rosstuck rosstuck.com
  5. 5. About Today
  6. 6. Hemoglobin
  7. 7. Anemia.
  8. 8. Objects can too.
  9. 9. Model class TodoList { function setName($name); function getName(); function setStatus($status); function getStatus(); function addTask($task); function setTasks($tasks); function getTasks(); }
  10. 10. array( 'name' => '', 'status' => '', 'tasks' => '' ); Model
  11. 11. Bad ThingTM
  12. 12. “In essence the problem with anemic domain models is that they incur all of the costs of a domain model, without yielding any of the benefits.” -Martin Fowler
  13. 13. Our industry standard i s a n a n t i p a t t ern.
  14. 14. Ouch.
  15. 15. Important Note
  16. 16. Models Stuf
  17. 17. Integration over implementation
  18. 18. Our Setup
  19. 19. Model class TodoList { function setName($name); function getName(); function setStatus($status); function getStatus(); function addTask($task); function setTasks($tasks); function getTasks(); }
  20. 20. Model class Task { function setDescription($desc); function getDescription(); function setPriority($priority); function getPriority(); }
  21. 21. An ORM that's not Doctrine 2. A framework that's not Symfony2. I promise.
  22. 22. CRUD
  23. 23. Controller function addTaskAction($req) { $task = new Task(); $task->setDescription($req->get('desc')); $task->setPriority($req->get('priority')); $list = $this->todoRepo->findById($req->get('id')); if (!$list) { throw new NotFoundException(); } $task->setTodoList($list); $this->repository->save($task); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); return $this->redirect('edit_page'); }
  24. 24. Controller function addTaskAction($req) { $task = new Task(); $task->setDescription($req->get('desc')); $task->setPriority($req->get('priority')); $list = $this->todoRepo->findById($req->get('id')); if (!$list) { throw new NotFoundException(); } $task->setTodoList($list); $this->repository->save($task); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); return $this->redirect('edit_page'); }
  25. 25. Anemic Model Hard to Maintain Testability SRP wha?
  26. 26. In Defense Of CRUD. No, seriously.
  27. 27. Low Barrier to Entry.
  28. 28. Easy to follow. If you can keep it in your head.
  29. 29. Sometimes it really is just data entry. (but it usually isn't) (but sometimes it is)
  30. 30. Not entirely a technical issue.
  31. 31. Service Layer
  32. 32. • Service Layer • Service Container • Web Service • Service Oriented Architecture • Domain Service • Stateless Service • Software-as-a-service • Platform-as-a-service • Whatever-as-a-service meme • Delivery Service • Laundry Service
  33. 33. Application Service
  34. 34. Model Controller View
  35. 35. Model Service Layer Controller View
  36. 36. Why?
  37. 37. 1) Multiple User Interfaces Web + REST API + CLI + Workers
  38. 38. 2) “In between” Logic
  39. 39. 3) Decouple from frameworks
  40. 40. Model Service Layer Controller View
  41. 41. Just Build The Stupid Thing
  42. 42. Service Layer
  43. 43. Controller function addTaskAction($req) { $task = new Task(); $task->setDescription($req->get('desc')); $task->setPriority($req->get('priority')); $list = $this->todoRepo->findById($req->get('id')); if (!$list) { throw new NotFoundException(); } $task->setTodoList($list); $this->repository->save($task); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); return $this->redirect('edit_page'); }
  44. 44. Service class TodoService { function addTask(TodoList $list, $desc, $priority) { $task = new Task(); $task->setDescription($desc); $task->setPriority($priority); $task->setTodoList($list); $this->repository->save($task); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); } }
  45. 45. Service class TodoService { function findById($id) { return $this->repository->findById($id); } }
  46. 46. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); if (!$list) { throw new NotFoundException(); } $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); }
  47. 47. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); if (!$list) { throw new NotFoundException(); } $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); CLI function addTaskCommand($id, $desc, $priority) { $list = $this->todoService->findById($id); if (!$list) { throw new NotFoundException(); } $this->todoService->addTask($list, $desc, $priority);
  48. 48. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); if (!$list) { throw new NotFoundException(); } $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); function addTaskCommand($id, $desc, $priority) { $list = $this->todoService->findById($id); if (!$list) { throw new NotFoundException(); } $this->todoService->addTask($list, $desc, $priority); CLI
  49. 49. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); if (!$list) { throw new NotFoundException(); } $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); CLI function addTaskCommand($id, $desc, $priority) { $list = $this->todoService->findById($id); if (!$list) { throw new NotFoundException(); } $this->todoService->addTask($list, $desc, $priority);
  50. 50. Service class TodoService { function findById($id) { return $this->repository->findById($id); } }
  51. 51. Service class TodoService { function findById($id) { $todo = $this->repository->findById($id); if (!$todo) { throw new TodoNotFoundException(); } return $todo; } } not http exception
  52. 52. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); CLI function addTaskCommand($id, $desc, $priority) { $list = $this->todoService->findById($id); $this->todoService->addTask($list, $desc, $priority);
  53. 53. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); CLI function addTaskCommand($id, $desc, $priority) { $list = $this->todoService->findById($id); $this->todoService->addTask($list, $desc, $priority);
  54. 54. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); CLI function addTaskCommand($name, $desc, $priority) { $list = $this->todoService->findByName($name); $this->todoService->addTask($list, $desc, $priority);
  55. 55. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); CLI function addTaskCommand($name, $desc, $priority) { $list = $this->todoService->findByName($name); $this->todoService->addTask($list, $desc, $priority);
  56. 56. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); CLI function addTaskCommand($name, $desc, $priority) { $list = $this->todoService->findByName($name); $this->todoService->addTask($list, $desc, $priority);
  57. 57. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); CLI function addTaskCommand($name, $desc, $priority) { $listId = $this->todoService->findIdByName($name); $this->todoService->addTask($listId, $desc, $priority);
  58. 58. Service class TodoService { public function findLatestLists() { return $this->repository->findLatestLists(); } }
  59. 59. Service class TodoService { public function findLatestLists() { if ($this->cache->has('latest:lists')) { return $this->cache->get('latest:lists'); } $results = $this->repository->findLatestLists(); $this->cache->set('latest:lists', $results); return $results; } }
  60. 60. Indirect Advantages
  61. 61. Readability
  62. 62. Interface Protection
  63. 63. Discoverability
  64. 64. Service class TodoService { function findById($id); function addTask($todo, $desc, $priority); function prance(); }
  65. 65. Mission Accomplished
  66. 66. Model class TodoList { function setName($name); function getName(); function setStatus($status); function getStatus(); function setTasks($tasks); function getTasks(); }
  67. 67. Dumb as a box of rocks.
  68. 68. Model class TodoList { function setName($name); function getName(); function setStatus($status); function getStatus(); function setTasks($tasks); function getTasks(); } Where's mah logic?
  69. 69. Service class TodoListService { function addTask(TodoList $list, $desc, $priority) { $task = new Task(); $task->setDescription($desc); $task->setPriority($priority); $task->setTodoList($list); $this->repository->save($task); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); } }
  70. 70. “Organizes business logic by procedures where each procedure handles a single request from the presentation.” -Fowler
  71. 71. Transaction Scripts
  72. 72. Simple
  73. 73. More flexible Than CRUD, at least
  74. 74. Don't scale quite as well
  75. 75. What does belong in a service layer?
  76. 76. Service class TodoListService { function addTask(TodoList $list, $desc, $priority) { $task = new Task(); $task->setDescription($desc); $task->setPriority($priority); $task->setTodoList($list); $this->repository->save($task); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); } }
  77. 77. Orchestration
  78. 78. Transactions Security Notifications Bulk operations
  79. 79. Facade
  80. 80. Fat Model, Skinny Controller
  81. 81. Fat Model, Skinny Service Layer
  82. 82. (re)Thinking
  83. 83. addTask() findById() findLatestLists() Service write read read
  84. 84. Remodeling our Reading by Refactoring our Repository Redux
  85. 85. Service class TodoService { function findById($id) { $todo = $this->repository->findById($id); if (!$todo) { throw new TodoNotFoundException(); } return $todo; } }
  86. 86. Repository interface TodoRepository { public function findById($id); public function findLatestLists(); }
  87. 87. Repository class TodoDbRepository implements TodoRepository { public function findById($id) { $todo = $this->db->select(...); if (!$todo) { throw new TodoNotFoundException(); } return $todo; } }
  88. 88. Repository class TodoDbRepository implements TodoRepository { public function findById($id) { $todo = $this->db->select(...); if (!$todo) { throw new TodoNotFoundException(); } return $todo; } } raw db connection
  89. 89. Repository class TodoDbRepository implements TodoRepository { public function findById($id) { $todo = $this->repository->find($id); if (!$todo) { throw new TodoNotFoundException(); } return $todo; } } FIXED
  90. 90. Repository interface TodoRepository { public function findById($id); public function findLatestLists(); }
  91. 91. Repository interface EntityRepository { public function createQueryBuilder($alias); public function createResultSetMappingBuilder($alias); public function createNamedQuery($queryName); public function createNativeNamedQuery($queryName); public function clear(); public function find($id, $lockMode, $lockVersion); public function findAll(); public function findBy($criteria, $orderBy, $limit, $offset); public function findOneBy($criteria, $orderBy); public function __call($method, $arguments); public function getClassName(); public function matching(Criteria $criteria); }
  92. 92. Repository interface TodoRepository { public function findById($id); public function findLatestLists(); }
  93. 93. Service class TodoService { function findById($id) { $todo = $this->repository->findById($id); if (!$todo) { throw new TodoNotFoundException(); } return $todo; } }
  94. 94. Service class TodoService { function findById($id) { return $this->repository->findById($id); } }
  95. 95. Service class TodoService { public function findLatestLists() { if ($this->cache->has('latest:lists')) { return $this->cache->get('latest:lists'); } $results = $this->repository->findLatestLists(); $this->cache->set('latest:lists', $results); return $results; } }
  96. 96. Repository class TodoDbRepository implements TodoRepository { public function findLatestLists() { if ($this->cache->has('latest:lists')) { return $this->cache->get('latest:lists'); } $results = $this->repository->query(...); $this->cache->set('latest:lists', $results); return $results; } }
  97. 97. Repository Decorator Decorator object class CachingTodoRepository implements TodoRepository { public function findLatestLists() { if ($this->cache->has('latest:lists')) { return $this->cache->get('latest:lists'); } $results = $this->innerRepository->findLatestLists(); $this->cache->set('latest:lists', $results); return $results; } } TodoDbRepository
  98. 98. DI Layer new TodoService( new CachingTodoRepository( new TodoDbRepository( $entityManager->getRepository('TodoList') ) ) )
  99. 99. The Inverse Biggie Law
  100. 100. Mo' classes Mo' decoupling and reduced overall design issues
  101. 101. Too many finder methods?
  102. 102. Controller $this->todoService->matching(array( new ListIsClosedCriteria(), new HighPriorityCriteria() ));
  103. 103. DoctrineCriteria
  104. 104. Interlude: Services here... ...services there... ...services everywhere!
  105. 105. TaskService Task TodoListService TodoList TagService Tag TaskRepository TodoListRepository TagRepository
  106. 106. TaskService Task TodoListService TodoList TagService Tag TaskRepository TodoListRepository TagRepository
  107. 107. Task TodoService TodoList Tag
  108. 108. Task UserService TodoService TodoList Tag User
  109. 109. Task TodoService TodoList Tag UserService User
  110. 110. Service class TodoListService { public function findByUser(User $user) { return $this->repository->findByUser($user); } }
  111. 111. Service class TodoListService { public function findByUser(UserId $userId) { return $this->repository->findByUser($userId); } }
  112. 112. Task TodoService TodoList Tag Interfaces! UserService User
  113. 113. Services aren't only for entities
  114. 114. Scale can differ wildly
  115. 115. PrintingService
  116. 116. Quality of Implementation
  117. 117. (re)Modeling our Writing
  118. 118. Service class TodoListService { function addTask(TodoList $list, $desc, $priority) { $task = new Task(); $task->setDescription($desc); $task->setPriority($priority); $task->setTodoList($list); $this->repository->save($task); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); } }
  119. 119. Model class TodoList { function addTask(Task $task) { $this->tasks[] = $task; } }
  120. 120. Model class TodoList { function addTask($desc, $priority) { $task = new Task(); $task->setDescription($desc); $task->setPriority($priority); $this->tasks[] = $task; } }
  121. 121. Model class TodoList { function addTask($desc, $priority) { $task = new Task($desc, $priority); $this->tasks[] = $task; } }
  122. 122. Model class TodoList { function addTask($desc, $priority) { $task = new Task($desc, $priority, $this); $this->tasks[] = $task; } } ORM allowance
  123. 123. Model class TodoList { function addTask($desc, $priority) { $task = new Task($desc, $priority); $this->tasks[] = $task; } }
  124. 124. Service class TodoListService { function addTask(TodoList $list, $desc, $priority) { $list->addTask($desc, $priority); $this->repository->save($list); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); } }
  125. 125. Model class TodoList { function addTask($desc, $priority) { $task = new Task($desc, $priority); $this->tasks[] = $task; } }
  126. 126. Model class TodoList { function addTask($desc, $priority) { $task = new Task($desc, $priority); $this->tasks[] = $task; if (count($this->tasks) > 10) { $this->status = static::UNREALISTIC; } } }
  127. 127. Meaningful Tests
  128. 128. Working Together
  129. 129. Service class TodoListService { function addTask(TodoList $list, $desc, $priority) { $list->addTask($desc, $priority); $this->repository->save($list); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); } }
  130. 130. Model class TodoList { function addTask($desc, $priority) { $task = new Task($desc, $priority); $this->tasks[] = $task; if (count($this->tasks) > 10) { $this->status = static::UNREALISTIC; } } }
  131. 131. Service class TodoListService { function addTask(TodoList $list, $desc, $priority) { $list->addTask($desc, $priority); $this->repository->save($list); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); } }
  132. 132. class TodoListService { function addTask(TodoList $list, $desc, $priority) { $list->addTask($desc, $priority); $this->repository->save($list); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); if (count($this->tasks) > 10) { $this->auditLog->logTooAmbitious($task); $this->mailer->sendMessage('Too unrealistic'); } Service
  133. 133. class TodoListService { function addTask(TodoList $list, $desc, $priority) { $list->addTask($desc, $priority); $this->repository->save($list); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); if (count($this->tasks) > 10) { $this->auditLog->logTooAmbitious($task); $this->mailer->sendMessage('Too unrealistic'); } Service
  134. 134. TodoService PrintingService
  135. 135. Something new...
  136. 136. Something better...
  137. 137. Domain Events
  138. 138. Common Pattern
  139. 139. Observer
  140. 140. New usage
  141. 141. Model function addTask($desc, $priority) { $task = new Task($desc, $priority, $this); $this->tasks[] = $task; $this->raise( new TaskAddedEvent($this->id, $desc, $priority) ); if (count($this->tasks) > 10) { $this->status = static::UNREALISTIC; } }
  142. 142. Event class TaskAddedEvent { protected $description; protected $priority; function __construct($desc, $priority) { $this->description = $desc; $this->priority = $priority; } }
  143. 143. Model function addTask($desc, $priority) { $task = new Task($desc, $priority, $this); $this->tasks[] = $task; $this->raise( new TaskAddedEvent($this->id, $desc, $priority) ); if (count($this->tasks) > 10) { $this->status = static::UNREALISTIC; } }
  144. 144. Model class TodoList { protected $pendingEvents = array(); protected function raise($event) { $this->pendingEvents[] = $event; } public function releaseEvents() { $events = $this->pendingEvents; $this->pendingEvents = array(); return $events; } } Excellent Trait
  145. 145. No dispatcher
  146. 146. Service class TodoListService { function addTask(TodoList $list, $desc, $priority) { $list->addTask($desc, $priority); $this->repository->save($list); $this->auditLog->logNewTask($task); $this->mailer->sendMessage('New thingy!'); } }
  147. 147. Service class TodoListService { function addTask(TodoList $list, $desc, $priority) { $list->addTask($desc, $priority); $this->repository->save($list); } }
  148. 148. Service class TodoListService { function addTask(TodoList $list, $desc, $priority) { $list->addTask($desc, $priority); $this->repository->save($list); $events = $list->releaseEvents(); $this->eventDispatcher->dispatch($events); } }
  149. 149. Event Listeners class EmailListener { function onTaskAdded($event) { $taskDesc = $event->getDescription(); $this->mailer->sendMessage('New thingy: '.$taskDesc); } function onUserRegistered($event) { $this->mailer->sendMessage('welcome sucka!'); } }
  150. 150. Model function addTask($desc, $priority) { $task = new Task($desc, $priority, $this); $this->tasks[] = $task; $this->raise( new TaskAddedEvent($this->id, $desc, $priority) ); if (count($this->tasks) > 10) { $this->status = static::UNREALISTIC; } }
  151. 151. Model function addTask($desc, $priority) { $task = new Task($desc, $priority, $this); $this->tasks[] = $task; $this->raise( new TaskAddedEvent($this->id, $desc, $priority) ); if (count($this->tasks) > 10) { $this->status = static::UNREALISTIC; $this->raise(new WasTooAmbitiousEvent($this->id)); } }
  152. 152. Nice things:
  153. 153. Model function addTask($desc, $priority) { $task = new Task($desc, $priority, $this); $this->tasks[] = $task; $this->raise( new TaskAddedEvent($this->id, $desc, $priority) ); if (count($this->tasks) > 10) { $this->status = static::UNREALISTIC; $this->raise(new WasTooAmbitiousEvent($this->id)); } } Logic is here!
  154. 154. Service class TodoListService { protected $dependency1; protected $dependency2; protected $dependency3; protected $dependency4; protected $dependency5; protected $dependency6; } Big ball of mud in the making
  155. 155. Event Listeners class EmailListener { function onTaskAdded($event) { $taskName = $event->task->getName(); $this->mailer->sendMessage('New thingy: '.$taskName); } function onUserRegistered($event) { $this->mailer->sendMessage('welcome sucka!'); } } Thin. Easy to test
  156. 156. TodoService Serialize & Send, Sucka! PrintingService
  157. 157. Model function addTask($desc, $priority) { $task = new Task($desc, $priority, $this); $this->tasks[] = $task; $this->raise( new TaskAddedEvent($this->id, $desc, $priority) ); if (count($this->tasks) > 10) { $this->status = static::UNREALISTIC; } }
  158. 158. Less nice things.
  159. 159. Humans hate debugging events. Dev Logging. Debug commands.
  160. 160. Model Service Layer Controller View
  161. 161. Model Service Layer Controller View
  162. 162. Model Service Layer Controller View
  163. 163. Consuming Application Services
  164. 164. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); }
  165. 165. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $this->todoService->addTask( $list, $req->get('desc'), $req->get('priority') ); return $this->redirect('edit_page'); }
  166. 166. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $list->addTask($req->get('desc'), $req->get('priority')); return $this->redirect('edit_page'); }
  167. 167. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $list->addTask($req->get('desc'), $req->get('priority')); $list->rename('blah'); return $this->redirect('edit_page'); }
  168. 168. Controller function addTaskAction($req) { $list = $this->todoService->findById($req->get('id')); $list->addTask($req->get('desc'), $req->get('priority')); $list->rename('blah'); $this->todoService->addTask(...); return $this->redirect('edit_page'); }
  169. 169. Model Service Layer Controller View
  170. 170. Model Service Layer Controller View
  171. 171. Model Service Layer Controller View
  172. 172. View Models
  173. 173. PHP version, not MVVM.
  174. 174. Service class TodoService { function findById($id) { $todoList = $this->repository->findById($id); return $todoList; } }
  175. 175. Service class TodoService { function findById($id) { $todoList = $this->repository->findById($id); return new TodoDTO($todoList); } }
  176. 176. TodoDTO class TodoDTO { public function getName(); public function getStatus(); public function getMostRecentTask(); }
  177. 177. Service class TodoService { function generateReport() { $data = $this->repository->performSomeCrazyQuery(); return new AnnualGoalReport($data); } }
  178. 178. Ain't rocket science.
  179. 179. Reverse it: DTOs not for output...
  180. 180. ...but for input.
  181. 181. Going Commando
  182. 182. Command class AddTaskCommand { public $description; public $priority; public $todoListId; }
  183. 183. Controller function addTaskAction($req) { $command = new AddTaskCommand(); $command->description = $req->get('description'); $command->priority = $req->get('priority'); $command->todoListId = $req->get('todo_id'); $this->todoService->execute($command); return $this->redirect('edit_page'); } }
  184. 184. Handler Foo Controller Service Handler Bar Handler Baz
  185. 185. Controller Service Handler Baz
  186. 186. Service class TodoListService { }
  187. 187. Service class TodoListService { function execute($command) { } }
  188. 188. Service class TodoListService { function execute($command) { get_class($command); } }
  189. 189. Service class TodoListService { function execute($command) { $command->getName(); } }
  190. 190. Service class TodoListService { function execute($command) { $command->execute(); } }
  191. 191. Service class TodoListService { function execute($command) { } }
  192. 192. What goes in a handler?
  193. 193. Handler class TodoListHandler { function handleAddTask($cmd) { $list = $this->repository->findById($cmd->todoListId); $list->addTask($cmd->description, $cmd->priority); } function handleCompleteTask($command) function handleRemoveTask($command) }
  194. 194. Service class TodoListService { function execute($command) { } }
  195. 195. Service class CommandBus { function execute($command) { } }
  196. 196. Service class MyCommandBus implements CommandBus { function execute($command) { } }
  197. 197. Service class ValidatingCommandBus implements CommandBus { function execute($command) { if (!$this->validator->isValid($command)) { throw new InvalidCommandException(); } $this->innerCommandBus->execute($command); } }
  198. 198. Command class AddTaskCommand { public $description; public $priority; public $todoListId; }
  199. 199. Command use SymfonyComponentValidatorConstraints as Assert; class AddTaskCommand { /** @AssertLength(max="50") */ public $description; public $priority; public $todoListId; }
  200. 200. Logging Transactions Event Dispatching
  201. 201. Fewer Dependencies per class. Simple layers. Easy to test.
  202. 202. View Models + Commands
  203. 203. Model Service Layer Commands ViewModels Controller View
  204. 204. forms templates validators CRUD for the framework. Domain Model for the chewy center. tough logic semantics testing
  205. 205. Diverge Further
  206. 206. CQRS
  207. 207. On the surface, it looks the same.
  208. 208. Controller function addTaskAction($req) { $command = new AddTaskCommand(); $command->description = $req->get('description'); $command->priority = $req->get('priority'); $command->todoListId = $req->get('todo_id'); $this->commandBus->execute($command); return $this->redirect('edit_page'); } }
  209. 209. CQS
  210. 210. Commands = Change Data Queries = Read Data
  211. 211. CQRS
  212. 212. Model class TodoList { function rename($name); function addTask($desc, $priority); function getName(); function getTasks(); }
  213. 213. Two Models
  214. 214. Write Model class TodoListModel { function rename($name); function addTask($desc, $priority); } Read Model class TodoListView { function getName(); function getTasks(); }
  215. 215. Model class TodoList { function rename($name); function addTask($desc, $priority); function getName(); function getTasks(); function getParticipatingUsers(); }
  216. 216. Write Model class TodoListModel { function rename($name); function addTask($desc, $priority); } Read Model class TodoListView { function getName(); function getTasks(); function getParticipatingUsers(); }
  217. 217. Write Model class TodoListModel { function rename($name); function addTask($desc, $priority); } Read Model class TodoListView { function getName(); function getTasks(); function getParticipatingUsers(); } ORM entity 1 Model SQL query N Models
  218. 218. Read and Write are two different systems.
  219. 219. User and Shopping Cart?
  220. 220. Same kind of split.
  221. 221. Surrounding classes?
  222. 222. A lot of it looks the same.
  223. 223. Handler class TodoListHandler { function handleAddTask($cmd) { $list = $this->repository->findById($cmd->todoListId); $list->addTask($cmd->description, $cmd->priority); } }
  224. 224. Service class TodoListService { public function findByUser(User $user) { return $this->repository->findByUser($user); } }
  225. 225. Handler class TodoListHandler { Service class TodoListService { public function findByUser(User $user) { return $this->repository->findByUser($user); } } function handleAddTask($cmd) { $list = $this->repository->findById($cmd->todoListId); $list->addTask($cmd->description, $cmd->priority); } }
  226. 226. $todoList = new TodoList(); $this->repository->save($todoList); $todoList->getId(); Controller
  227. 227. $command = new CreateTodoCommand(UUID::create()); $commandBus->execute($command); $command->uuid; Controller
  228. 228. Zoom Out
  229. 229. Martin Fowler waz here
  230. 230. Domain events
  231. 231. DB Views
  232. 232. Big Honking Queue
  233. 233. github.com/beberlei/litecqrs-php/ github.com/qandidate-labs/broadway github.com/gregoryyoung/m-r
  234. 234. Pros & Cons
  235. 235. Big mental leap. Usually more LOC. Not for every domain. Can be mixed.
  236. 236. Easy to Scale. Bears Complexity. Async Operations. Event Sourcing.
  237. 237. Event Sourcing?
  238. 238. CQRS + Event Sourcing
  239. 239. Instead of storing the current state in the db...
  240. 240. ...store the domain events?
  241. 241. Snapshots Debugging Audit Log Business Intelligence Online/Offline users Retroactively Fix Bugs
  242. 242. Google it. Or ask me afterwards.
  243. 243. Epilogue
  244. 244. "A foolish consistency is the hobgoblin of little minds." - Ralph Waldo Emerson
  245. 245. Strong opinions, weakly held.
  246. 246. Strong techniques, weakly held.
  247. 247. PHP 3
  248. 248. PHP 4 -5
  249. 249. PHP 5.3+
  250. 250. PHP 7
  251. 251. Might seem crazy.
  252. 252. Bang for the buck.
  253. 253. People ARE doing this.
  254. 254. It IS working for them.
  255. 255. You can too.
  256. 256. Questions?
  257. 257. Further Reading • codebetter.com/gregyoung • martinfowler.com/tags/domain driven design.html • shawnmc.cool/domain-driven-design • whitewashing.de • verraes.net
  258. 258. Thanks To: • Warnar Boekkooi @boekkooi • Daan van Renterghem @DRvanR • Matthijs van den Bos @matthijsvandenb
  259. 259. Image Credits • http://www.flickr.com/photos/calgaryreviews/6427412605/sizes/l/ • http://msnbcmedia.msn.com/j/MSNBC/Components/Slideshows/_production/twisp_090511_ /twisp_090511_02.ss_full.jpg • http://shotthroughawindow.wordpress.com/2011/07/22/hp7b/ • http://www.sxc.hu/photo/605471 • http://martinfowler.com/bliki/images/cqrs/cqrs.png • http://www.flickr.com/photos/83346641@N00/5578975430/in/photolist-9uZH2y-8rTZL1-di xjR-ffBPiv-8SbK8K-ffS4md-6UeEGP • http://www.flickr.com/photos/lac-bac/7195938394/sizes/o/ • http://tdzdaily.org/wp-content/uploads/2013/03/Dumb-and-Dumber.png • http://upload.wikimedia.org/wikipedia/commons/1/17/Charlton_Heston_in_The_Ten_Comm andments_film_trailer.jpg • http://commons.wikimedia.org/wiki/File:Trench_construction_diagram_1914.png • http://www.flickr.com/photos/jeffreyww/4747314852/sizes/l/ • http://www.flickr.com/photos/jdhancock/3540861791/sizes/l/ • http://www.flickr.com/photos/superfantastic/50088733/sizes/l
  260. 260. joind.in/12101 Ross Tuck rosstuck.com @rosstuck

×