Slide 1

Slide 1 text

@xabbuh && @el_stoffel

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

@xabbuh && @el_stoffel Our Use Case

Slide 4

Slide 4 text

@xabbuh && @el_stoffel Simple Product CRUD

Slide 5

Slide 5 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 6

Slide 6 text

@xabbuh && @el_stoffel Forms

Slide 7

Slide 7 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 8

Slide 8 text

@xabbuh && @el_stoffel Domain

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

@xabbuh && @el_stoffel Standard Forms 1. Solution

Slide 11

Slide 11 text

@xabbuh && @el_stoffel Implementation

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

@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

Slide 15

Slide 15 text

@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

Slide 16

Slide 16 text

@xabbuh && @el_stoffel Data Flow

Slide 17

Slide 17 text

@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

Slide 18

Slide 18 text

@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

Slide 19

Slide 19 text

@xabbuh && @el_stoffel HttpFoundation Controller Controller @xabbuh && @el_stoffel

Slide 20

Slide 20 text

@xabbuh && @el_stoffel HttpFoundation Controller Controller @xabbuh && @el_stoffel

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

@xabbuh && @el_stoffel Code

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

@xabbuh && @el_stoffel Anemic Domain Model

Slide 26

Slide 26 text

@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

Slide 27

Slide 27 text

@xabbuh && @el_stoffel Rich Domain Model

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

@xabbuh && @el_stoffel Anemic vs. Rich Model

Slide 30

Slide 30 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 31

Slide 31 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 32

Slide 32 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 33

Slide 33 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 34

Slide 34 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 35

Slide 35 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 36

Slide 36 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 37

Slide 37 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 38

Slide 38 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

@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

Slide 43

Slide 43 text

@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

Slide 44

Slide 44 text

@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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

@xabbuh && @el_stoffel @xabbuh && @el_stoffel

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

@xabbuh && @el_stoffel Implementation

Slide 50

Slide 50 text

@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

Slide 51

Slide 51 text

@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

Slide 52

Slide 52 text

@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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

@xabbuh && @el_stoffel Data Flow

Slide 56

Slide 56 text

@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

Slide 57

Slide 57 text

@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

Slide 58

Slide 58 text

@xabbuh && @el_stoffel HttpFoundation Controller Controller @xabbuh && @el_stoffel

Slide 59

Slide 59 text

@xabbuh && @el_stoffel HttpFoundation Controller Controller @xabbuh && @el_stoffel

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

@xabbuh && @el_stoffel HttpFoundation Controller Controller @xabbuh && @el_stoffel

Slide 62

Slide 62 text

@xabbuh && @el_stoffel Code

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

@xabbuh && @el_stoffel Implementation

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

@xabbuh && @el_stoffel Data Flow

Slide 74

Slide 74 text

@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

Slide 75

Slide 75 text

@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

Slide 76

Slide 76 text

@xabbuh && @el_stoffel HttpFoundation Controller Controller @xabbuh && @el_stoffel

Slide 77

Slide 77 text

@xabbuh && @el_stoffel HttpFoundation Controller Controller @xabbuh && @el_stoffel

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

@xabbuh && @el_stoffel Code

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

@xabbuh && @el_stoffel Still not satisfied?

Slide 84

Slide 84 text

@xabbuh && @el_stoffel We feel you …

Slide 85

Slide 85 text

@xabbuh && @el_stoffel Introducing …

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

@xabbuh && @el_stoffel Installation

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

@xabbuh && @el_stoffel Features

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

@xabbuh && @el_stoffel Exception Handling

Slide 94

Slide 94 text

@xabbuh && @el_stoffel Exception Handling

Slide 95

Slide 95 text

@xabbuh && @el_stoffel Mandatory Constructor Arguments

Slide 96

Slide 96 text

@xabbuh && @el_stoffel Mandatory Constructor Arguments

Slide 97

Slide 97 text

@xabbuh && @el_stoffel Immutable Value Object

Slide 98

Slide 98 text

@xabbuh && @el_stoffel Immutable Value Object

Slide 99

Slide 99 text

@xabbuh && @el_stoffel Implementation

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

@xabbuh && @el_stoffel Data Flow

Slide 104

Slide 104 text

@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

Slide 105

Slide 105 text

@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

Slide 106

Slide 106 text

@xabbuh && @el_stoffel HttpFoundation Controller Controller @xabbuh && @el_stoffel

Slide 107

Slide 107 text

@xabbuh && @el_stoffel HttpFoundation Controller Controller @xabbuh && @el_stoffel

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

@xabbuh && @el_stoffel Code

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

@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

Slide 113

Slide 113 text

@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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

@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

Slide 116

Slide 116 text

@xabbuh && @el_stoffel Thank you! Questions?