Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. The Action-Domain-Responder
    Design Pattern

    View full-size slide

  2. Code's over here:
    https://github.com/tobiasgies/adr-phpnw14

    View full-size slide

  3. Design Pattern?

    View full-size slide

  4. Design Pattern!

    View full-size slide

  5. A look at MVC

    Separates concerns
    of desktop app

    Interactive
    environment

    Event-driven

    OLDER THAN ME!
    (Says the 23-year-old)

    View full-size slide

  6. A look at MVC
    Controller
    Model
    View

    View full-size slide

  7. Actually...
    Controller
    Model
    View
    Controller
    Model
    View
    Controller
    Model
    View
    Controller
    Model
    View
    Controller
    Model
    View
    Controller
    Model
    View
    Controller
    Model
    View

    View full-size slide

  8. MVC on the web
    Controller
    Model
    View
    SYSTEM
    BO
    UN
    DARY

    View full-size slide

  9. Let's refine this.

    View full-size slide

  10. 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

    View full-size slide

  11. 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

    View full-size slide

  12. A controller does too much.

    Controller's methods:
    – indexAction
    – createAction
    – editAction
    – deleteAction
    – …

    Dependency Injection nightmare

    Single Responsibility Principle?!

    View full-size slide

  13. 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?!

    View full-size slide

  14. From Controller to Action

    One class per Action

    Sharper focus:
    – Make sense of input
    – Pass input to Domain
    – Pass Domain's results to Responder

    View full-size slide

  15. 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

    View full-size slide

  16. From View to Responder

    Sends HTTP headers

    Assembles output

    Returns Response

    View full-size slide

  17. 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.

    View full-size slide

  18. Enough talking,
    let's look at the code.

    View full-size slide

  19. 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() { /* ... */ }
    }

    View full-size slide

  20. 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())
    )
    );
    }
    }

    View full-size slide

  21. Let's refactor this.

    View full-size slide

  22. 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);
    }
    }

    View full-size slide

  23. 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)
    )
    );
    }
    }
    }

    View full-size slide

  24. Can we clean up further?

    View full-size slide

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

    View full-size slide

  26. 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);
    }
    }

    View full-size slide

  27. 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)
    )
    );
    }
    }
    }

    View full-size slide

  28. Action-Domain-Responder

    View full-size slide

  29. Any questions?

    View full-size slide

  30. Thank you!
    [email protected] - @tobiasgies
    https://joind.in/11806

    View full-size slide