Using Symfony Forms with Rich Domain Models

Using Symfony Forms with Rich Domain Models

With the popularisation of DDD people started shifting from anemic models with only getters and setters to a rich model describing the state changes in specific methods. This way of designing models does not play well with Symfony forms. User provided input is inherently invalid while we want to maintain certain invariants in our domain model. A common approach to overcome these limitations is to create data transfer objects our forms are then bound to. This can lead to lots of mapping & glue code that might be cumbersome to write and maintain. But couldn’t we do better? In this talk we will discuss the different aspects of a rich domain model that makes it hard to use it in conjunction with the Form component. We will then look at the possibilities to hook into the data flow of the form handling and discover how we can modify it to interact seamlessly with our model.

Bundle: https://github.com/sensiolabs-de/rich-model-forms-bundle
Demo App: https://github.com/sensiolabs-de/rich-model-forms-demo

0bee6a2886272e60be8888ae48baf42d?s=128

Christopher Hertel

September 28, 2018
Tweet

Transcript

  1. @xabbuh && @el_stoffel

  2. @xabbuh && @el_stoffel Christopher Hertel Consultant & Trainer @ SensioLabs

    Symfony User Group Berlin Christian Flothmann Software Developer @ SensioLabs Symfony Core & Docs Member
  3. @xabbuh && @el_stoffel Our Use Case

  4. @xabbuh && @el_stoffel Simple Product CRUD

  5. @xabbuh && @el_stoffel

  6. @xabbuh && @el_stoffel Forms

  7. @xabbuh && @el_stoffel

  8. @xabbuh && @el_stoffel Domain

  9. @xabbuh && @el_stoffel Product Name Category Price Category Name Parent

    Price Amount Taxrate Currency Category
  10. @xabbuh && @el_stoffel Focus on product form today

  11. @xabbuh && @el_stoffel Standard Forms 1. Solution

  12. @xabbuh && @el_stoffel Implementation

  13. @xabbuh && @el_stoffel Controller src/Controller/ProductController.php @xabbuh && @el_stoffel

  14. @xabbuh && @el_stoffel FormType @xabbuh && @el_stoffel src/Form/ProductType.php

  15. @xabbuh && @el_stoffel class Product { private $id; /** *

    @Assert\Length(min=3) */ private $name; /** * @Assert\NotNull */ private $category; /** * @Assert\GreaterThan(0) */ private $priceAmount; /** Model @xabbuh && @el_stoffel src/Entity/Product.php
  16. @xabbuh && @el_stoffel public function setPriceTax(?int $priceTax): void { $this->priceTax

    = $priceTax; } public function getPriceCurrency(): ?string { return $this->priceCurrency; } public function setPriceCurrency(?string $priceCurrency): void { $this->priceCurrency = $priceCurrency; } } Model @xabbuh && @el_stoffel src/Entity/Product.php
  17. @xabbuh && @el_stoffel Data Flow

  18. @xabbuh && @el_stoffel HTML Form POST /product HTTP/1.1 Host: localhost:8000

    Connection: keep-alive Content-Length: 133 Cache-Control: max-age=0 [...] Accept-Language: en;q=0.9 product[name]: rocket product[category]: 18 product[priceAmount]: 0.59 product[priceTax]: 7 product[priceCurrency]: EUR HTTP Request Form Submit
  19. @xabbuh && @el_stoffel POST /product HTTP/1.1 Host: localhost:8000 Connection: keep-alive

    Content-Length: 133 Cache-Control: max-age=0 [...] Accept-Language: en;q=0.9 product[name]: rocket product[category]: 18 product[priceAmount]: 0.59 product[priceTax]: 7 product[priceCurrency]: EUR HTTP Request HttpFoundation Front Controller
  20. @xabbuh && @el_stoffel HttpFoundation Controller Controller

  21. @xabbuh && @el_stoffel HttpFoundation Controller Controller

  22. @xabbuh && @el_stoffel Controller FormType Request Handler

  23. @xabbuh && @el_stoffel Code

  24. @xabbuh && @el_stoffel Symfony Front Controller HttpFoundation Form Validator Glue

    Code Controller FormType Domain Model
  25. @xabbuh && @el_stoffel FormType Model HTTP Request Front Controller Controller

    Form Glue Code Domain Symfony Model
  26. @xabbuh && @el_stoffel Anemic Domain Model

  27. @xabbuh && @el_stoffel Anemic Domain Model focus on data structured

    easy to implement and to maintain contains little or no logic no guarantee to be valid or consistent
  28. @xabbuh && @el_stoffel Rich Domain Model

  29. @xabbuh && @el_stoffel Rich Domain Model combines data and logic

    valid by design easy to test defined state transitions
  30. @xabbuh && @el_stoffel Anemic vs. Rich Model

  31. @xabbuh && @el_stoffel

  32. @xabbuh && @el_stoffel

  33. @xabbuh && @el_stoffel

  34. @xabbuh && @el_stoffel

  35. @xabbuh && @el_stoffel

  36. @xabbuh && @el_stoffel

  37. @xabbuh && @el_stoffel

  38. @xabbuh && @el_stoffel

  39. @xabbuh && @el_stoffel

  40. @xabbuh && @el_stoffel "how not to agree about software design"

    - sp00m on stackoverflow -
  41. @xabbuh && @el_stoffel Anemic Models Prototyping Ease of use Easily

    generated Rich Models Clean Code Testability Truly OOP Your Choice!
  42. @xabbuh && @el_stoffel Let's use Rich Domain Models with Symfony

    Forms
  43. @xabbuh && @el_stoffel class Product { private $id; private $name;

    private $category; private $price; public function __construct(string $name, Category $category, Price $price) { $this->validateName($name); $this->name = $name; $this->category = $category; $this->price = $price; } public function getId(): int { return $this->id; } Model src/Entity/Product.php @xabbuh && @el_stoffel
  44. @xabbuh && @el_stoffel } public function getName(): string { return

    $this->name; } public function rename(string $name): void { $this->validateName($name); $this->name = $name; } public function getCategory(): Category { return $this->category; } public function moveToCategory(Category $category): void { $this->category = $category; } public function getPrice(): Price Model src/Entity/Product.php @xabbuh && @el_stoffel
  45. @xabbuh && @el_stoffel { $this->category = $category; } public function

    getPrice(): Price { return $this->price; } public function costs(Price $price): void { $this->price = $price; } private function validateName(string $name): void { if (strlen($name) < 3) { throw ProductException::invalidName($name); } } } Model src/Entity/Product.php @xabbuh && @el_stoffel
  46. @xabbuh && @el_stoffel Model src/Entity/Price.php @xabbuh && @el_stoffel

  47. @xabbuh && @el_stoffel Model src/Entity/Category.php @xabbuh && @el_stoffel

  48. @xabbuh && @el_stoffel

  49. @xabbuh && @el_stoffel

  50. @xabbuh && @el_stoffel

  51. @xabbuh && @el_stoffel 2. Solution Easy: Data ransfer bject T

    O s
  52. @xabbuh && @el_stoffel Implementation

  53. @xabbuh && @el_stoffel final class Product { public $id; /**

    * @Assert\Length(min=3) */ public $name; /** * @Assert\NotNull */ public $category; /** * @Assert\GreaterThan(0) */ public $priceAmount; /** DTO src/Form/DTO/Product.php @xabbuh && @el_stoffel
  54. @xabbuh && @el_stoffel * @Assert\Currency */ public $priceCurrency; public static

    function fromEntity(ProductEntity $product = null): self { $self = new self(); if (null === $product) { return $self; } $self->id = $product->getId(); $self->name = $product->getName(); $self->category = $product->getCategory(); $self->priceAmount = $product->getPrice()->getAmount(); $self->priceTax = $product->getPrice()->getTax(); $self->priceCurrency = $product->getPrice()->getCurrency(); return $self; } public function toEntity(ProductEntity $product = null): ProductEntity { $price = new Price((int) $this->priceAmount, $this->priceTax, $this->priceCurrency); DTO src/Form/DTO/Product.php @xabbuh && @el_stoffel
  55. @xabbuh && @el_stoffel return $self; } public function toEntity(ProductEntity $product

    = null): ProductEntity { $price = new Price((int) $this->priceAmount, $this->priceTax, $this->priceCurrency); if (null === $product) { return new ProductEntity($this->name, $this->category, $price); } if ($product->getName() !== $this->name) { $product->rename($this->name); } if ($product->getCategory() !== $this->category) { $product->moveToCategory($this->category); } if (!$price->equals($product->getPrice())) { $product->costs($price); } return $product; } } DTO src/Form/DTO/Product.php @xabbuh && @el_stoffel
  56. @xabbuh && @el_stoffel FormType src/Form/ProductType.php @xabbuh && @el_stoffel

  57. @xabbuh && @el_stoffel Controller src/Controller/ProductController.php @xabbuh && @el_stoffel

  58. @xabbuh && @el_stoffel Data Flow

  59. @xabbuh && @el_stoffel HTML Formular POST /product HTTP/1.1 Host: localhost:8000

    Connection: keep-alive Content-Length: 133 Cache-Control: max-age=0 [...] Accept-Language: en;q=0.9 product[name]: rocket product[category]: 18 product[priceAmount]: 0.59 product[priceTax]: 7 product[priceCurrency]: EUR HTTP Request Form Submit
  60. @xabbuh && @el_stoffel POST /product HTTP/1.1 Host: localhost:8000 Connection: keep-alive

    Content-Length: 133 Cache-Control: max-age=0 [...] Accept-Language: en;q=0.9 product[name]: rocket product[category]: 18 product[priceAmount]: 0.59 product[priceTax]: 7 product[priceCurrency]: EUR HTTP Request HttpFoundation Front Controller
  61. @xabbuh && @el_stoffel HttpFoundation Controller Controller

  62. @xabbuh && @el_stoffel HttpFoundation Controller Controller

  63. @xabbuh && @el_stoffel Controller FormType Request Handler ProductDto ProductDto::

  64. @xabbuh && @el_stoffel HttpFoundation Controller Controller

  65. @xabbuh && @el_stoffel Code

  66. @xabbuh && @el_stoffel Symfony Front Controller HttpFoundation Form Validator Glue

    Code Controller FormType Domain Model DTO
  67. @xabbuh && @el_stoffel FormType HTTP Request Front Controller Controller Form

    Glue Code Domain Symfony DTO Model Model DTO
  68. @xabbuh && @el_stoffel DTO Solution enables us to use Rich

    Domain Models easy to implement and maintain additional Glue Code redundant validation rules
  69. @xabbuh && @el_stoffel Hard: DataMapper 3. Solution

  70. @xabbuh && @el_stoffel Implementation

  71. @xabbuh && @el_stoffel Controller src/Controller/ProductController.php same as first solution @xabbuh

    && @el_stoffel
  72. @xabbuh && @el_stoffel FormType src/Form/ProductType.php @xabbuh && @el_stoffel

  73. @xabbuh && @el_stoffel FormType src/Form/ProductType.php @xabbuh && @el_stoffel

  74. @xabbuh && @el_stoffel FormType src/Form/ProductType.php @xabbuh && @el_stoffel

  75. @xabbuh && @el_stoffel FormType src/Form/ProductType.php @xabbuh && @el_stoffel

  76. @xabbuh && @el_stoffel Data Flow

  77. @xabbuh && @el_stoffel HTML Formular POST /product HTTP/1.1 Host: localhost:8000

    Connection: keep-alive Content-Length: 133 Cache-Control: max-age=0 [...] Accept-Language: en;q=0.9 product[name]: rocket product[category]: 18 product[price][amount]: 0.59 product[price][tax]: 7 product[price][currency]: EUR HTTP Request Form Submit
  78. @xabbuh && @el_stoffel POST /product HTTP/1.1 Host: localhost:8000 Connection: keep-alive

    Content-Length: 133 Cache-Control: max-age=0 [...] Accept-Language: en;q=0.9 product[name]: rocket product[category]: 18 product[price][amount]: 0.59 product[price][tax]: 7 product[price][currency]: EUR HTTP Request HttpFoundation Front Controller
  79. @xabbuh && @el_stoffel HttpFoundation Controller Controller

  80. @xabbuh && @el_stoffel HttpFoundation Controller Controller

  81. @xabbuh && @el_stoffel Controller FormType Request Handler DataMapper

  82. @xabbuh && @el_stoffel Code

  83. @xabbuh && @el_stoffel Symfony Front Controller HttpFoundation Form Validator Glue

    Code Controller FormType Domain Model DataMapper
  84. @xabbuh && @el_stoffel Form Type Model HTTP Request Front Controller

    Controller Form Glue Code Domain Symfony Model Data Mapper Data Mapper
  85. @xabbuh && @el_stoffel DataMapper Solution enables us to use Rich

    Domain Models requires advanced knowledge about Forms additional Glue Code hard to test
  86. @xabbuh && @el_stoffel Still not satisfied?

  87. @xabbuh && @el_stoffel We feel you …

  88. @xabbuh && @el_stoffel Introducing …

  89. @xabbuh && @el_stoffel New: RichModelFormsBundle 4. Solution

  90. @xabbuh && @el_stoffel sensiolabs-de/rich-model-forms-bundle Disclaimer: Still Experimental

  91. @xabbuh && @el_stoffel Installation

  92. @xabbuh && @el_stoffel composer require \ sensiolabs-de/rich-model-forms-bundle:dev-master

  93. @xabbuh && @el_stoffel Features

  94. @xabbuh && @el_stoffel Different read/write property paths

  95. @xabbuh && @el_stoffel Different read/write property paths

  96. @xabbuh && @el_stoffel Exception Handling

  97. @xabbuh && @el_stoffel Exception Handling

  98. @xabbuh && @el_stoffel Mandatory Constructor Arguments

  99. @xabbuh && @el_stoffel Mandatory Constructor Arguments

  100. @xabbuh && @el_stoffel Immutable Value Object

  101. @xabbuh && @el_stoffel Immutable Value Object

  102. @xabbuh && @el_stoffel Implementation

  103. @xabbuh && @el_stoffel Controller src/Controller/ProductController.php still same as first solution

    @xabbuh && @el_stoffel
  104. @xabbuh && @el_stoffel FormType src/Form/ProductType.php @xabbuh && @el_stoffel

  105. @xabbuh && @el_stoffel FormType src/Form/ProductType.php @xabbuh && @el_stoffel

  106. @xabbuh && @el_stoffel Data Flow

  107. @xabbuh && @el_stoffel HTML Formular POST /product HTTP/1.1 Host: localhost:8000

    Connection: keep-alive Content-Length: 133 Cache-Control: max-age=0 [...] Accept-Language: en;q=0.9 product[name]: rocket product[category]: 18 product[price][amount]: 0.59 product[price][tax]: 7 product[price][currency]: EUR HTTP Request Form Submit
  108. @xabbuh && @el_stoffel POST /product HTTP/1.1 Host: localhost:8000 Connection: keep-alive

    Content-Length: 133 Cache-Control: max-age=0 [...] Accept-Language: en;q=0.9 product[name]: rocket product[category]: 18 product[price][amount]: 0.59 product[price][tax]: 7 product[price][currency]: EUR HTTP Request HttpFoundation Front Controller
  109. @xabbuh && @el_stoffel HttpFoundation Controller Controller

  110. @xabbuh && @el_stoffel HttpFoundation Controller Controller

  111. @xabbuh && @el_stoffel Controller FormType Request Handler RichModelFormsBundle

  112. @xabbuh && @el_stoffel Code

  113. @xabbuh && @el_stoffel Symfony Front Controller HttpFoundation Form Validator Glue

    Code Controller FormType Domain Model
  114. @xabbuh && @el_stoffel FormType Model HTTP Request Front Controller Controller

    Form Glue Code Domain Symfony Model
  115. @xabbuh && @el_stoffel RichModelFormsBundle to the rescue! generalizes the previous

    approach additional form config options tailored for rich model form needs released today
  116. @xabbuh && @el_stoffel Final Disclaimer not feature complete covers common

    use cases we discovered needs input for more use cases no, not yet in production
  117. @xabbuh && @el_stoffel Your turn! Help us make it stable!

  118. @xabbuh && @el_stoffel Thank you! Questions?

  119. @xabbuh && @el_stoffel Please give us feedback on joind.in/talk/c0228