Slide 1

Slide 1 text

Applying Domain Driven Design with Symfony Marek Matulka 16 Apr 2014

Slide 2

Slide 2 text

Marek Matulka @super_marek Software engineer at SensioLabs UK Photo by @cakper

Slide 3

Slide 3 text

the easy way

Slide 4

Slide 4 text

(not applying DDD) the easy way*

Slide 5

Slide 5 text

Typical Symfony project...

Slide 6

Slide 6 text

Typical Symfony project... … starts with intention to write: * high quality code * reusable code * portable code * code you’re proud of

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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’); } } }

Slide 9

Slide 9 text

Typical Symfony controller... Dependency Injection * used as a service locator Fat controllers * with actions full of business logic

Slide 10

Slide 10 text

NO TESTS?!

Slide 11

Slide 11 text

Typical Symfony controller... Over time it gets difficult to change. Then we are afraid to change it.

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Typical Symfony project...

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Why it ended like that?

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Is there another way?

Slide 19

Slide 19 text

the right way

Slide 20

Slide 20 text

(in my humble opinion) the right way*

Slide 21

Slide 21 text

Forget about the framework

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

Forget about the framework

Slide 24

Slide 24 text

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’); } }

Slide 25

Slide 25 text

Make your dependencies explicit

Slide 26

Slide 26 text

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’); } }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Use interfaces to describe communication

Slide 30

Slide 30 text

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’); } }

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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();

Slide 33

Slide 33 text

# src/Acme/ConferenceBundle/Resources/config/services.xml # src/Acme/Learning/Learner.php namespace Acme\Learning; interface Learner { public function isEnrolled(); }

Slide 34

Slide 34 text

# 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() { ... } ... }

Slide 35

Slide 35 text

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)) { ... } } }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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’); } }

Slide 38

Slide 38 text

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’) ); } }

Slide 39

Slide 39 text

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’)); } }

Slide 40

Slide 40 text

am I happy?

Slide 41

Slide 41 text

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!

Slide 42

Slide 42 text

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!

Slide 43

Slide 43 text

INTERFACES! I SEE INTERFACES EVERYWHERE!

Slide 44

Slide 44 text

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; } }

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

am I happy?

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

(I’ve forgotten about Doctrine!) oops!

Slide 50

Slide 50 text

# 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

Slide 51

Slide 51 text

What is Domain Driven Design?

Slide 52

Slide 52 text

What is Domain Driven Design? Changing folder structure and locations won’t #DDD your project.

Slide 53

Slide 53 text

What is Domain Driven Design? Fully understanding your domain and describing your business process using the same language as your business.

Slide 54

Slide 54 text

Layered Architecture Acme\EnrolmentBundle Acme\Enrolment Acme\Learning Acme\DoctrineAdapter User Interface Application Domain Infrastructure

Slide 55

Slide 55 text

Dependency Inversion “High level modules should not depend on lower level implementation” – Good old Uncle Bob

Slide 56

Slide 56 text

Controller Services Domain Infrastructure User Interface Dependency Inversion

Slide 57

Slide 57 text

Controller Services Domain Infrastructure User Interface Dependency Inversion Interfaces!

Slide 58

Slide 58 text

Controller Services Domain Infrastructure User Interface Dependency Inversion

Slide 59

Slide 59 text

Controller Services Domain Infrastructure User Interface Dependency Inversion Implementation detail

Slide 60

Slide 60 text

Don’t mix layers!

Slide 61

Slide 61 text

Know your domain

Slide 62

Slide 62 text

Use right tools

Slide 63

Slide 63 text

@super_marek Questions?

Slide 64

Slide 64 text

10% discount code: SymfonyUKmeetup valid until April 25th

Slide 65

Slide 65 text

Thank you! @super_marek