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.
Models and Service Layers, Hemoglobin and Hobgoblins
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. Models & Service Layers
Hemoglobin & Hobgoblins
ZendCon 2014
Ross Tuck
10. Model
class TodoList {
function setName($name);
function getName();
function setStatus($status);
function getStatus();
function addTask($task);
function setTasks($tasks);
function getTasks();
}
13. “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
23. Model
class TodoList {
function setName($name);
function getName();
function setStatus($status);
function getStatus();
function addTask($task);
function setTasks($tasks);
function getTasks();
}
24. Model
class Task {
function setDescription($desc);
function getDescription();
function setPriority($priority);
function getPriority();
}
25. An ORM that's not Doctrine 2.
A framework that's not Symfony2.
I promise.
37. • 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
58. Service
class TodoService {
function findById($id) {
$todo = $this->repository->findById($id);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
not http exception
66. Service
class TodoService {
public function findLatestLists() {
return $this->repository->findLatestLists();
}
}
67. 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;
}
}
74. Model
class TodoList {
function setName($name);
function getName();
function setStatus($status);
function getStatus();
function setTasks($tasks);
function getTasks();
}
76. Model
class TodoList {
function setName($name);
function getName();
function setStatus($status);
function getStatus();
function setTasks($tasks);
function getTasks();
}
Where's mah logic?
77. 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!');
}
}
79. “Organizes business logic by procedures
where each procedure handles a single
request from the presentation.”
-Fowler
94. Service
class TodoService {
function findById($id) {
$todo = $this->repository->findById($id);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
96. Repository
class TodoDbRepository implements TodoRepository {
public function findById($id) {
$todo = $this->db->select(...);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
97. Repository
class TodoDbRepository implements TodoRepository {
public function findById($id) {
$todo = $this->db->select(...);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
raw db connection
98. Repository
class TodoDbRepository implements TodoRepository {
public function findById($id) {
$todo = $this->repository->find($id);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
FIXED
100. 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);
}
102. Service
class TodoService {
function findById($id) {
$todo = $this->repository->findById($id);
if (!$todo) {
throw new TodoNotFoundException();
}
return $todo;
}
}
104. 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;
}
}
105. 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;
}
}
106. 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
107. DI Layer
new TodoService(
new CachingTodoRepository(
new TodoDbRepository(
$entityManager->getRepository('TodoList')
)
)
)
131. 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!');
}
}
132. Model
class TodoList {
function addTask(Task $task) {
$this->tasks[] = $task;
}
}
133. Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task();
$task->setDescription($desc);
$task->setPriority($priority);
$this->tasks[] = $task;
}
}
134. Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority);
$this->tasks[] = $task;
}
}
135. Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority, $this);
$this->tasks[] = $task;
}
}
ORM allowance
136. Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority);
$this->tasks[] = $task;
}
}
137. 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!');
}
}
138. Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority);
$this->tasks[] = $task;
}
}
139. Model
class TodoList {
function addTask($desc, $priority) {
$task = new Task($desc, $priority);
$this->tasks[] = $task;
if (count($this->tasks) > 10) {
$this->status = static::UNREALISTIC;
}
}
}
166. 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!
167. Service
class TodoListService {
protected $dependency1;
protected $dependency2;
protected $dependency3;
protected $dependency4;
protected $dependency5;
protected $dependency6;
}
Big ball of mud
in the making
168. 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
189. Service
class TodoService {
function findById($id) {
$todoList = $this->repository->findById($id);
return $todoList;
}
}
190. Service
class TodoService {
function findById($id) {
$todoList = $this->repository->findById($id);
return new TodoDTO($todoList);
}
}
191. TodoDTO
class TodoDTO {
public function getName();
public function getStatus();
public function getMostRecentTask();
}
193. Service
class TodoService {
function generateReport() {
$data = $this->repository->performSomeCrazyQuery();
return new AnnualGoalReport($data);
}
}
211. Handler
class TodoListHandler {
function handleAddTask($cmd) {
$list = $this->repository->findById($cmd->todoListId);
$list->addTask($cmd->description, $cmd->priority);
}
function handleCompleteTask($command)
function handleRemoveTask($command)
}
215. Service
class ValidatingCommandBus implements CommandBus {
function execute($command) {
if (!$this->validator->isValid($command)) {
throw new InvalidCommandException();
}
$this->innerCommandBus->execute($command);
}
}
232. Write Model
class TodoListModel {
function rename($name);
function addTask($desc, $priority);
}
Read Model
class TodoListView {
function getName();
function getTasks();
}
234. Model
class TodoList {
function rename($name);
function addTask($desc, $priority);
function getName();
function getTasks();
function getParticipatingUsers();
}
235. Write Model
class TodoListModel {
function rename($name);
function addTask($desc, $priority);
}
Read Model
class TodoListView {
function getName();
function getTasks();
function getParticipatingUsers();
}
236. 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
242. Handler
class TodoListHandler {
function handleAddTask($cmd) {
$list = $this->repository->findById($cmd->todoListId);
$list->addTask($cmd->description, $cmd->priority);
}
}
243. Service
class TodoListService {
public function findByUser(User $user) {
return $this->repository->findByUser($user);
}
}
244. 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);
}
}
246. $todoList = new TodoList();
$this->repository->save($todoList);
$todoList->getId();
Controller
247. $command = new CreateTodoCommand(UUID::create());
$commandBus->execute($command);
$command->uuid;
Controller