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.

Marek Matulka

April 16, 2014
Tweet

More Decks by Marek Matulka

Other Decks in Programming

Transcript

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

    View full-size slide

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

    View full-size slide

  3. the easy way

    View full-size slide

  4. (not applying DDD)
    the easy way*

    View full-size slide

  5. Typical Symfony project...

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. Typical Symfony project...

    View full-size slide

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

    View full-size slide

  14. Why it ended like that?

    View full-size slide

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

    View full-size slide

  16. Is there another way?

    View full-size slide

  17. the right way

    View full-size slide

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

    View full-size slide

  19. Forget about
    the framework

    View full-size slide

  20. Forget about
    the framework

    View full-size slide

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

    View full-size slide

  22. Make your dependencies
    explicit

    View full-size slide

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

    View full-size slide

  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’);
    return $this->redirect(‘registration_successful’);
    }
    return $this->redirect(‘registration_failed’);
    }
    }
    Repository
    Response
    Service
    Service
    Value

    View full-size slide

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

    View full-size slide

  26. Use interfaces to
    describe communication

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  30. # src/Acme/ConferenceBundle/Resources/config/services.xml
    class="%symfony.security.auth.token.class%"
    factory-service="security.context"
    factory-method="getToken">

    class="%inviqa.learner.class%"
    factory-service="symfony.security.auth.token"
    factory-method="getUser">

    # src/Acme/Learning/Learner.php
    namespace Acme\Learning;
    interface Learner
    {
    public function isEnrolled();
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  37. 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!

    View full-size slide

  38. 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!

    View full-size slide

  39. INTERFACES!
    I SEE INTERFACES EVERYWHERE!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  45. # 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
    repository-class="Acme\DoctrineAdapter\ConferenceRepository">

    View full-size slide

  46. What is Domain Driven Design?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  51. Controller Services
    Domain
    Infrastructure
    User
    Interface
    Dependency Inversion

    View full-size slide

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

    View full-size slide

  53. Controller Services
    Domain
    Infrastructure
    User
    Interface
    Dependency Inversion

    View full-size slide

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

    View full-size slide

  55. Don’t mix layers!

    View full-size slide

  56. Know your domain

    View full-size slide

  57. Use right tools

    View full-size slide

  58. @super_marek
    Questions?

    View full-size slide

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

    View full-size slide

  60. Thank you!
    @super_marek

    View full-size slide