$30 off During Our Annual Pro Sale. View Details »

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

December 06, 2018
Tweet

More Decks by Christopher Hertel

Other Decks in Programming

Transcript

  1. @xabbuh && @el_stoffel

    View Slide

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

    View Slide

  3. @xabbuh && @el_stoffel
    Our Use Case

    View Slide

  4. @xabbuh && @el_stoffel
    Simple Product CRUD

    View Slide

  5. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

  6. @xabbuh && @el_stoffel
    Forms

    View Slide

  7. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

  8. @xabbuh && @el_stoffel
    Domain

    View Slide

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

    View Slide

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

    View Slide

  11. @xabbuh && @el_stoffel
    Implementation

    View Slide

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

    View Slide

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

    View Slide

  14. @xabbuh && @el_stoffel
    class Product
    {
    private $id;
    /**
    * @Assert\Length(min=3)
    */
    private $name;
    /**
    * @Assert\NotNull
    */
    private $category;
    /**
    * @Assert\GreaterThan(0)
    */
    private $priceAmount;
    /**
    * @Assert\GreaterThanOrEqual(0)
    */
    private $priceTax;
    /**
    * @Assert\Currency
    */
    private $priceCurrency;
    public function getId(): ?int
    {
    Model
    @xabbuh && @el_stoffel
    src/Entity/Product.php

    View Slide

  15. @xabbuh && @el_stoffel
    * @Assert\Currency
    */
    private $priceCurrency;
    public function getId(): ?int
    {
    return $this->id;
    }
    public function getName(): ?string
    {
    return $this->name;
    }
    public function setName(?string $name): void
    {
    $this->name = $name;
    }
    public function getCategory(): ?Category
    {
    return $this->category;
    }
    public function setCategory(?Category $category): void
    {
    $this->category = $category;
    }
    public function getPriceAmount(): ?int
    {
    return $this->priceAmount;
    }
    public function setPriceAmount(?int $priceAmount): void
    {
    $this->priceAmount = $priceAmount;
    }
    Model
    @xabbuh && @el_stoffel
    src/Entity/Product.php

    View Slide

  16. @xabbuh && @el_stoffel
    Data Flow

    View Slide

  17. @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
    @xabbuh && @el_stoffel

    View Slide

  18. @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
    @xabbuh && @el_stoffel

    View Slide

  19. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller
    @xabbuh && @el_stoffel

    View Slide

  20. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller
    @xabbuh && @el_stoffel

    View Slide

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

    View Slide

  22. @xabbuh && @el_stoffel
    Code

    View Slide

  23. @xabbuh && @el_stoffel
    Symfony
    Front Controller
    HttpFoundation
    Form
    Validator
    Glue Code
    Controller
    FormType
    Domain
    Product
    Category
    Price

    View Slide

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

    View Slide

  25. @xabbuh && @el_stoffel
    Anemic Domain Model

    View Slide

  26. @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 Slide

  27. @xabbuh && @el_stoffel
    Rich Domain Model

    View Slide

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

    View Slide

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

    View Slide

  30. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

  31. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

  32. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

  33. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

  34. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

  35. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

  36. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

  37. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

  38. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  42. @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;
    }
    public function getName(): string
    {
    return $this->name;
    }
    public function rename(string $name): void
    {
    $this->validateName($name);
    Model src/Entity/Product.php
    @xabbuh && @el_stoffel

    View Slide

  43. @xabbuh && @el_stoffel
    public function getId(): int
    {
    return $this->id;
    }
    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
    {
    return $this->price;
    }
    public function costs(Price $price): void
    {
    $this->price = $price;
    }
    Model src/Entity/Product.php
    @xabbuh && @el_stoffel

    View Slide

  44. @xabbuh && @el_stoffel
    $this->name = $name;
    }
    public function getCategory(): Category
    {
    return $this->category;
    }
    public function moveToCategory(Category $category): void
    {
    $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 Slide

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

    View Slide

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

    View Slide

  47. @xabbuh && @el_stoffel
    @xabbuh && @el_stoffel

    View Slide

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

    View Slide

  49. @xabbuh && @el_stoffel
    Implementation

    View Slide

  50. @xabbuh && @el_stoffel
    final class Product
    {
    public $id;
    /**
    * @Assert\Length(min=3)
    */
    public $name;
    /**
    * @Assert\NotNull
    */
    public $category;
    /**
    * @Assert\GreaterThan(0)
    */
    public $priceAmount;
    /**
    * @Assert\GreaterThanOrEqual(0)
    */
    public $priceTax;
    /**
    * @Assert\Currency
    */
    public $priceCurrency;
    public static function fromEntity(ProductEntity $product = null): self
    DTO src/Form/DTO/Product.php
    @xabbuh && @el_stoffel

    View Slide

  51. @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 Slide

  52. @xabbuh && @el_stoffel
    $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);
    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 Slide

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

    View Slide

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

    View Slide

  55. @xabbuh && @el_stoffel
    Data Flow

    View Slide

  56. @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
    @xabbuh && @el_stoffel

    View Slide

  57. @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
    @xabbuh && @el_stoffel

    View Slide

  58. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller
    @xabbuh && @el_stoffel

    View Slide

  59. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller
    @xabbuh && @el_stoffel

    View Slide

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

    View Slide

  61. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller
    @xabbuh && @el_stoffel

    View Slide

  62. @xabbuh && @el_stoffel
    Code

    View Slide

  63. @xabbuh && @el_stoffel
    Symfony
    Front Controller
    HttpFoundation
    Form
    Validator
    Glue Code
    Controller
    FormType
    DTO
    Domain
    Product
    Category
    Price

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  67. @xabbuh && @el_stoffel
    Implementation

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  73. @xabbuh && @el_stoffel
    Data Flow

    View Slide

  74. @xabbuh && @el_stoffel
    HTML Formular
    Form Submit
    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
    @xabbuh && @el_stoffel

    View Slide

  75. @xabbuh && @el_stoffel
    Front Controller
    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
    @xabbuh && @el_stoffel

    View Slide

  76. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller
    @xabbuh && @el_stoffel

    View Slide

  77. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller
    @xabbuh && @el_stoffel

    View Slide

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

    View Slide

  79. @xabbuh && @el_stoffel
    Code

    View Slide

  80. @xabbuh && @el_stoffel
    Symfony
    Front Controller
    HttpFoundation
    Form
    Validator
    Glue Code
    Controller
    FormType
    DataMapper
    Domain
    Product
    Category
    Price

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  85. @xabbuh && @el_stoffel
    Introducing …

    View Slide

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

    View Slide

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

    View Slide

  88. @xabbuh && @el_stoffel
    Installation

    View Slide

  89. @xabbuh && @el_stoffel
    composer require \
    sensiolabs-de/rich-model-forms-bundle

    View Slide

  90. @xabbuh && @el_stoffel
    Features

    View Slide

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

    View Slide

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

    View Slide

  93. @xabbuh && @el_stoffel
    Exception Handling

    View Slide

  94. @xabbuh && @el_stoffel
    Exception Handling

    View Slide

  95. @xabbuh && @el_stoffel
    Mandatory Constructor Arguments

    View Slide

  96. @xabbuh && @el_stoffel
    Mandatory Constructor Arguments

    View Slide

  97. @xabbuh && @el_stoffel
    Immutable Value Object

    View Slide

  98. @xabbuh && @el_stoffel
    Immutable Value Object

    View Slide

  99. @xabbuh && @el_stoffel
    Implementation

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  103. @xabbuh && @el_stoffel
    Data Flow

    View Slide

  104. @xabbuh && @el_stoffel
    HTML Formular
    Form Submit
    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

    View Slide

  105. @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 Slide

  106. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller
    @xabbuh && @el_stoffel

    View Slide

  107. @xabbuh && @el_stoffel
    HttpFoundation Controller
    Controller
    @xabbuh && @el_stoffel

    View Slide

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

    View Slide

  109. @xabbuh && @el_stoffel
    Code

    View Slide

  110. @xabbuh && @el_stoffel
    Symfony
    Front Controller
    HttpFoundation
    Form
    New Bundle
    Glue Code
    Controller
    FormType
    Domain
    Model

    View Slide

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

    View Slide

  112. @xabbuh && @el_stoffel
    RichModelFormsBundle to the rescue!
    generalizes the previous approach
    additional form config options
    tailored for rich model form needs
    released, but still in development

    View Slide

  113. @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 Slide

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

    View Slide

  115. @xabbuh && @el_stoffel
    DTOs New Bundle
    DataMapper
    hard to implement
    hard to test
    additional glue code
    easy to implement
    duplicated validation
    additional glue code
    no glue code needed
    experimental
    future core feature?
    Using Symfony Forms with Rich Domain Models

    View Slide

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

    View Slide