models and service layers, hemoglobin and hobgoblins
DESCRIPTION
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.TRANSCRIPT
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?
Surgeon General's Warning
Notice in accordance with the PHP Disarmament Compact of 1992. Void where prohibited.
ZendCon 2014
Models & Service Layers
Hemoglobin & Hobgoblins
Ross Tuck
Freerange CodemonkeyKnow-It-All
Hot-Air Balloon
About Today
Hemoglobin
Anemia.
Objects can too.
class TodoList {
function setName($name);
function getName();
function setStatus($status);
function getStatus();
function addTask($task);
function setTasks($tasks);
function getTasks();
}
Model
array(
'name' => '',
'status' => '',
'tasks' => ''
);
Model
Bad Thing TM
“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
Our industry standard is an antipattern.
Ouch.
Important Note
Models
Stuf
Integration over implementation
Our Setup
class TodoList {
function setName($name);
function getName();
function setStatus($status);
function getStatus();
function addTask($task);
function setTasks($tasks);
function getTasks();
}
Model
class Task {
function setDescription($desc);
function getDescription();
function setPriority($priority);
function getPriority();
}
Model
An ORM that's not Doctrine 2.A framework that's not Symfony2.
I promise.
CRUD
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');
}
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');
}
Controller
Anemic ModelHard to Maintain
TestabilitySRP wha?
In Defense Of CRUD.No, seriously.
Low Barrier to Entry.
Easy to follow.If you can keep it in your head.
Sometimes it really is just data entry.(but it usually isn't)(but sometimes it is)
Not entirely a technical issue.
Service Layer
• 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
Application Service
Model
Controller
View
Model
Service Layer
Controller
View
Why?
1) Multiple User InterfacesWeb + REST API
+ CLI+ Workers
2) “In between” Logic
3) Decouple from frameworks
Model
Service Layer
Controller
View
Just Build The Stupid Thing
ServiceLayer
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');
}
Controller
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!');
}
}
Service
class TodoService {
function findById($id) {
return $this->repository->findById($id);
}
}
Service
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');
}
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');
Controller
CLI
function addTaskCommand($id, $desc, $priority) {
$list = $this->todoService->findById($id);
if (!$list) { throw new NotFoundException(); }
$this->todoService->addTask($list, $desc, $priority);
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');
Controller
function addTaskCommand($id, $desc, $priority) {
$list = $this->todoService->findById($id);
if (!$list) { throw new NotFoundException(); }
$this->todoService->addTask($list, $desc, $priority);
CLI
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');
Controller
CLI
function addTaskCommand($id, $desc, $priority) {
$list = $this->todoService->findById($id);
if (!$list) { throw new NotFoundException(); }
$this->todoService->addTask($list, $desc, $priority);
class TodoService {
function findById($id) {
return $this->repository->findById($id);
}
}
Service
class TodoService {
function findById($id) {
$todo = $this->repository->findById($id);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
Service
not http exception
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');
Controller
CLI
function addTaskCommand($id, $desc, $priority) {
$list = $this->todoService->findById($id);
$this->todoService->addTask($list, $desc, $priority);
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');
Controller
CLI
function addTaskCommand($id, $desc, $priority) {
$list = $this->todoService->findById($id);
$this->todoService->addTask($list, $desc, $priority);
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');
Controller
CLI
function addTaskCommand($name, $desc, $priority) {
$list = $this->todoService->findByName($name);
$this->todoService->addTask($list, $desc, $priority);
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');
Controller
CLI
function addTaskCommand($name, $desc, $priority) {
$list = $this->todoService->findByName($name);
$this->todoService->addTask($list, $desc, $priority);
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');
Controller
CLI
function addTaskCommand($name, $desc, $priority) {
$list = $this->todoService->findByName($name);
$this->todoService->addTask($list, $desc, $priority);
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');
Controller
CLI
function addTaskCommand($name, $desc, $priority) {
$listId = $this->todoService->findIdByName($name);
$this->todoService->addTask($listId, $desc, $priority);
class TodoService {
public function findLatestLists() {
return $this->repository->findLatestLists();
}
}
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;
}
}
Service
Indirect Advantages
Readability
Interface Protection
Discoverability
class TodoService {
function findById($id);
function addTask($todo, $desc, $priority);
function prance();
}
Service
Mission Accomplished
class TodoList {
function setName($name);
function getName();
function setStatus($status);
function getStatus();
function setTasks($tasks);
function getTasks();
}
Model
Dumb as a box of rocks.
class TodoList {
function setName($name);
function getName();
function setStatus($status);
function getStatus();
function setTasks($tasks);
function getTasks();
}
Model
Where's mah logic?
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!');
}
}
Service
“Organizes business logic by procedures where each procedure handles a single
request from the presentation.”-Fowler
Transaction Scripts
Simple
More flexibleThan CRUD, at least
Don't scale quite as well
What does belong in a service layer?
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!');
}
}
Service
Orchestration
TransactionsSecurity
NotificationsBulk operations
Facade
Fat Model, Skinny Controller
Fat Model, Skinny Service Layer
(re)Thinking
addTask()
findById()
findLatestLists()
Service
writereadread
Remodeling our Reading
by
Refactoring our Repository
Redux
class TodoService {
function findById($id) {
$todo = $this->repository->findById($id);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
Service
interface TodoRepository {
public function findById($id);
public function findLatestLists();
}
Repository
class TodoDbRepository implements TodoRepository {
public function findById($id) {
$todo = $this->db->select(...);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
Repository
class TodoDbRepository implements TodoRepository {
public function findById($id) {
$todo = $this->db->select(...);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
Repository
raw db connection
class TodoDbRepository implements TodoRepository {
public function findById($id) {
$todo = $this->repository->find($id);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
Repository
FIXED
interface TodoRepository {
public function findById($id);
public function findLatestLists();
}
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);
}
Repository
interface TodoRepository {
public function findById($id);
public function findLatestLists();
}
Repository
class TodoService {
function findById($id) {
$todo = $this->repository->findById($id);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
Service
class TodoService {
function findById($id) {
return $this->repository->findById($id);
}
}
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;
}
}
Service
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;
}
}
Repository
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;
}
}
Repository Decorator Decorator object
TodoDbRepository
new TodoService(
new CachingTodoRepository(
new TodoDbRepository(
$entityManager->getRepository('TodoList')
)
)
)
DI Layer
The Inverse Biggie Law
Mo' classesMo' decoupling and reduced overall design issues
Too many finder methods?
$this->todoService->matching(array(
new ListIsClosedCriteria(),
new HighPriorityCriteria()
));
Controller
Doctrine\Criteria
Interlude: Services here...
...services there...
...services everywhere!
Task
TaskService
TodoList
TodoListService
Tag
TagService
TaskRepository TodoListRepository TagRepository
Task
TaskService
TodoList
TodoListService
Tag
TagService
TaskRepository TodoListRepository TagRepository
Task
TodoList
TodoService
Tag
Task
TodoList
TodoService
Tag
User
UserService
Task
TodoList
TodoService
Tag
User
UserService
class TodoListService {
public function findByUser(User $user) {
return $this->repository->findByUser($user);
}
}
Service
class TodoListService {
public function findByUser(UserId $userId) {
return $this->repository->findByUser($userId);
}
}
Service
Task
TodoList
TodoService
Tag
User
UserService
Interfaces!
Services aren't only for entities
Scale can differ wildly
PrintingService
Quality of Implementation
(re)Modeling our Writing
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!');
}
}
Service
class TodoList {
function addTask(Task $task) {
$this->tasks[] = $task;
}
}
Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task();
$task->setDescription($desc);
$task->setPriority($priority);
$this->tasks[] = $task;
}
}
Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority);
$this->tasks[] = $task;
}
}
Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority, $this);
$this->tasks[] = $task;
}
}
Model
ORM allowance
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority);
$this->tasks[] = $task;
}
}
Model
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!');
}
}
Service
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority);
$this->tasks[] = $task;
}
}
Model
Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority);
$this->tasks[] = $task;
if (count($this->tasks) > 10) {
$this->status = static::UNREALISTIC;
}
}
}
Meaningful Tests
Working Together
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!');
}
}
Service
Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority);
$this->tasks[] = $task;
if (count($this->tasks) > 10) {
$this->status = static::UNREALISTIC;
}
}
}
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!');
}
}
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!');
if (count($this->tasks) > 10) {
$this->auditLog->logTooAmbitious($task);
$this->mailer->sendMessage('Too unrealistic');
}
}
}
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!');
if (count($this->tasks) > 10) {
$this->auditLog->logTooAmbitious($task);
$this->mailer->sendMessage('Too unrealistic');
}
}
}
Service
PrintingService
TodoService
Something new...
Something better...
Domain Events
Common Pattern
Observer
New usage
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;
}
}
Event
class TaskAddedEvent {
protected $description;
protected $priority;
function __construct($desc, $priority) {
$this->description = $desc;
$this->priority = $priority;
}
}
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;
}
}
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
No dispatcher
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!');
}
}
Service
class TodoListService {
function addTask(TodoList $list, $desc, $priority) {
$list->addTask($desc, $priority);
$this->repository->save($list);
}
}
Service
class TodoListService {
function addTask(TodoList $list, $desc, $priority) {
$list->addTask($desc, $priority);
$this->repository->save($list);
$events = $list->releaseEvents();
$this->eventDispatcher->dispatch($events);
}
}
Service
Event Listeners
class EmailListener {
function onTaskAdded($event) {
$taskDesc = $event->getDescription();
$this->mailer->sendMessage('New thingy: '.$taskDesc);
}
function onUserRegistered($event) {
$this->mailer->sendMessage('welcome sucka!');
}
}
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;
}
}
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));
}
}
Nice things:
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!
class TodoListService {
protected $dependency1;
protected $dependency2;
protected $dependency3;
protected $dependency4;
protected $dependency5;
protected $dependency6;
}
Service
Big ball of mud
in the making
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
PrintingService
TodoService
Serialize & Send,
Sucka!
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;
}
}
Less nice things.
Humans hate debugging events.
Dev Logging.Debug comma
nds.
Model
Service Layer
Controller
View
Model
Service Layer
Controller
View
Model
Service Layer
Controller
View
Consuming Application Services
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');
}
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');
}
Controller
function addTaskAction($req) {
$list = $this->todoService->findById($req->get('id'));
$list->addTask($req->get('desc'), $req->get('priority'));
return $this->redirect('edit_page');
}
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');
}
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');
}
Controller
Model
Service Layer
Controller
View
Model
Service Layer
Controller
View
Model
Service Layer
Controller
View
View Models
PHP version, not MVVM.
class TodoService {
function findById($id) {
$todoList = $this->repository->findById($id);
return $todoList;
}
}
Service
class TodoService {
function findById($id) {
$todoList = $this->repository->findById($id);
return new TodoDTO($todoList);
}
}
Service
class TodoDTO {
public function getName();
public function getStatus();
public function getMostRecentTask();
}
TodoDTO
class TodoService {
function generateReport() {
$data = $this->repository->performSomeCrazyQuery();
return new AnnualGoalReport($data);
}
}
Service
Ain't rocket science.
Reverse it: DTOs not for output...
...but for input.
Going Commando
Command
class AddTaskCommand {
public $description;
public $priority;
public $todoListId;
}
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');
}
}
Controller
ServiceController Handler Bar
Handler Baz
Handler Foo
ServiceController
Handler Baz
class TodoListService {
}
Service
class TodoListService {
function execute($command) {
}
}
Service
class TodoListService {
function execute($command) {
get_class($command);
}
}
Service
class TodoListService {
function execute($command) {
$command->getName();
}
}
Service
class TodoListService {
function execute($command) {
$command->execute();
}
}
Service
class TodoListService {
function execute($command) {
}
}
Service
What goes in a handler?
class TodoListHandler {
function handleAddTask($cmd) {
$list = $this->repository->findById($cmd->todoListId);
$list->addTask($cmd->description, $cmd->priority);
}
function handleCompleteTask($command)
function handleRemoveTask($command)
}
Handler
class TodoListService {
function execute($command) {
}
}
Service
class CommandBus {
function execute($command) {
}
}
Service
class MyCommandBus implements CommandBus {
function execute($command) {
}
}
Service
class ValidatingCommandBus implements CommandBus {
function execute($command) {
if (!$this->validator->isValid($command)) {
throw new InvalidCommandException();
}
$this->innerCommandBus->execute($command);
}
}
Service
Command
class AddTaskCommand {
public $description;
public $priority;
public $todoListId;
}
Command
use Symfony\Component\Validator\Constraints as Assert;
class AddTaskCommand {
/** @Assert\Length(max="50") */
public $description;
public $priority;
public $todoListId;
}
LoggingTransactions
Event Dispatching
Fewer Dependencies per class.Simple layers.Easy to test.
View Models + Commands
Model
Service Layer
Controller
View
ViewModelsCommands
CRUD for the framework.Domain Model for the chewy center.
formstemplatesvalidators
tough logicsemanticstesting
Diverge Further
CQRS
On the surface, it looks the same.
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');
}
}
Controller
CQS
Commands = Change DataQueries = Read Data
CQRS
Model
class TodoList {
function rename($name);
function addTask($desc, $priority);
function getName();
function getTasks();
}
Two Models
Write Model
class TodoListModel {
function rename($name);
function addTask($desc, $priority);
}
class TodoListView {
function getName();
function getTasks();
}
Read Model
Model
class TodoList {
function rename($name);
function addTask($desc, $priority);
function getName();
function getTasks();
function getParticipatingUsers();
}
Write Model
class TodoListModel {
function rename($name);
function addTask($desc, $priority);
}
class TodoListView {
function getName();
function getTasks();
function getParticipatingUsers();
}
Read Model
Write Model
class TodoListModel {
function rename($name);
function addTask($desc, $priority);
}
class TodoListView {
function getName();
function getTasks();
function getParticipatingUsers();
}
Read Model
ORM entity1 Model
SQL queryN Models
Read and Write are two different systems.
User and Shopping Cart?
Same kind of split.
Surrounding classes?
A lot of it looks the same.
class TodoListHandler {
function handleAddTask($cmd) {
$list = $this->repository->findById($cmd->todoListId);
$list->addTask($cmd->description, $cmd->priority);
}
}
Handler
class TodoListService {
public function findByUser(User $user) {
return $this->repository->findByUser($user);
}
}
Service
class TodoListService {
public function findByUser(User $user) {
return $this->repository->findByUser($user);
}
}
Service
class TodoListHandler {
function handleAddTask($cmd) {
$list = $this->repository->findById($cmd->todoListId);
$list->addTask($cmd->description, $cmd->priority);
}
}
Handler
$todoList = new TodoList();
$this->repository->save($todoList);
$todoList->getId();
Controller
$command = new CreateTodoCommand(UUID::create());
$commandBus->execute($command);
$command->uuid;
Controller
Zoom Out
Martin Fowler waz here
Domain events
DB Views
BigHonkingQueue
github.com/beberlei/litecqrs-php/github.com/qandidate-labs/broadway
github.com/gregoryyoung/m-r
Pros & Cons
Big mental leap.Usually more LOC.
Not for every domain.Can be mixed.
Easy to Scale.Bears Complexity.Async Operations.
Event Sourcing.
Event Sourcing?
CQRS+
Event Sourcing
Instead of storing the current state in the db...
...store the domain events?
SnapshotsDebuggingAudit Log
Business IntelligenceOnline/Offline users
Retroactively Fix Bugs
Google it.Or ask me afterwards.
Epilogue
"A foolish consistency is the hobgoblin of little minds."
- Ralph Waldo Emerson
Strong opinions, weakly held.
Strong techniques, weakly held.
PHP 3
PHP 4 -5
PHP 5.3+
PHP 7
Might seem crazy.
Bang for the buck.
People ARE doing this.
It IS working for them.
You can too.
Questions?
Further Reading• codebetter.com/gregyoung• martinfowler.com/tags/domain driven design.html• shawnmc.cool/domain-driven-design• whitewashing.de• verraes.net
Thanks To:
• Warnar Boekkooi @boekkooi
• Daan van Renterghem @DRvanR• Matthijs van den Bos @matthijsvandenb
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-dixjR-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_Commandments_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
joind.in/12101
@rosstuck
Ross Tuckrosstuck.com