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

Domain-driven Design in PHP - PHP Benelux 2018

Domain-driven Design in PHP - PHP Benelux 2018

Building PHP applications using Domain-driven design techniques results in code that is easier to modify, maintain, and test, and a better user experience. Once you try DDD, you will never design software in the same way again.

In this tutorial, we will start by learning how to build a strong ubiquitous language with stakeholders. Then, we will learn the benefits of encapsulating business logic in value objects using test-driven development.

Next, we will move on to using bounded contexts, entities, and aggregate roots to manage state and protect invariants. We will also cover more advanced topics in the DDD world, such as event sourcing and command query responsibility segregation.

No prior knowledge of domain-driven design required.

https://conference.phpbenelux.eu/2018/sessions/domain-driven-design-in-php/

Andrew Cassell

January 26, 2018
Tweet

More Decks by Andrew Cassell

Other Decks in Technology

Transcript

  1. 22

  2. People don't want to buy a quarter-inch drill, they want

    a quarter-inch hole. Theodore Levitt
  3. DDD CRUD/MUD Recipe Building Inventory Brewhouse Activity Beer Menu /

    Prices Brew Sessions Brewery Website Calclations QA Record Keeping
  4. 1. Naming Things 2. Cache Invalidation 3. Off By One

    Errors Top 10 Reasons Programming is Hard
  5. Ubiquitous Language $recipe->addGrains($grains); Code: We need to be able to

    add a measured amount of grain to a recipe. Business:
  6. Ubiquitous Language $recipe->addDryHop($hopQuantity,$schedule); Code: We need to add .2oz of

    12% AA per gallon of Simcoe hops to a recipe 7 days after the boil is complete. This is called dry hopping. $schedule = new DryHopSchedule(‘7 Days’); $hopQuantity = HopQuantity::(“Simcoe”,”12%AA”,”.2oz per gallon”); Business:
  7. @recipe Feature: A user should be able to dry hop

    a recipe Scenario: User adds dry hops to recipe Given I am authenticated as a user And I am on “/recipe/0ef360fd“ And I press "Add Hops” And I fill out form with: | Hop Name | | Simcoe | Then I see “Alpha Acid: 11.5 - 15.0“
  8. $hop = (new HopsSupply(…))->findOneByName(‘Simcoe’); $catalog = new RecipeCatalog(…); $recipe =

    $catalog->findOneRecipeByName(‘iPHPA’); $recipe->addDryHop($hop, new DryHopSchedule(‘7 Days’)); $brewers = new Brewers(…); $brewer = $brewers->findOneByUsername(‘cassell’); $brewSession = $brewer->brew($recipe); $brewSession->dryHopsWereAdded($hop, Datetime::now());
  9. $hop = (new HopsSupplyRepo())->findOneByName(‘Simcoe’); $catalog = new RecipeCatalogRepository(…); $recipe =

    $catalog->findOneRecipeByName(‘iPHPA’); $recipe->addDryHop($hop, new DryHopSchedule(‘7 Days’)); $brewers = new BrewersRepository(…); $brewer = $brewers->findOneByUsername(‘cassell’); $brewSession = $brewer->brew($recipe); $brewSession->dryHopsWereAdded($hop, Datetime::now());
  10. CRUD/MUD DDD Users Brewers String RecipeName Integer IBU Float(7,3) AlphaAcid

    Array of ORM Objects GrainBill BeerInventoryRepositoryInterface Menu
  11. $hop = (new HopsSupply())->findOneByName(‘Centennial Pellets’); $catalog = new RecipeCatalog(…); $recipe

    = $catalog->findOneRecipeByName(‘iPHPA’); $recipe->addDryHop($hop, new DryHopSchedule(‘7 Days’)); $brewers = new Brewers(…); $brewer = $brewers->findOneByUsername(‘cassell’); $brewSession = $brewer->brew($recipe); $brewSession->dryHopsWereAdded($hop, Datetime::now());
  12. […], we want to establish the idea that a computer

    language is not just a way of getting a computer to perform operations but rather that it is a novel formal medium for expressing ideas about methodology. Thus, programs must be written for people to read, and only incidentally for machines to execute.
  13. Photos: Terry Brown and Gordon Stettinius Alice Shane P.J. Sam

    George Male Age 32 Brewer for 3 Years Got into brewing after the record store he worked at closed. Is very tech savvy and carries an Android. Male Age 50 Brewing for 10 Years Just Promoted to COO Retired civil servant who believes software can make a brewery for regulatory compliant. Female Age 27 New Hire Library science graduate who was just hired away from the local university. iPhone user. Male Age 52 Librarian for 30 years Has been brewing beer as long as he can remember. Loves beer podcasts but is not a fan of technology. Male Age 46 Brewer for 20 Years Loves poetry and writing. Brewing is just a job to pay the bills.
  14. 7 Dirty Words When Meeting With a Domain Expert 1.Session

    2.Repository 3.Abstract 4.Interface 5.Class 6.Database 7.Foreign Key
  15. Domain Event • Beer Was Brewed • Grain Was Added

    • Hops Were Added • Brewer Changed Email Address
  16. Command • Start Brewing Session • Add Grain to Recipe

    • Add Hops To Fermenter • Change Email Address
  17. Actor • Who did it? • If you only have

    one user you might not need this
  18. Value Object Immutable No Identity (Only Values) Hop Name Amount

    Paid Temperature Entity Identifiable Mutable Lifecycle Contains Value Objects Line Item Brewer Water Chemistry Aggregate Entity Responsible For Child Entities Transaction Boundary Invoice Recipe Brew Session
  19. Value Objects Email Address Recipe Name Date We Brewed On

    Weight Temperature Ubiquitous Language
  20. Plain Old PHP Objects (POPOs) • Declare Class Properties as

    Private • No Setters (behaviors will return new) • No References to Mutable Objects • Throw Exceptions in Constructor Value Objects
  21. cassell:beeriously cassell$ docker run --rm --interactive --tty --network beeriously_default --volume

    `pwd`:/app --user : --workdir /app beeriously_php-fpm /app/vendor/bin/phpunit --configuration /app/src/Tests/Unit/ phpunit.xml.dist PHPUnit 6.4.3 by Sebastian Bergmann and contributors. FEE 3 / 3 (100%) Time: 2.34 seconds, Memory: 4.00MB There were 2 errors: 1) Beeriously\Tests\Unit\Domain\Recipe\RecipeNameTest::testGetter Error: Class 'Beeriously\Domain\Recipe\RecipeName' not found /app/src/Tests/Unit/Domain/Recipe/RecipeNameTest.php:20 2) Beeriously\Tests\Unit\Domain\Recipe\RecipeNameTest::testToString Error: Class 'Beeriously\Domain\Recipe\RecipeName' not found /app/src/Tests/Unit/Domain/Recipe/RecipeNameTest.php:26 -- There was 1 failure: 1) Beeriously\Tests\Unit\Domain\Recipe\RecipeNameTest::testEmptyFails Failed asserting that exception of type "Error" matches expected exception "Beeriously\Domain\Recipe\InvalidRecipeNameException". Message was: "Class 'Beeriously\Domain\Recipe\RecipeName' not found" at /app/src/Tests/Unit/Domain/Recipe/RecipeNameTest.php:15 . ERRORS! Tests: 3, Assertions: 1, Errors: 2, Failures: 1.
  22. cassell:beeriously cassell$ docker run --rm --interactive --tty --network beeriously_default --volume

    `pwd`:/app --user : --workdir /app beeriously_php-fpm /app/ vendor/bin/phpunit --configuration /app/src/Tests/Unit/phpunit.xml.dist PHPUnit 6.4.3 by Sebastian Bergmann and contributors. .EE 3 / 3 (100%) Time: 1.56 seconds, Memory: 4.00MB There were 2 errors: 1) Beeriously\Tests\Unit\Domain\Recipe\RecipeNameTest::testGetter Error: Call to undefined method Beeriously\Domain\Recipe\RecipeName::getValue() /app/src/Tests/Unit/Domain/Recipe/RecipeNameTest.php:21 2) Beeriously\Tests\Unit\Domain\Recipe\RecipeNameTest::testToString Object of class Beeriously\Domain\Recipe\RecipeName could not be converted to string /app/src/Tests/Unit/Domain/Recipe/RecipeNameTest.php:27 ERRORS! Tests: 3, Assertions: 2, Errors: 2. cassell:beeriously cassell$
  23. docker run --rm --interactive --tty --network beeriously_default --volume `pwd`:/app --

    user : --workdir /app beeriously_php-fpm /app/vendor/bin/phpunit --configuration /app/ src/Tests/Unit/phpunit.xml.dist PHPUnit 6.4.3 by Sebastian Bergmann and contributors. ..E 2 / 3 (67%) Time: 292 ms, Memory: 4.00MB There was 1 error: 1) Beeriously\Tests\Unit\Domain\Recipe\RecipeNameTest::testToString Object of class Beeriously\Domain\Recipe\RecipeName could not be converted to string /app/src/Tests/Unit/Domain/Recipe/RecipeNameTest.php:27 ERRORS! Tests: 35, Assertions: 78, Errors: 1. make: *** [unit] Error 2 cassell:beeriously cassell$
  24. cassell:beeriously cassell$ docker run --rm --interactive --tty --network beeriously_default --volume

    `pwd`:/app --user : --workdir /app beeriously_php-fpm /app/vendor/bin/phpunit --configuration /app/src/Tests/ Unit/phpunit.xml.dist PHPUnit 6.4.3 by Sebastian Bergmann and contributors. ... 3 / 3 (100%) Time: 192 ms, Memory: 4.00MB OK (3 tests, 4 assertions)
  25. ABV

  26. Sugar + Yeast = Alcohol + CO 2 C H

    O ->2C H OH + 2CO 6 12 6 2 5 2
  27. Identifiable Have State and are Mutable (Lifecycle) Never in an

    Invalid State Operate using Value Objects No Security or Permission Checks *(Ideally) Storage Agnostic Entities
  28. Making an Entity: Brewer Identifiable ID (UUID) Mutable $brewer->changeUsername $brewer->changeName

    $brewer->changeEmail Never in an Invalid State __construct Operate Using Value Objects FullName, EmailAddress, …
  29. Mathias Verraes - Decoupling the Model from the Framework at

    Laracon EU 2014 https://www.youtube.com/watch?v=QaIGN_cTcc8
  30. Ruby Midwest 2011 - Keynote: Architecture the Lost Years by

    Robert Martin https://www.youtube.com/watch?v=WpkDN78P884
  31. Domain Events • Part of the Core Domain • Happened

    In The Past • Important Enough To Record (Persist) • Important Enough To Concern Other Bounded Contexts • Immutable Value Objects • Do Not Contain Entities or Other Mutable Objects • Can Be Created in an Entity
  32. Event Sourcing • Object Properties are Not Persisted • Events

    Are Persisted to 
 Append Only Event Storage
  33. • Avoids Data Mapping • Avoids Object-relational Impedance Mismatch •

    Reduces Database Table Counts (Related Tables) • Potentially Reduces Model Counts Event Sourcing
  34. https://joind.in/talk/93cfd DDD Topics Covered •Ubiquitous Language •Event Storming •Modelling •Value

    Objects •Entities •Aggregates •Hexagonal Architecture •CQRS •Event Sourcing