Minimize the framework

...

and allow yourself some DDD

Milko Kosturkov

  • PHP developer for over 12 years
  • fullstack
  • bachelor in Electronics
  • master in IT
  • contractor
  • Ty's Software

Fields I've worked in

SMS and Voice services

MMO Games

local positioning systems 

TV productions

Healthcare

e-commerce

websites

SEO

online video

Bulgaria PHP Conference

@bgphpconf

#bgphp19

What is this talk about:

  1. A quick look at the most important things in DDD
  2. The problems that occur while trying to practice it
  3. How to mitigate those problems
  4. ???
  5. Profit

Domain Driven Design

Domain Driven Design:

The important parts (IMHO)

  • Focus on the domain of the problem
  • Collaboration with the domain experts
  • Usage of Ubiquitous Language
  • Recognition of Bounded Contexts

Music Idol

Music Idol

The Domain

Help all the teams involved in the production do their job quicker, easier and with better quality...

We didn't really know what we were supposed to create

Music Idol

The Domain Experts

  • the casting team
  • the assistant directors
  • the wardrobe and makeup people
  • the producers

Music Idol

The Ubiquitous Language

  • contestant
  • list (of contestants)
  • location
  • room (blue, red, green...)
  • tape
  • time code
  • rehearsal

Music Idol

Bounded Contexts

  • contestants sign-up
  • contestant communication
  • casting management
  • video tapes management
  • user roles management

So, let's start with a quick setup...

Our file structure

No Focus on the Domain

Lost the Ubiquitous Language

Unclear Bounded Contexts

This is actually a Blog

"Screaming Architecture"

"Your architectures should tell readers about the system, not about the frameworks you used in your system."

"Uncle" Bob Martin

A Domain Oriented Structure

A quick example of 'traditional' code

ContestantController



/**
 * @Route("/contestants/notes/new", methods={"POST"}, name="contestant_note_new")
 * @IsGranted("IS_CASTING_TEAM")
 *
 */
public function noteNew(Request $request, Contestant $contestant, EventDispatcherInterface $eventDispatcher): Response
{
    $note = new Note();
    $note->setAuthor($this->getUser());
    $contestant->addNote($note);

    $form = $this->createForm(NoteType::class, $note);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $em = $this->getDoctrine()->getManager();
        $em->persist($note);
        $em->flush();

        $event = new GenericEvent($note);
        $eventDispatcher->dispatch(Events::CONTESTANT_NOTE_CREATED, $event);

        return $this->redirectToRoute('contestang', ['id' => $contestant->getId()]);
    }

    return $this->render('contestant/contestant_form_error.html.twig', [
        'contestant' => $contestant,
        'form' => $form->createView(),
    ]);
}

Authorization

Validation

Business logic

The Issues

  • Domain logic coupled with delivery logic
  • Hard to reuse
  • Hard to test
  • A lot of actions in one class lead to a lot of dependencies

An Application Service

  • kind of an entry point to your domain logic
  • holds the logic for a business case (use case)
  • returns the result of the action it performs
  • does not know about the environment it is being ran
  • reusable in different environments
namespace \MusicIdol\Domain\Contestants;

class NewNote {

    private $em;
    private $ed;
    public function __construct(EntityManagerInterface $em, EventDispatcherInterface $ed) {
        $this->em = $em;
        $this->ed = $ed;
    }
    public function __invoke(User $user, int $contestantId, string $noteText) {
        if (empty ($noteText)) {
            throw new InvalidArgumentException('The note can not be empty');
        }
        $contestant = $this->em->find(Contestant::class, $contestantId);
        if (!$contestant) {
            throw new EntityNotFoundException('Contestant does not exist');
        }
        if (!$user->isGranted('IS_CASTING_TEAM')) {
            throw new UnauthorizedException('User is not authorized to add notes');
        }
        $note = new Note();
        $note->setText($noteText);
        $note->setAuthor($user);
        $contestant->addNote($note);
        $this->em->persist($note);
        $this->em->flush();

        $event = new GenericEvent($note);
        $this->ed->dispatch(Events::CONTESTANT_NOTE_CREATED, $event);

        return true;
    }
}
    





    
/**
 * @Route("/contestants/{id}/notes/new", methods={"POST"}, name="contestant_note_new")
 *
 */
public function noteNew(Request $request, Contestant $contestant, EntityManagerInterface $em, 
    EventDispatcherInterface $ed): Response
{
    $appService = new NewNote($em, $ed);
    try {
            if ($appService($this->getUser(), $contestant, $request->request->get('note'))) {
                return $this->redirectToRoute('contestang', ['id' => $contestant->getId()]);
            }
    } catch (...) {
        /* handle error here */
    }
    return $this->render('contestant/contestant_form_error.html.twig', [
        'contestant' => $contestant,
        'form' => $form->createView(),
    ]);
}

A recap up to here:

  • More expressive structure
  • No infrastructure worries in our model
  • Distinguishable bounded contexts
  • Really dull controller actions

Action Domain Responder

Action Domain Responder

  1. The action calls the domain with parameters from the input
  2. The domain returns a result
  3. The returned data is passed to the responder
  4. The responder returns all needed meta data and representation of the domain data

Can't we automate it?

  1. Map a route to the Application service (Domain)
  2. Map the input parameters to the service's parameters
  3. Execute the service
  4. Handle any errors
  5. Pass the results to the appropriate responder

Essentially a front controller

Generic configurable request handler

[
    'match' => [
        'method' => 'GET',
        'path' => '/contestants/{id}/notes/new'
    ],
    'service' => [
        'className' => MusicIdol\ContestantsManagement\AddNote::class,
        'parametersMapping' => [
            'contestantId' => 'parameters.id',
            'textNote' => 'post.note'
        ],
        'responder' => \UI\JsonResponder::class
    ]
],
...

And you can use all that on the CLI

[
    'match' => [
        'command' => 'add-note'
    ],
    'service' => [
        'className' => MusicIdol\ContestantsManagement\AddNote::class,
        'parametersMapping' => [
            'contestantId' => '--contestant-id',
            'textNote' => '--note'
        ],
        'responder' => \UI\ConsoleResponder::class
    ]
],
...

What we achieved

  • More expressive structure
  • Focus on the Domain (the interesting stuff)
  • More reusable code
  • More testable code
  • Decoupling from the delivery mechanism
  • No more controllers!!!

The Inspirations

Domain Driven Design: Tackling Complexity in the Heart of Software 

Eric Evans

Clean Architecture Talk

https://www.youtube.com/watch?v=Nsjsiz2A9mg

"Uncle" Bob Martin

Action Domain Responder

https://github.com/pmjones/adr

Paul Jones

Thank you!

Milko Kosturkov

@mkosturkov

linkedin.com/in/milko-kosturkov

mailto: mkosturkov@gmail.com

 

These slides:

https://slides.com/milkokosturkov/minimize-framework

 

Minimize the framework and allow yourself some DDD

By Milko Kosturkov

Minimize the framework and allow yourself some DDD

By now we've all heard what Domain Driven Design is about. Some of us have actually tried it. When we did, we stumbled upon the fact that the frameworks we use do not help us a lot with shaping our code in a domain driven manner. Actually, they were in the way. DDD and the generic MVC frameworks we use are an oxymoron. Still, we don't want to loose all the sweet tools that help us deal with the HTTP and the Console. In this talk I'll tell you how we changed our view perspective on input/output/request/response, pushed the framework in the infrastructure corner and cleaned ourselves some space for sweet DDD.

  • 1,303