Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Action-Domain-Responder - PHPNW14

Action-Domain-Responder - PHPNW14

The slides for my talk about the Action-Domain-Responder design pattern at PHP Northwest 2014. If you were at my talk, please rate it: https://joind.in/11806

The example code I used in this slide deck is also on github: https://github.com/tobiasgies/adr-phpnw14

Tobias Gies

October 05, 2014
Tweet

More Decks by Tobias Gies

Other Decks in Programming

Transcript

  1. A look at MVC • Separates concerns of desktop app

    • Interactive environment • Event-driven • OLDER THAN ME! (Says the 23-year-old)
  2. Actually... Controller Model View Controller Model View Controller Model View

    Controller Model View Controller Model View Controller Model View Controller Model View
  3. Web-specific • We are on a server • In-memory desktop

    patterns won't help us • Use existing ideas as a basis • Refine towards better practices
  4. From Model to Domain • Essentially identical responsibilities: – Communicate

    with data source – Encapsulate business rules • Renamed to reflect more modern concepts: – Domain Logic – Domain Driven Design
  5. A controller does too much. • Controller's methods: – indexAction

    – createAction – editAction – deleteAction – … • Dependency Injection nightmare • Single Responsibility Principle?!
  6. An action method does too much. • An action method's

    job: – Make sense of input – Pass input to Model – Check model's results – Figure out which response to send – Prepare and return response – … • Single Responsibility Principle?!
  7. From Controller to Action • One class per Action •

    Sharper focus: – Make sense of input – Pass input to Domain – Pass Domain's results to Responder
  8. A View is not enough • A view's task: –

    Accept application data – Build output from data and templates • View does not take care of: – Deciding which data type to output – Sending neccessary HTTP headers • View is Response, we need a Responder
  9. Components • Domain: The logic to manipulate data and application

    state, based on your business rules. • Responder: Handles everything connected to building an HTTP response. Headers, cookies, status codes and response content are built here. • Action: Connects the Domain and Responder. Passes input data to Domain, passes Domain output to Responder.
  10. class BlogController { private $request; private $response; private $model; private

    $view; public function __construct( Request $request, Response $response, BlogRepository $model, TemplateSystem $view ) { /* ... */ } public function indexAction() { /* ... */ } public function createAction() { if ($this->request->isPost()) { $data = $this->request->getParameters(Request::SOURCE_POST); $post = $this->model->createPost($data); if ($post->isValid()) { $post->save(); $this->response->redirect('/blog/edit/' . $post->getId()); } else { $this->response->setContent( $this->view->render('create.tpl', array('post' => $post)) ); } } else { $this->response->setContent( $this->view->render('create.tpl', array('blog' => $this->model->getDefault())) ); } } public function editAction() { /* ... */ } public function deleteAction() { /* ... */ } }
  11. public function createAction() { if ($this->request->isPost()) { $data = $this->request->getParameters(Request::SOURCE_POST);

    $post = $this->model->createPost($data); if ($post->isValid()) { $post->save(); $this->response->redirect('/blog/edit/' . $post->getId()); } else { $this->response->setContent( $this->view->render( 'create.tpl', array('post' => $post) ) ); } } else { $this->response->setContent( $this->view->render( 'create.tpl', array('blog' => $this->model->getDefault()) ) ); } }
  12. class BlogCreateAction { private $request; private $domain; private $responder; public

    function __construct( Request $request, BlogRepository $domain, BlogCreateResponder $responder ) { /* ... */ } public function __invoke() { if ($this->request->isPost()) { $data = $this->request->getParameters( Request::SOURCE_POST ); $post = $this->domain->createPost($data); if ($post->isValid()) { $post->save(); } } else { $post = $this->domain->getDefault(); } $this->responder->__invoke($post); } }
  13. class BlogCreateResponder { private $response; private $templateSystem; public function __construct(

    Response $response, TemplateSystem $templateSystem ) { /* ... */ } public function __invoke(BlogPost $post) { if (!empty($post->getId())) { $this->response->redirect('/blog/edit/' . $post->getId()); } else { $this->response->setContent( $this->templateSystem->render( 'create.tpl', array('post' => $post) ) ); } } }
  14. class BlogAddAction { private $domain; private $responder; public function __construct(

    BlogRepository $domain, BlogCreateResponder $responder ) { /* ... */ } public function __invoke() { $post = $this->domain->getDefault(); $this->responder->__invoke($post); } }
  15. class BlogCreateAction { private $request; private $domain; private $responder; public

    function __construct( Request $request, BlogRepository $domain, BlogCreateResponder $responder ) { /* ... */ } public function __invoke() { try { $data = $this->request->getParameters( Request::SOURCE_POST ); $post = $this->domain->createPost($data); } catch (BlogException $ex) { $post = $this->domain->getDefault(); } $this->responder->__invoke($post); } }
  16. class BlogCreateResponder { private $response; private $templateSystem; public function __construct(

    Response $response, TemplateSystem $templateSystem ) { /* ... */ } public function __invoke(BlogPost $post) { if (!empty($post->getId())) { $this->response->redirect('/blog/edit/' . $post->getId()); } else { $this->response->setContent( $this->templateSystem->render( 'create.tpl', array('post' => $post) ) ); } } }