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

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

Christopher Hertel

September 28, 2018
Tweet

More Decks by Christopher Hertel

Other Decks in Programming

Transcript

  1. @xabbuh && @el_stoffel

    View full-size slide

  2. @xabbuh && @el_stoffel
    Christopher Hertel
    Consultant & Trainer @ SensioLabs
    Symfony User Group Berlin
    Christian Flothmann
    Software Developer @ SensioLabs
    Symfony Core & Docs Member

    View full-size slide

  3. @xabbuh && @el_stoffel
    Our Use Case

    View full-size slide

  4. @xabbuh && @el_stoffel
    Simple Product CRUD

    View full-size slide

  5. @xabbuh && @el_stoffel

    View full-size slide

  6. @xabbuh && @el_stoffel
    Forms

    View full-size slide

  7. @xabbuh && @el_stoffel

    View full-size slide

  8. @xabbuh && @el_stoffel
    Domain

    View full-size slide

  9. @xabbuh && @el_stoffel
    Product
    Name
    Category
    Price
    Category
    Name
    Parent
    Price
    Amount
    Taxrate
    Currency
    Category

    View full-size slide

  10. @xabbuh && @el_stoffel
    Focus on product form today

    View full-size slide

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

    View full-size slide

  12. @xabbuh && @el_stoffel
    Implementation

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  17. @xabbuh && @el_stoffel
    Data Flow

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  20. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller

    View full-size slide

  21. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller

    View full-size slide

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

    View full-size slide

  23. @xabbuh && @el_stoffel
    Code

    View full-size slide

  24. @xabbuh && @el_stoffel
    Symfony
    Front Controller
    HttpFoundation
    Form
    Validator
    Glue Code
    Controller
    FormType
    Domain
    Model

    View full-size slide

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

    View full-size slide

  26. @xabbuh && @el_stoffel
    Anemic Domain Model

    View full-size slide

  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

    View full-size slide

  28. @xabbuh && @el_stoffel
    Rich Domain Model

    View full-size slide

  29. @xabbuh && @el_stoffel
    Rich Domain Model
    combines data and logic
    valid by design
    easy to test
    defined state transitions

    View full-size slide

  30. @xabbuh && @el_stoffel
    Anemic vs. Rich Model

    View full-size slide

  31. @xabbuh && @el_stoffel

    View full-size slide

  32. @xabbuh && @el_stoffel

    View full-size slide

  33. @xabbuh && @el_stoffel

    View full-size slide

  34. @xabbuh && @el_stoffel

    View full-size slide

  35. @xabbuh && @el_stoffel

    View full-size slide

  36. @xabbuh && @el_stoffel

    View full-size slide

  37. @xabbuh && @el_stoffel

    View full-size slide

  38. @xabbuh && @el_stoffel

    View full-size slide

  39. @xabbuh && @el_stoffel

    View full-size slide

  40. @xabbuh && @el_stoffel
    "how not to agree about software design"
    - sp00m on stackoverflow -

    View full-size slide

  41. @xabbuh && @el_stoffel
    Anemic Models
    Prototyping
    Ease of use
    Easily generated
    Rich Models
    Clean Code
    Testability
    Truly OOP
    Your Choice!

    View full-size slide

  42. @xabbuh && @el_stoffel
    Let's use Rich Domain Models
    with Symfony Forms

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  46. @xabbuh && @el_stoffel
    Model src/Entity/Price.php
    @xabbuh && @el_stoffel

    View full-size slide

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

    View full-size slide

  48. @xabbuh && @el_stoffel

    View full-size slide

  49. @xabbuh && @el_stoffel

    View full-size slide

  50. @xabbuh && @el_stoffel

    View full-size slide

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

    View full-size slide

  52. @xabbuh && @el_stoffel
    Implementation

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  58. @xabbuh && @el_stoffel
    Data Flow

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  61. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller

    View full-size slide

  62. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller

    View full-size slide

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

    View full-size slide

  64. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller

    View full-size slide

  65. @xabbuh && @el_stoffel
    Code

    View full-size slide

  66. @xabbuh && @el_stoffel
    Symfony
    Front Controller
    HttpFoundation
    Form
    Validator
    Glue Code
    Controller
    FormType
    Domain
    Model
    DTO

    View full-size slide

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

    View full-size slide

  68. @xabbuh && @el_stoffel
    DTO Solution
    enables us to use Rich Domain Models
    easy to implement and maintain
    additional Glue Code
    redundant validation rules

    View full-size slide

  69. @xabbuh && @el_stoffel
    Hard: DataMapper
    3. Solution

    View full-size slide

  70. @xabbuh && @el_stoffel
    Implementation

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  76. @xabbuh && @el_stoffel
    Data Flow

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  79. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller

    View full-size slide

  80. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller

    View full-size slide

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

    View full-size slide

  82. @xabbuh && @el_stoffel
    Code

    View full-size slide

  83. @xabbuh && @el_stoffel
    Symfony
    Front Controller
    HttpFoundation
    Form
    Validator
    Glue Code
    Controller
    FormType
    Domain
    Model
    DataMapper

    View full-size slide

  84. @xabbuh && @el_stoffel
    Form
    Type
    Model
    HTTP
    Request
    Front Controller
    Controller
    Form
    Glue Code Domain
    Symfony
    Model
    Data
    Mapper
    Data
    Mapper

    View full-size slide

  85. @xabbuh && @el_stoffel
    DataMapper Solution
    enables us to use Rich Domain Models
    requires advanced knowledge about Forms
    additional Glue Code
    hard to test

    View full-size slide

  86. @xabbuh && @el_stoffel
    Still not satisfied?

    View full-size slide

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

    View full-size slide

  88. @xabbuh && @el_stoffel
    Introducing …

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  91. @xabbuh && @el_stoffel
    Installation

    View full-size slide

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

    View full-size slide

  93. @xabbuh && @el_stoffel
    Features

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  96. @xabbuh && @el_stoffel
    Exception Handling

    View full-size slide

  97. @xabbuh && @el_stoffel
    Exception Handling

    View full-size slide

  98. @xabbuh && @el_stoffel
    Mandatory Constructor Arguments

    View full-size slide

  99. @xabbuh && @el_stoffel
    Mandatory Constructor Arguments

    View full-size slide

  100. @xabbuh && @el_stoffel
    Immutable Value Object

    View full-size slide

  101. @xabbuh && @el_stoffel
    Immutable Value Object

    View full-size slide

  102. @xabbuh && @el_stoffel
    Implementation

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  106. @xabbuh && @el_stoffel
    Data Flow

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  109. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller

    View full-size slide

  110. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller

    View full-size slide

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

    View full-size slide

  112. @xabbuh && @el_stoffel
    Code

    View full-size slide

  113. @xabbuh && @el_stoffel
    Symfony
    Front Controller
    HttpFoundation
    Form
    Validator
    Glue Code
    Controller
    FormType
    Domain
    Model

    View full-size slide

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

    View full-size slide

  115. @xabbuh && @el_stoffel
    RichModelFormsBundle to the rescue!
    generalizes the previous approach
    additional form config options
    tailored for rich model form needs
    released today

    View full-size slide

  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

    View full-size slide

  117. @xabbuh && @el_stoffel
    Your turn!
    Help us make it stable!

    View full-size slide

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

    View full-size slide

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

    View full-size slide