Minimize the framework

...

and allow yourself some DDD

Milko Kosturkov

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

Fields I've worked in

SMS and Voice services

MMO Games

local positioning systems 

TV productions

Healthcare

e-commerce

websites

SEO

online video

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

What does a framework give us?

  • WebMVC
  • Easy work with I/O (request/response/console)
  • Easy routing
  • DI Containers
  • DBALs
  • Common security - CSRF, I/O sanitization, etc.
  • Data validation
  • Pretty error pages
  • Logging, cache, queues, FS abstractions, Events, I18n.....

This is all Infrastructure

"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 use case

  • a contestant is allowed to advance by a jury member
  • before advancing, the contestant has to sign a contract
  • the contract must be saved in the system
  • after signing the contract a member of the casting team scans the contract and uploads it and marks the contestant as having advanced

A quick example of 'traditional' code

ContestantsController

<?php

use App\Http\Controllers;

class ContestantController extends Controller {
    .
    .
    .
    public function advanceToRound(Request $request) {
    	
        Gate::authorize('contestant-advancement');
        
    	$params = $request->validate([
            'contestantId' => 'required|int',
            'round' => 'required|in:casting,competition',
            'signedContract' => 'required|file',
        ]);
        
        $contestant = Contestant::findOrFail($params['contestantId']);
        
        if ($contestant->jurryApprovals()->doesntExist()) {
            throw new RuntimeException(t('Not approved for advancement!'));
        }
        
        $contestant->round = $params['round'];
        $contastant->advancedByUserId = Auth::id();
        $contestant->save();
        
        $request->file('signedContract')->store('contestants/docs');
        
        return redirect(route('contestant-profile', $params['contenstantId']));
    }
    .
    .
    .
}

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
  • We are now married to out framework

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 runs on
  • reusable in different environments
<?php 

namespace MusicIdol\Domain\Contestants;

use Exceptions\ContestantNotApprovedForAdvancement;

class AdvanceToRound {

    public function __construct(
        private ActorProvider $ap,
        private ContestantAuthorizations $auth,
        private ContestantsRepository $repo,
        private Transaction $tr
    ) {}
  
    public function __invoke(AdvanceToRoundParams $params): void
    {
        $this->auth->authorizeContestantAdvancement($this->ap->actor());
    
        $contestant = $this->repo->findById($params->contestantId());
        if ($contestant->isNotApproved()) {
            throw new ContestantNotApprovedForAdvancement();
        }
    
        $contestant->round = $params->round();
        $contestant->advancedByUser = $this->ap->actor();
        $contestant->signedContract = $params->signedContract();
    
        $tr->persist($contestant);
    }
}
<?php 

namespace MusicIdol\Domain\Contestants;

class AdvanceToRoundParams {
    private int $contestantId;
    private string $round;
    private File $signedContract;
    
    private function __construct() {}
    
    public function contestantId(): int {
        return $this->contestantId;
    }
    .
    .
    .
    public static function fromArray(array $params): self
    {
        $validated = Validator::make($params, [
            'contestantId' => 'required|int',
            'round' => 'required|in:casting,competition',
            'signedContract' => 'required|file',
        ])->validate();
        
        $instance = new self();
        $instance->contestantId = $validated['contestantId'];
        $instance->round = $validated['round'];
        $instance->signedContract = $validated['signedContract'];
        
        return $instance;
    }
}
<?php 

namespace MusicIdol\Domain\Contestants;

use Excepttions\UnauthorizedToAdvanceUser;

class ContestantAuthorizations {
    
    public function authorizeContestantAdvancement(User $actor): void {
        if ($user->role !== User::ROLE_CASTING_TEAM) {
            throw new UnauthorizedToAdvanceUser();
        }
    }
    
}
<?php 

namespace MusicIdol\Domain\Users;

interface ActorProvider {
    
    public function actor(): User;
    
}
<?php 

namespace MusicIdol\Domain\Transaction;

interface Transaction {
    
    public function persist(): void;
    
}
<?php 

namespace MusicIdol\Domain\Contestants;

interface ContestantsRepository {
    
    public function findById(): Contestant;
    
}
<?php


class AdvanceToRoundController extends Controller {

    public function __contructor(
        private AdvanceToRound $action;
    ) {}
    
    public function __invoke(Request $request) {
    	$params = AdvanceToRoundParams::fromArray($request->post());
        ($this->action)($params);
        return redirect(route('contestant-profile', $request->post('contenstantId')));
    }
}

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\AdvanceToRound::class,
        'parametersFactory' => fn (R $r) => AdvanceToRoundParams::fromArray($r->post()),
        'responder' => fn (R $r, $result) => redirect(
        	route('contestant-profile', $r->post('contestantId'))
         );
    ]
],
...

And you can use all that on the CLI

[
    'match' => [
        'command' => 'advanceToRound'
    ],
    'service' => [
        'className' => MusicIdol\ContestantsManagement\AdvanceToRound::class,
        'parametersFactory' => fn (Command $c) => 
            AdvanceToRoundParams::fromArray($c->arguments()),
        'responder' => fn (Command $c, $result) => $c->info('Done');
    ]
],
...

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 bloated 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-the-framework-and-allow-yourself-some-ddd-v2

 

Made with Slides.com