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/

F4bb45b2a18ee44c4a28b1664de150bd?s=128

Andrew Cassell

January 26, 2018
Tweet

Transcript

  1. Domain-Driven Design Workshop Domain-Driven Design Workshop Andrew Cassell @alc277 andrewcassell.com

  2. None
  3. None
  4. None
  5. None
  6. None
  7. None
  8. None
  9. None
  10. Domain-Driven Design

  11. Domain-Driven Design

  12. None
  13. Common Sense Software Development

  14. None
  15. None
  16. None
  17. None
  18. None
  19. None
  20. None
  21. None
  22. 22

  23. None
  24. Encapsulation & Immutability & Modeling & Behavior

  25. Domain

  26. Domain

  27. BEER

  28. None
  29. None
  30. None
  31. None
  32. Photo: Kathleen Pierce - Bangor Daily News

  33. None
  34. None
  35. None
  36. None
  37. None
  38. None
  39. None
  40. None
  41. None
  42. None
  43. None
  44. Brewing Software

  45. None
  46. None
  47. None
  48. None
  49. None
  50. None
  51. None
  52. None
  53. None
  54. None
  55. recipe hop grain hop_inventory grain_inventory recipe_hop_link user recipe_user_link brew_event ROLE_USER

    ROLE_ADMIN
  56. Source: http://jonclaytonbiz.com/

  57. None
  58. None
  59. People don't want to buy a quarter-inch drill, they want

    a quarter-inch hole. Theodore Levitt
  60. None
  61. None
  62. None
  63. #JBTD (http://jobstobedone.org/)

  64. None
  65. YYYYY

  66. Why? Why? Why? Why? Why?

  67. Search Caluclation List Automation

  68. DDD !=

  69. None
  70. None
  71. None
  72. None
  73. None
  74. http://www.csharpstar.com/

  75. None
  76. DDD CRUD Correctness Testability Usability Maintainability Modifiability

  77. None
  78. DDD BALL OF MUD / CRUD Correctness Testability Usability Maintainability

    Modifiability
  79. DDD CRUD/MUD Recipe Building Inventory Brewhouse Activity Beer Menu /

    Prices Brew Sessions Brewery Website Calclations QA Record Keeping
  80. Who is responsible

  81. None
  82. None
  83. None
  84. None
  85. Ubiquitous Language

  86. Lingua franca

  87. Ubiquitous Language

  88. Ubiquitous Language Developers Domain Experts

  89. Ubiquitous Language

  90. User Brewer Employee Taster Customer

  91. None
  92. 1. Naming Things 2. Cache Invalidation 3. Off By One

    Errors Top 10 Reasons Programming is Hard
  93. $recipe = BeerRecipe::findOrFail($uuid);
 $hop = HopRepo::where(“name”,”=“,”Simcoe”)->take(1)->get(); 
 if (!$recipe->hopsArray()->contains(‘hop_id’,$hop->id)) {


    
 $recipe->hopsArray()->attach($hop->id, [‘weight' => 1.0]);
 }
 
 
 $recipe->save();
  94. None
  95. Ubiquitous Language $recipe->addGrains($grains); Code: We need to be able to

    add a measured amount of grain to a recipe. Business:
  96. Ubiquitous Language $recipe->getEstimatedAlcoholByVolumeABV(); Code: We need to be able to

    calculate an estimated ABV for a recipe. Business:
  97. Ubiquitous Language $storage = (new StorageFacilities())->findByOneName(“Silo #4”); $storage->howMuchOfGrainType(new GrainType(‘Winter Wheat’));

    Business: Code: We need to know how much Winter Wheat is in our storage silo #4.
  98. 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:
  99. @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“
  100. $recipe->dryHop($hop); $recipe->addIngredient($hop); $recipe->addToBoil($hop); $recipe->addToBoilKettle($hop); $recipe->addToBoiler($hop); $recipe->addFirstWortHop($hop); $recipe->addToFermenter($hop); $recipe->addToMashTun($hop); //fails

  101. $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());
  102. $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());
  103. CRUD/MUD DDD Users Brewers String RecipeName Integer IBU Float(7,3) AlphaAcid

    Array of ORM Objects GrainBill BeerInventoryRepositoryInterface Menu
  104. None
  105. None
  106. None
  107. $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());
  108. […], 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.
  109. DDD BALL OF MUD/CRUD Correctness ✓ Testability Usability Maintainability Modifiability

  110. None
  111. The Brewer, designed and engraved in the Sixteenth Century, by

    Jost Amman.
  112. Inventory Marketing Finance Production “Hop”

  113. Quantity at a Location Flavor Expenditure Ingredient “Hop”

  114. Bounded Contexts

  115. Inventory Recipes Brewhouse

  116. Slide: Vaughn Vernon

  117. None
  118. Inventory Recipes Brewhouse

  119. Inventory Recipes Brewhouse

  120. Message Inventory Recipes Brewhouse

  121. Domain Event Inventory Recipes Brewhouse

  122. None
  123. None
  124. None
  125. None
  126. None
  127. Personas

  128. adaptivepath.com

  129. 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.
  130. None
  131. None
  132. None
  133. None
  134. https://blog.intercom.io/using-job-stories-design-features-ui-ux/

  135. None
  136. None
  137. 7 Dirty Words When Meeting With a Domain Expert 1.Session

    2.Repository 3.Abstract 4.Interface 5.Class 6.Database 7.Foreign Key
  138. User Research

  139. Photo: Mathias Verraes

  140. Command Actor Model Domain Event

  141. Domain Event • Something that happens in the software. •

    Past Tense
  142. Domain Event • Beer Was Brewed • Grain Was Added

    • Hops Were Added • Brewer Changed Email Address
  143. None
  144. None
  145. None
  146. None
  147. None
  148. None
  149. None
  150. None
  151. None
  152. Command Actor Model Domain Event

  153. Command • The action that precipitates the event • Written

    in Present Tense
  154. Command • Start Brewing Session • Add Grain to Recipe

    • Add Hops To Fermenter • Change Email Address
  155. None
  156. None
  157. Command Actor Model Domain Event

  158. Model • Command is acted upon this thing • Might

    Include Supporting Models
  159. Model • Recipe • Ingredient • Brewer

  160. None
  161. Command Actor Model Domain Event

  162. Actor • Who did it? • If you only have

    one user you might not need this
  163. None
  164. None
  165. Command Actor Model Domain Event

  166. None
  167. None
  168. business origami

  169. Jess McMullin Business Origami

  170. Jess McMullin Business Origami

  171. Jess McMullin Business Origami

  172. Sean Jalleh

  173. None
  174. None
  175. Source: Jeff Patton

  176. None
  177. None
  178. None
  179. None
  180. None
  181. DDD BALL OF MUD/CRUD Correctness ✓✓ Testability Usability ✓ Maintainability

    Modifiability
  182. Domain Objects Value Objects Domain Events Entities Aggregates

  183. 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
  184. Value Objects

  185. Email Address Recipe Name Date We Brewed On Weight Temperature

    Value Objects
  186. Value Objects Email Address Recipe Name Date We Brewed On

    Weight Temperature Ubiquitous Language
  187. Value Objects EmailAddress RecipeName BrewedOn Pounds DegreesFahrenheit Email Address Recipe

    Name Date We Brewed On Weight Temperature
  188. Not Just Static Typing https://github.com/Fiedzia/type-system-research/blob/master/README.md Value Objects

  189. Immutable

  190. Immutable Avoid Spooky Action at a Distance

  191. ALWAYS VALID Value Objects

  192. Value Objects

  193. “Gateway Drug to Test Driven Development” Value Objects

  194. “Gateway Drug to Test Driven Development” Value Objects #MYTESTSDONTPASS

  195. Plain Old PHP Objects (POPOs) Value Objects

  196. 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
  197. Only depend on scalars or other value objects. Value Objects

  198. They are not dependencies that you inject. Value Objects

  199. RecipeName Example

  200. None
  201. None
  202. None
  203. None
  204. 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.
  205. None
  206. 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$
  207. None
  208. 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$
  209. None
  210. 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)
  211. None
  212. None
  213. BrewedOn Example

  214. None
  215. None
  216. None
  217. Pounds Example

  218. None
  219. None
  220. None
  221. $poundA->reduceBy($poundB);

  222. Eliminates Checking Value Objects

  223. None
  224. Static Constructors

  225. Natural Language Constructors

  226. None
  227. None
  228. Temperature Example

  229. None
  230. None
  231. None
  232. None
  233. None
  234. Composite Value Objects

  235. Full Name First Name + Last Name

  236. None
  237. Encoding Business Logic in Value Objects

  238. None
  239. None
  240. Amount of Alcohol

  241. None
  242. ABV

  243. Sugar + Yeast = Alcohol + CO2

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

    O ->2C H OH + 2CO 6 12 6 2 5 2
  245. Sugar + Yeast = Alcohol + CO2

  246. Sugar + Yeast = Alcohol + CO2

  247. Sugar - Sugar = Alcohol START FINISH

  248. Sugar - Sugar = Alcohol START FINISH Volume Volume

  249. Specific Gravity

  250. None
  251. Hydrometer

  252. None
  253. learntomoonshine.com

  254. Michael L. Hall - Zymurgy, Summer 1995, vol. 18, no.

    2
  255. None
  256. None
  257. None
  258. None
  259. None
  260. None
  261. None
  262. None
  263. None
  264. None
  265. None
  266. None
  267. None
  268. None
  269. None
  270. None
  271. None
  272. None
  273. None
  274. DDD CRUD/MUD RecipeName String Temperature Float(7,3) DateBrewed DateTime Gravity Float(7,3)

    ABV Function + Validation?
  275. DDD BALL OF MUD/CRUD Correctness ✓✓ Testability ✓ Usability ✓

    Maintainability Modifiability
  276. None
  277. Entities

  278. 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
  279. DO AS MUCH AS YOU CAN IN VALUE OBJECTS!

  280. Doctrine Mappings Doctrine Annotations Laravel Eloquent ORM “Storage Agnostic”

  281. BEHAVIOR FIRST! STORAGE SECOND! “Storage Agnostic”

  282. Single Entity Entities Regular Aggregate Contain Other Entities

  283. Brewer Grain Hops BeerOnTap Entity Types Regular Aggregate Invoice Recipe

    BrewSession BeerMenu
  284. 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, …
  285. None
  286. SETTERS ARE BAD

  287. None
  288. None
  289. None
  290. None
  291. Calling two setters in a row on the same object

    Missing a Concept?
  292. Calling two methods in a row on the same object

    Missing a Concept?
  293. Passing more than one parameter to a method Missing a

    Concept?
  294. None
  295. None
  296. Doctrine

  297. None
  298. None
  299. None
  300. None
  301. None
  302. None
  303. None
  304. None
  305. None
  306. Bounded Contexts

  307. Inventory Recipes Brewhouse

  308. Inventory Recipes Brewhouse Recipe\Grain Inventory\Grain Brewing\BrewSession\Grain

  309. Inventory Recipes Brewhouse

  310. Inventory Recipes Brewhouse Brewers Supporting Bounded Context

  311. Mathias Verraes - Mathias Verraes - Emergent Boundaries https://www.youtube.com/watch?v=ECM1rPYxvD4

  312. • Entities • Manages Child Entities • Transactional Boundary •

    The Easiest Models to Identify Aggregates
  313. Aggregates Recipe BrewSession BeerMenu

  314. None
  315. None
  316. None
  317. None
  318. None
  319. None
  320. None
  321. None
  322. DDD BALL OF MUD/CRUD Correctness ✓✓ Testability ✓✓ Usability ✓

    Maintainability ✓✓ Modifiability ✓
  323. Business Rules in a Specification

  324. None
  325. None
  326. None
  327. DDD BALL OF MUD/CRUD Correctness ✓✓✓ Testability ✓✓ Usability ✓

    Maintainability ✓✓ Modifiability ✓
  328. Task Based User Interface

  329. None
  330. None
  331. None
  332. DDD BALL OF MUD/CRUD Correctness ✓✓✓ Testability ✓✓ Usability ✓✓

    Maintainability ✓✓ Modifiability ✓
  333. None
  334. Security

  335. Domain (Business Logic) Application Services Controller Persistence Event/Command Bus Security

    Templates
  336. Controller Domain Framework

  337. None
  338. None
  339. None
  340. None
  341. Controller Domain Framework

  342. Mathias Verraes - Decoupling the Model from the Framework at

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

    Robert Martin https://www.youtube.com/watch?v=WpkDN78P884
  344. None
  345. None
  346. Domain (Business Logic) Application Services Controller Persistence Event/Command Bus Security

    Templates
  347. DDD BALL OF MUD/CRUD Correctness ✓✓✓ Testability ✓✓ Usability ✓✓

    Maintainability ✓✓✓ Modifiability ✓
  348. Domain Events

  349. 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
  350. None
  351. BrewSessionWasPlanned

  352. BrewSessionWasPlanned

  353. None
  354. None
  355. None
  356. None
  357. None
  358. None
  359. None
  360. DDD BALL OF MUD/CRUD Correctness ✓✓✓ Testability ✓✓ Usability ✓✓

    Maintainability ✓✓✓ Modifiability ✓✓
  361. None
  362. Command Query Responsibility Segregation

  363. CQRS

  364. Write Read

  365. Write Model Read Model(s)

  366. Write Domain Model Read Model(s)

  367. Write Domain Model Read (Complicated SQL Queries)

  368. Controller Command Bus Handler Handler Handler

  369. Controller Command Bus Handler Handler Handler Request

  370. Controller Command Bus Handler Handler Handler Command

  371. None
  372. None
  373. Controller Command Bus Handler Handler Handler Command

  374. Controller Command Bus Handler Handler Handler Command

  375. Controller Command Bus Handler Handler Handler Command

  376. Controller Command Bus Handler Handler Handler Command Response

  377. Controller Command Bus Handler Handler Handler Command

  378. Controller Command Bus Handler Handler Handler Response

  379. Controller Command Bus Handler Handler Handler Response

  380. Controller Command Bus Handler Handler Handler Response Command

  381. Controller Command Bus Handler Handler Handler Response

  382. https://gnugat.github.io/ 2016/05/11/towards-cqrs- command-bus.html

  383. None
  384. None
  385. Controller Command Bus Handler Handler Handler Response Command

  386. Controller Command Bus Handler Handler Handler Response

  387. None
  388. None
  389. None
  390. None
  391. Why Do CQRS?

  392. Controller Domain Framework

  393. Controller API (versions?) Console Commands

  394. Command Bus Command Query Responsibility Segregation

  395. Write Domain Model Read Model(s)

  396. Event Sourcing

  397. None
  398. None
  399. None
  400. Event Sourcing • Object Properties are Not Persisted • Events

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

    Reduces Database Table Counts (Related Tables) • Potentially Reduces Model Counts Event Sourcing
  402. Slide: Microsoft

  403. Libraries • https://github.com/broadway • https://github.com/prooph • https://github.com/szjani/predaddy

  404. Recipe Example

  405. Learning More

  406. homebrewersassociation.org

  407. None
  408. None
  409. DEV BOOK CLUB https://www.youtube.com/user/devbookclub

  410. None
  411. None
  412. None
  413. None
  414. None
  415. DDDinPHP Google Group http://DDDinPHP.org

  416. Final Thoughts

  417. None
  418. None
  419. None
  420. None
  421. Inventory Recipes Brewhouse

  422. Inventory Recipes Brewhouse Legacy Context

  423. None
  424. None
  425. None
  426. Value Objects “Light”

  427. Andrew Cassell @alc277 andrewcassell.com Immutability to Save an Ever-Changing World

  428. https://joind.in/talk/93cfd DDD Topics Covered •Ubiquitous Language •Event Storming •Modelling •Value

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