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

Applying Domain Driven Design with Symfony

Applying Domain Driven Design with Symfony

When we learn Symfony2 from the documentation, it provides us with easy and convenient ways of getting the job done. Over time our projects grow and this early convenience can become an obstacle. In this session Marek will focus on how to make our code less dependent on Symfony2 and more domain focused.

2bd48651cd01e0ca2e0a255a63da77aa?s=128

Marek Matulka

April 16, 2014
Tweet

Transcript

  1. Applying Domain Driven Design with Symfony Marek Matulka 16 Apr

    2014
  2. Marek Matulka @super_marek Software engineer at SensioLabs UK Photo by

    @cakper
  3. the easy way

  4. (not applying DDD) the easy way*

  5. Typical Symfony project...

  6. Typical Symfony project... … starts with intention to write: *

    high quality code * reusable code * portable code * code you’re proud of
  7. None
  8. Typical Symfony controller... use Symfony\Bundle\FrameworkBundle\Controller\Controller; class RegistrationController extends Controller {

    public function registerAction($id) { $conference = $this->getDoctrine() ->getRepository(‘AcmeConferenceBundle:Conference’) ->find($id); if ($this->get(‘acme_conference.enrolment.service’) ->enrol($conference, $this->getUser())) { $this->get(‘acme_conference.notification.service’) ->notify($this->getUser(), ‘success’); } } }
  9. Typical Symfony controller... Dependency Injection * used as a service

    locator Fat controllers * with actions full of business logic
  10. NO TESTS?!

  11. Typical Symfony controller... Over time it gets difficult to change.

    Then we are afraid to change it.
  12. Typical Project Architecture... Acme\TestBundle\Entity ← Models Acme\TestBundle\Resource\views ← Views Acme\TestBundle\Controller

    ← Controllers
  13. Typical Project Architecture... Acme\TestBundle\Entity ← Models Acme\TestBundle\Resource\views ← Views Acme\TestBundle\Controller

    ← Controllers FAT Controllers Thin Models
  14. Typical Symfony project...

  15. Typical Symfony project... ...ends full of: * tightly coupled objects

    * objects that are difficult to re-use * code you’re afraid to change * code you cannot test
  16. Why it ended like that?

  17. Why it ended like that? * poor understanding of the

    core domain * bad names * urge to use framework’s goodies * ContainerAware is evil * locked into framework’s structure * Acme\Bundle\TestBundle\Entity
  18. Is there another way?

  19. the right way

  20. (in my humble opinion) the right way*

  21. Forget about the framework

  22. None
  23. Forget about the framework

  24. namespace Acme\ConferenceBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class RegistrationController extends Controller { public

    function registerAction($id) { $conference = $this->getDoctrine() ->getRepository(‘AcmeConferenceBundle:Conference’) ->find($id); if ($this->get(‘acme_conference.enrolment.service’) ->enrol($conference, $this->getUser())) { $this->get(‘acme_conference.notification.service’) ->notify($this->getUser(), ‘success’); $this->redirect(‘registration_successful’) } $this->redirect(‘registration_failed’); } }
  25. Make your dependencies explicit

  26. namespace Acme\ConferenceBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class RegistrationController extends Controller { public

    function registerAction($id) { $conference = $this->getDoctrine() ->getRepository(‘AcmeConferenceBundle:Conference’) ->find($id); if ($this->get(‘acme_conference.enrolment.service’) ->enrol($conference, $this->getUser())) { $this->get(‘acme_conference.notification.service’) ->notify($this->getUser(), ‘success’); return $this->redirect(‘registration_successful’); } return $this->redirect(‘registration_failed’); } }
  27. namespace Acme\ConferenceBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class RegistrationController extends Controller { public

    function registerAction($id) { $conference = $this->getDoctrine() ->getRepository(‘AcmeConferenceBundle:Conference’) ->find($id); if ($this->get(‘acme_conference.enrolment.service’) ->enrol($conference, $this->getUser())) { $this->get(‘acme_conference.notification.service’) ->notify($this->getUser(), ‘success’); return $this->redirect(‘registration_successful’); } return $this->redirect(‘registration_failed’); } } Repository Response Service Service Value
  28. namespace Acme\ConferenceBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class RegistrationController extends Controller { public

    function registerAction($id) { $conference = $this->getDoctrine() ->getRepository(‘AcmeConferenceBundle:Conference’) ->find($id); if ($this->get(‘acme_conference.enrolment.service’) ->enrol($conference, $this->getUser())) { $this->get(‘acme_conference.notification.service’) ->notify($this->getUser(), ‘success’); return $this->redirect(‘registration_successful’); } return $this->redirect(‘registration_failed’); } } ConferenceRepository RedirectResponse ConferenceEnrolment EnrolmentNotification Conference
  29. Use interfaces to describe communication

  30. namespace Acme\ConferenceBundle\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Acme\Learning\Conference; class RegistrationController

    { /** * @Route("/conference/{id}/enrol") * @ParamConverter("conference", class="AcmeConferenceBundle:Conference") */ public function registerAction(Conference $conference) { if ($this->get(‘acme_conference.enrolment.service’) ->enrol($conference, $this->getUser())) { $this->get(‘acme_conference.notification.service’) ->notify($this->getUser(), ‘success’); return $this->redirect(‘registration_successful’); } return $this->redirect(‘registration_failed’); } }
  31. namespace Acme\ConferenceBundle\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Acme\Enrolment\ConferceEnrolment; class RegistrationController

    { public function __construct(ConferceEnrolment $enrolment) { $this->enrolment = $enrolment; } /** * @Route("/conference/{id}/enrol") * @ParamConverter("conference", class="AcmeConferenceBundle:Conference") */ public function registerAction(Conference $conference) { if ($this->enrolment->enrol($conference, $this->getUser())) { ... } } }
  32. namespace Acme\ConferenceBundle\Controller; use Acme\Enrolment\ConferceEnrolment; class RegistrationController { public function __construct(ConferceEnrolment

    $enrolment) { $this->enrolment = $enrolment; } public function registerAction(Conference $conference) { $conference = $this->repository->find($id); if ($this->enrolment->enrol($conference, $this->getUser())) { ... } } } $this->container->get(‘security.context’) ->getToken()->getUser();
  33. # src/Acme/ConferenceBundle/Resources/config/services.xml <service id="symfony.security.auth.token" class="%symfony.security.auth.token.class%" factory-service="security.context" factory-method="getToken"> </service> <service id="acme.learner"

    class="%inviqa.learner.class%" factory-service="symfony.security.auth.token" factory-method="getUser"> </service> # src/Acme/Learning/Learner.php namespace Acme\Learning; interface Learner { public function isEnrolled(); }
  34. # src/Acme/ConferenceBundle/Entity/User.php namespace Acme\ConferenceBundle\Enity\User; use Acme\Learning\Learner; use Symfony\Component\Security\Core\User\UserInterface; class User

    implements Learner, UserInterface { ... public function isEnrolled() { ... } ... }
  35. namespace Acme\ConferenceBundle\Controller; use Acme\Learning\Learner; use Acme\Enrolment\ConferceEnrolment; class RegistrationController { public

    function __construct( Learner $learner, ConferceEnrolment $enrolment ) { $this->learner = $learner; $this->enrolment = $enrolment; } public function registerAction(Conference $conference) { if ($this->enrolment->enrol($conference, $this->learner)) { ... } } }
  36. namespace Acme\ConferenceBundle\Controller; use Acme\Learning\Learner; use Acme\Enrolment\ConferceEnrolment; class RegistrationController { public

    function __construct( Learner $learner; ConferceEnrolment $enrolment ) { $this->learner = $learner; $this->enrolment = $enrolment; } public function registerAction(Conference $conference) { if ($this->enrolment->enrol($conference, $this->learner)) { ... } } } Interface
  37. namespace Acme\ConferenceBundle\Controller; use Acme\Learning\Learner; use Acme\Enrolment\ConferceEnrolment; use Symfony\Component\HttpFoundation\RedirectResponse; class RegistrationController

    { ... public function registerAction(Conference $conference) { if ($this->enrolment->enrol($conference, $this->learner)) { return new RedirectResponse(‘success’); } return new RedirectResponse(‘fail’); } }
  38. namespace Acme\ConferenceBundle\Controller; use Acme\Learning\Learner; use Acme\Enrolment\ConferceEnrolment; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Routing\RouterInterface;

    class RegistrationController { ... public function registerAction(Conference $conference) { if ($this->enrolment->enrol($conference, $this->learner)) { return new RedirectResponse( $this->router->generate(‘enrolment_successful’) ); } return new RedirectResponse( $this->router->generate(‘enrolment_failed’) ); } }
  39. namespace Acme\ConferenceBundle\Controller; ... use Acme\Enrolment\EnrolmentNotifier; class RegistrationController { ... public

    function registerAction(Conference $conference) { if ($this->enrolment->enrol($conference, $this->learner)) { $this->notifier->notify($this->learner, ‘success’); return new RedirectResponse( $this->router->generate(‘enrolment_successful’)); } return new RedirectResponse( $this->router->generate(‘enrolment_failed’)); } }
  40. am I happy?

  41. namespace Acme\ConferenceBundle\Controller; use Acme\Learning\Learner; use Acme\Enrolment\ConferceEnrolment; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Routing\RouterInterface;

    use Acme\Enrolment\EnrolmentNotifier; class RegistrationController { public function __construct( Learner $learner, ConferenceEnrolment $enrolment, EnrolmentNotifier $notifier, RouterInterface $router ) { $this->learner = $learner; $this->enrolment = $enrolment; $this->notifier = $notifier; $this->router = $router; } ... Interfaces!
  42. namespace Acme\ConferenceBundle\Controller; use Acme\Learning\Learner; use Acme\Enrolment\ConferceEnrolment; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Routing\RouterInterface;

    use Acme\Enrolment\EnrolmentNotifier; class RegistrationController { public function __construct( Learner $learner ConferenceEnrolment $enrolment, EnrolmentNotifier $notifier, RouterInterface $router ) { $this->learner = $learner; $this->enrolment = $enrolment; $this->notifier = $notifier; $this->router = $router; } ... Interfaces!
  43. INTERFACES! I SEE INTERFACES EVERYWHERE!

  44. namespace Acme\Enrolment; use Acme\Learning\Learner; use Acme\Learning\Conference; use Acme\Enrolment\ConferceEnrolment; use Acme\Enrolment\EnrolmentNotifier;

    class ConferenceEnrolmentHandler { public function __construct( ConferceEnrolment $enrolment, EnrolmentNotifier $notifier ) { $this->enrolment = $enrolment; $this->notifier = $notifier; } public function handle(Conference $conference, Learner $learner) { if ($this->enrolment->handle($conference, $learner)) { $this->notifier->notify($learner, ‘success’); return true; } return false; } }
  45. namespace Acme\ConferenceBundle\Controller; use Acme\Learning\Learner; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Routing\RouterInterface; use Acme\Enrolment\ConferenceEnrolmentHandler;

    class RegistrationController { ... public function registerAction(Conference $conference) { $route = $this->handler->handle($conference, $this->learner) ? ‘enrolment_successful’ : ‘enrolment_failed’; return new RedirectResponse( $this->router->generate($route) ); } }
  46. am I happy?

  47. Project Architecture Acme\Learning Acme\Enrolment Acme\ConferenceBundle Acme\EnrolmentAdapter

  48. Project Architecture Acme\Learning Model Acme\Enrolment Application/Services Acme\DoctrineAdapter Doctrine Acme\EnrolmentBundle Controller/View

  49. (I’ve forgotten about Doctrine!) oops!

  50. # src/Acme/Conference/ConferenceRepository.php namespace Acme\Conference; interface ConferenceRepository { public function find($id);

    } # src/Acme/DoctrineAdapter/DoctrineConferenceAdapter.php namespace Acme\DoctrineAdapter; use Doctrine\ORM\EntityRepository; use Acme\Conference\ConferenceRepository; class DoctrineConferenceAdapter extends EntityRepository implements ConferenceRepository { // ConferenceRepository::find($id) is already implemented by EntityRepository } # src/Acme/DoctrineAdapter/Resources/mapping/conference.orm.xml <entity name="Acme\Conference\Conference" table="conferences" repository-class="Acme\DoctrineAdapter\ConferenceRepository">
  51. What is Domain Driven Design?

  52. What is Domain Driven Design? Changing folder structure and locations

    won’t #DDD your project.
  53. What is Domain Driven Design? Fully understanding your domain and

    describing your business process using the same language as your business.
  54. Layered Architecture Acme\EnrolmentBundle Acme\Enrolment Acme\Learning Acme\DoctrineAdapter User Interface Application Domain

    Infrastructure
  55. Dependency Inversion “High level modules should not depend on lower

    level implementation” – Good old Uncle Bob
  56. Controller Services Domain Infrastructure User Interface Dependency Inversion

  57. Controller Services Domain Infrastructure User Interface Dependency Inversion Interfaces!

  58. Controller Services Domain Infrastructure User Interface Dependency Inversion

  59. Controller Services Domain Infrastructure User Interface Dependency Inversion Implementation detail

  60. Don’t mix layers!

  61. Know your domain

  62. Use right tools

  63. @super_marek Questions?

  64. 10% discount code: SymfonyUKmeetup valid until April 25th

  65. Thank you! @super_marek