Slide 1

Slide 1 text

Samuele Lilli - DonCallisto Rome, 28 October 2016 A journey into Symfony form component

Slide 2

Slide 2 text

Samuele Lilli - DonCallisto WHO AM I?

Slide 3

Slide 3 text

Samuele Lilli - DonCallisto Samuele Lilli DonCallisto

Slide 4

Slide 4 text

Samuele Lilli - DonCallisto

Slide 5

Slide 5 text

Samuele Lilli - DonCallisto

Slide 6

Slide 6 text

Samuele Lilli - DonCallisto Backend developer @ website: www.madisoft.it tech blog: labs.madisoft.it

Slide 7

Slide 7 text

Samuele Lilli - DonCallisto WE ARE HIRING! (wanna join? ask us at the end of the talk or visit our website)

Slide 8

Slide 8 text

Samuele Lilli - DonCallisto My first talk

Slide 9

Slide 9 text

Samuele Lilli - DonCallisto https://joind.in/talk/5a6bd

Slide 10

Slide 10 text

Samuele Lilli - DonCallisto FORM COMPONENT (http://www.freepik.com/free-photos-vectors/smile - Smile vector designed by Freepik)

Slide 11

Slide 11 text

Samuele Lilli - DonCallisto FORM COMPONENT (http://www.freepik.com/free-photos-vectors/smile - Smile vector designed by Freepik)

Slide 12

Slide 12 text

Samuele Lilli - DonCallisto FORM COMPONENT (http://www.freepik.com/free-photos-vectors/smile - Smile vector designed by Freepik)

Slide 13

Slide 13 text

Samuele Lilli - DonCallisto FORM COMPONENT Standalone component (install it via composer/packagist or github)

Slide 14

Slide 14 text

Samuele Lilli - DonCallisto FORM COMPONENT Standalone component (install it via composer/packagist or github) Provides twig facilities for render labels/fields/errors

Slide 15

Slide 15 text

Samuele Lilli - DonCallisto FORM COMPONENT Standalone component (install it via composer/packagist or github) Provides twig facilities for render labels/fields/errors Handling for you data submission (bind to entity if any, validation, data transformations, …)

Slide 16

Slide 16 text

Samuele Lilli - DonCallisto FORM COMPONENT Standalone component (install it via composer/packagist or github) Provides twig facilities for render labels/fields/errors Handling for you data submission (bind to entity if any, validation, data transformations, …) Provides a bunch of built-in types

Slide 17

Slide 17 text

Samuele Lilli - DonCallisto SUMMARY EntityType CollectionType Form Data Filtering on Entity / Collection Form Events Form Data Types Data Transformers Value Objects Property Path

Slide 18

Slide 18 text

Samuele Lilli - DonCallisto ENTITY TYPE

Slide 19

Slide 19 text

Samuele Lilli - DonCallisto Use EntityType when you want list or “associate” one or more entities to another

Slide 20

Slide 20 text

Samuele Lilli - DonCallisto class Product { // …. /** * @ORM\ManyToMany(targetEntity="Category",inversedBy="products") */ protected $categories; class Category { // …. /** *@ORM\ManyToMany(targetEntity="Product",mappedBy="categories") */ protected $products;

Slide 21

Slide 21 text

Samuele Lilli - DonCallisto class Product { // …. /** * @ORM\ManyToMany(targetEntity="Category",inversedBy="products") */ protected $categories; class Category { // …. /** *@ORM\ManyToMany(targetEntity="Product",mappedBy="categories") */ protected $products; OWNING SIDE INVERSED SIDE

Slide 22

Slide 22 text

Samuele Lilli - DonCallisto class Category { // …. public function addProduct(Product $product) { $this->products[] = $product; return $this; } public function removeProduct(Product $product) { $this->products->removeElement($product); }

Slide 23

Slide 23 text

Samuele Lilli - DonCallisto class CategoryType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name', TextType::class) ->add('products', EntityType::class, [ 'class' => Product::class, 'multiple' => true, 'required' => false, ]); } //….

Slide 24

Slide 24 text

Samuele Lilli - DonCallisto Products are not associated to categories

Slide 25

Slide 25 text

Samuele Lilli - DonCallisto http://stackoverflow.com/questions/9102063/symfony2-doctrine2-many-to-many-form-not-saving-entities

Slide 26

Slide 26 text

Samuele Lilli - DonCallisto http://stackoverflow.com/questions/9102063/symfony2-doctrine2-many-to-many-form-not-saving-entities

Slide 27

Slide 27 text

Samuele Lilli - DonCallisto That should not be the right answer Never bend your needs to software limits (unless strictly necessary) Doctrine looks only for changes ONLY on the owning side of association, so those adds will not take place. Take care yourself for data consistency (during objects lifecycle, ensure that all references are setted well in both sides). This concept is not ORM-related. We didn’t add by_reference => false into into FormType.

Slide 28

Slide 28 text

Samuele Lilli - DonCallisto That should not be the right answer Never bend your needs to software limits (unless strictly necessary) Doctrine looks only for changes ONLY on the owning side of association, so those adds will not take place. Take care yourself for data consistency (during objects lifecycle, ensure that all references are setted well in both sides). This concept is not ORM-related. We didn’t add by_reference => false into into FormType.

Slide 29

Slide 29 text

Samuele Lilli - DonCallisto Data consistency Class Product { // …. public function addCategory(Category $category) { if (!$this->categories->contains($category)) { $this->categories[] = $category; $category->addProduct($this); } return $this; } public function removeCategory(Category $category) { if ($this->categories->contains($category)) { $this->categories->removeElement($category); $category->removeProduct($this); } } Class Category { // …. public function addProduct(Product $product) { if (!$this->products->contains($product)) { $this->products[] = $product; $product->addCategory($this); } return $this; } public function removeProduct(Product $product) { if ($this->products->contains($product)) { $this->products->removeElement($product); $product->removeCategory($this); } }

Slide 30

Slide 30 text

Samuele Lilli - DonCallisto This should not be the right answer Never bend your needs to software limits (unless strictly necessary) Doctrine looks only for changes ONLY on the owning side of association, so those adds will not take place. √ Take care yourself for data consistency (during objects lifecycle, ensure that all references are setted well in both sides). This concept is not ORM-related. √ We didn’t add by_reference => false into into FormType.

Slide 31

Slide 31 text

Samuele Lilli - DonCallisto public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name', TextType::class) ->add('products', EntityType::class, [ 'class' => Product::class, 'multiple' => true, 'required' => false, 'by_reference' => false, ]); } by_refence => false

Slide 32

Slide 32 text

Samuele Lilli - DonCallisto WHY BY_REFERENCE => FALSE It forces setter (adder) to be called on the parent element As a rule of thumb, set ALWAYS by_reference to false when dealing with objects (ArrayCollection included)

Slide 33

Slide 33 text

Samuele Lilli - DonCallisto BY_REFERENCE => TRUE Name Cat. Name Name Name $cat->getProducts()->get{0}->setName(‘bar’); $cat->getProducts()->get{1}->setName(‘foobar’); $product3 = new Product(); $product3->setName(); $cat->getProducts()->add($product3); $cat->setName(‘foo’);

Slide 34

Slide 34 text

Samuele Lilli - DonCallisto BY_REFERENCE => FALSE Name Cat. Name Name Name $cat->getProducts()->get{0}->setName(‘bar’); $cat->getProducts()->get{1}->setName(‘foobar’); $product3 = new Product(); $product3>setName(); $cat->addProduct($product3); $cat->setName(‘foo’);

Slide 35

Slide 35 text

Samuele Lilli - DonCallisto COLLECTION TYPE

Slide 36

Slide 36 text

Samuele Lilli - DonCallisto Use CollectionType when you want to embed a collection of forms or add/remove directly elements from collection

Slide 37

Slide 37 text

Samuele Lilli - DonCallisto Class User { /** * @ORM\OneToMany(targetEntity="Ticket", mappedBy="user", cascade={"persist"}) */ protected $tickets; public function addTickets(Ticket $ticket) { if (!$this->tickets->contains($ticket)) { $this->tickets[] = $ticket; $ticket->setUser($this); } return $this; } public function removeTicket(Ticket $ticket) { $this->tickets->removeElement($ticket); }

Slide 38

Slide 38 text

Samuele Lilli - DonCallisto $builder ->add('username', TextType::class) ->add(tickets, CollectionType::class, [ 'entry_type' => TicketType::class, 'allow_add' => true, 'allow_delete' => true, 'prototype' => true, 'by_reference' => false, ]); UserType

Slide 39

Slide 39 text

Samuele Lilli - DonCallisto

Slide 40

Slide 40 text

Samuele Lilli - DonCallisto

Slide 41

Slide 41 text

Samuele Lilli - DonCallisto Element not deleted!

Slide 42

Slide 42 text

In the court of the crimson king - king crimson (1969)

Slide 43

Slide 43 text

Samuele Lilli - DonCallisto Two possible solutions “Manually” (programmatically) remove elements Set orphanRemoval to true on the attribute If the relationship was ManyToMany and User was the owning side, no troubles

Slide 44

Slide 44 text

Samuele Lilli - DonCallisto “Manually” (programmatically) remove elements $originalTickets = new ArrayCollection(); foreach ($user->getTickets() as $ticket) { $originalTickets->add($ticket); } if ($this->isFormSubmittedAndValid($form, $request)) { foreach ($originalTickets as $originalTicket) { if (!$user->getTickets()->contains($originalTicket)) { $em->remove($originalTicket); } } }

Slide 45

Slide 45 text

Samuele Lilli - DonCallisto “Manually” (programmatically) remove elements $originalTickets = new ArrayCollection(); foreach ($user->getTickets() as $ticket) { $originalTickets->add($ticket); } if ($this->isFormSubmittedAndValid($form, $request)) { foreach ($originalTickets as $originalTicket) { if (!$user->getTickets()->contains($originalTicket)) { $em->remove($originalTicket); } } }

Slide 46

Slide 46 text

Samuele Lilli - DonCallisto Set orphanRemoval to true on the attribute /** * @ORM\OneToMany(targetEntity="Ticket", mappedBy="user", cascade={"persist"}, orphanRemoval=true) */ protected $tickets; NOT RECOMMENDED

Slide 47

Slide 47 text

Samuele Lilli - DonCallisto Set orphanRemoval to true on the attribute http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html

Slide 48

Slide 48 text

Samuele Lilli - DonCallisto Set orphanRemoval to true on the attribute public function passTicketsAction(User $yelding, User $beneficiary) { foreach ($yelding->getTickets() as $ticket) { $yelding->removeTicket($ticket); $beneficiary->addTicket($ticket); } }

Slide 49

Slide 49 text

Samuele Lilli - DonCallisto Set orphanRemoval to true on the attribute public function passTicketsAction(User $yelding, User $beneficiary) { foreach ($yelding->getTickets() as $ticket) { $beneficiary->addTicket($ticket); // OR → $ticket->setUser($beneficiary); } }

Slide 50

Slide 50 text

Samuele Lilli - DonCallisto FORM DATA FILTERING (for entity and collection type)

Slide 51

Slide 51 text

Samuele Lilli - DonCallisto EntityType USE built-in queryBuilder option public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add(‘foo’, EntityType::class, [ // …. ‘queryBuilder’ => function (FooRepo $fooRepo) { return $fooRepo->filterFunction(); } ]); }

Slide 52

Slide 52 text

Samuele Lilli - DonCallisto CollectionType CollectionType does not have any queryBuilder option Declare form as a service an inject repository (entity manager) This is the preferred way if you need the repo Pass repository as an option Usually, options are used for what you cannot inject into service

Slide 53

Slide 53 text

Samuele Lilli - DonCallisto EntityManager injection public function __construct(EntityManager $em) { $this->fooRepo = $em->getRepository(Foo::class); } public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add(‘foo’, CollectionType::class, [ // …. 'data' => $this->fooRepo->filterFunction() ]); }

Slide 54

Slide 54 text

Samuele Lilli - DonCallisto EntityManager as an option public function buildForm(FormBuilderInterface $builder, array $options) { $fooRepo = $options[‘fooRepo’]; $builder->add(‘foo’, CollectionType::class, [ // …. 'data' => $fooRepo>filterFunction() ]); }

Slide 55

Slide 55 text

Samuele Lilli - DonCallisto Is repository the only way to filter data? Do I need to create a repository on purpose?

Slide 56

Slide 56 text

Samuele Lilli - DonCallisto NO

Slide 57

Slide 57 text

Samuele Lilli - DonCallisto What if I need to access entity getters for filter operations?

Slide 58

Slide 58 text

Samuele Lilli - DonCallisto FORM EVENTS (overview)

Slide 59

Slide 59 text

Samuele Lilli - DonCallisto ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $e) { /** @var $data User */ $data = $e->getData(); $form = $e->getForm(); $today = new \DateTime(); $criteria = Criteria::create() ->where(Criteria::expr()->gte('date', $today)); $form->add('tickets', CollectionType::class, [ // …. 'data' => $data->getTickets()->matching($criteria) ]); }); }); FORM EVENTS

Slide 60

Slide 60 text

Samuele Lilli - DonCallisto /** * @ORM\OneToMany(targetEntity="Ticket", mappedBy="user", cascade={"persist"}, orphanRemoval=true) */ protected $tickets; ● All tickets (entities) filtered out in the event will be removed! ● Remove orphanRemoval option from the attribute and handle collection yourself FORM EVENTS

Slide 61

Slide 61 text

Samuele Lilli - DonCallisto FORM EVENTS $form = $this->createForm(UserType::class, $user); $originalTickets = new ArrayCollection(); foreach ($form->get('tickets')->getData() as $ticket) { $originalTickets->add($ticket); } if ($this->isFormSubmittedAndValid($form, $request)) { foreach ($originalTickets as $originalTicket) { if ($user->getTickets()->contains($originalTicket)) { continue; } $em->remove($originalTicket); }

Slide 62

Slide 62 text

Samuele Lilli - DonCallisto FORM EVENTS $form = $this->createForm(UserType::class, $user); $originalTickets = new ArrayCollection(); foreach ($form->get('tickets')->getData() as $ticket) { $originalTickets->add($ticket); } if ($this->isFormSubmittedAndValid($form, $request)) { foreach ($originalTickets as $originalTicket) { if ($user->getTickets()->contains($originalTicket)) { continue; } $em->remove($originalTicket); }

Slide 63

Slide 63 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM RENDER FORM CREATION

Slide 64

Slide 64 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM RENDER FORM CREATION

Slide 65

Slide 65 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM RENDER FORM CREATION

Slide 66

Slide 66 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM RENDER FORM CREATION

Slide 67

Slide 67 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM RENDER FORM CREATION

Slide 68

Slide 68 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM RENDER FORM CREATION

Slide 69

Slide 69 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM BIND FORM POST POST PRE_SUBMIT SUBMIT POST_SUBMIT IS VALID PERSIST

Slide 70

Slide 70 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM BIND FORM POST POST PRE_SUBMIT SUBMIT POST_SUBMIT IS VALID PERSIST

Slide 71

Slide 71 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM BIND FORM POST POST PRE_SUBMIT SUBMIT POST_SUBMIT IS VALID PERSIST

Slide 72

Slide 72 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM BIND FORM POST POST PRE_SUBMIT SUBMIT POST_SUBMIT IS VALID PERSIST

Slide 73

Slide 73 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM BIND FORM POST POST PRE_SUBMIT SUBMIT POST_SUBMIT IS VALID PERSIST

Slide 74

Slide 74 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM BIND FORM POST POST PRE_SUBMIT SUBMIT POST_SUBMIT IS VALID PERSIST

Slide 75

Slide 75 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM BIND FORM POST POST PRE_SUBMIT SUBMIT POST_SUBMIT IS VALID PERSIST

Slide 76

Slide 76 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM BIND FORM POST POST PRE_SUBMIT SUBMIT POST_SUBMIT IS VALID PERSIST

Slide 77

Slide 77 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM BIND FORM POST POST PRE_SUBMIT SUBMIT POST_SUBMIT IS VALID PERSIST

Slide 78

Slide 78 text

Samuele Lilli - DonCallisto PRE_SET_DATA POST_SET_DATA CONTROLLER VIEW NEW FORM BIND FORM POST POST PRE_SUBMIT SUBMIT POST_SUBMIT IS VALID PERSIST

Slide 79

Slide 79 text

Samuele Lilli - DonCallisto TIP Every group of events it’s called from START to END on every FORM. This means that if you have a chain of embedded form, all events are called starting from innermost forms, going up to parent form, ending on top form

Slide 80

Slide 80 text

Samuele Lilli - DonCallisto BECAUSE OF THIS

Slide 81

Slide 81 text

Samuele Lilli - DonCallisto NEVER SET PARENT DATA FROM CHILD FORM

Slide 82

Slide 82 text

Samuele Lilli - DonCallisto EVER!

Slide 83

Slide 83 text

Samuele Lilli - DonCallisto DATA TYPES

Slide 84

Slide 84 text

Samuele Lilli - DonCallisto DATA TYPES MODEL DATA NORM DATA VIEW DATA

Slide 85

Slide 85 text

Samuele Lilli - DonCallisto MODEL DATA Main data type of PRE_SET_DATA and POST_SET_DATA Reppresent data of underlying object. In previous example with product form, product field model data is a Product object. If field type is the same of underlying data, NORM DATA will be the same of MODEL DATA If field type is not the same of underlying data, you must use ModelTransformer to transform MODEL DATA into NORM DATA and vice versa (Don’t worry, we will talk about transformers next!)

Slide 86

Slide 86 text

Samuele Lilli - DonCallisto NORM DATA Main data type of SUBMIT event Reppresent data after normalization. Commonly not used directly. If on MODEL DATA is not present any ModelTransform, this is the same of MODEL DATA and so the same of underlying object. If NORM DATA isn’t the same of view reppresentation, you must use ViewTransformer to transform NORM DATA into VIEW DATA and vice versa

Slide 87

Slide 87 text

Samuele Lilli - DonCallisto VIEW DATA Main data type of POST_SUBMIT event Reppresent data presented to the View. It’s the data type that you get when you post the form. If on VIEW DATA is not present any ViewTransformer, this is the same of NORM DATA.

Slide 88

Slide 88 text

Samuele Lilli - DonCallisto DATA TRANSFORMERS

Slide 89

Slide 89 text

Samuele Lilli - DonCallisto MODEL TRANSFORMER

Slide 90

Slide 90 text

Samuele Lilli - DonCallisto ENTITY MODEL DATA NORM DATA MODEL TRANSFORMER NEW FORM BIND TRANSFORM REVERSE TRANSFORM

Slide 91

Slide 91 text

Samuele Lilli - DonCallisto ENTITY MODEL DATA NORM DATA NEW FORM BIND TRANSFORM REVERSE TRANSFORM MODEL TRANSFORMER

Slide 92

Slide 92 text

Samuele Lilli - DonCallisto ENTITY - MODEL DATA /** * @ORM\Column(type="array", nullable=true) */ protected $tags;

Slide 93

Slide 93 text

Samuele Lilli - DonCallisto ENTITY MODEL DATA NORM DATA NEW FORM BIND TRANSFORM REVERSE TRANSFORM MODEL TRANSFORMER

Slide 94

Slide 94 text

Samuele Lilli - DonCallisto FORM FIELD $builder->add('tags', TextType::class)

Slide 95

Slide 95 text

Samuele Lilli - DonCallisto Since model data (array) is different from norm data (text) we need a model transformer

Slide 96

Slide 96 text

Samuele Lilli - DonCallisto $builder->add('tags', TextType::class); $builder->get(‘tags’)->addModelTransformer(...); FORM FIELD

Slide 97

Slide 97 text

Samuele Lilli - DonCallisto ENTITY MODEL DATA NORM DATA NEW FORM BIND TRANSFORM REVERSE TRANSFORM MODEL TRANSFORMER

Slide 98

Slide 98 text

Samuele Lilli - DonCallisto TRANSFORM public function transform($tagsArray) { // transform the array to string if (null === $tagsArray) { return ''; } return implode(',', $tagsArray); }

Slide 99

Slide 99 text

Samuele Lilli - DonCallisto ENTITY MODEL DATA NORM DATA NEW FORM BIND TRANSFORM REVERSE TRANSFORM MODEL TRANSFORMER

Slide 100

Slide 100 text

Samuele Lilli - DonCallisto public function reverseTransform($tagsString) { // transform the string back to an array if (!$tagsString) { return []; } return explode(',', $tagsString); } REVERSE TRANSFORM

Slide 101

Slide 101 text

Samuele Lilli - DonCallisto VIEW TRANSFORMER

Slide 102

Slide 102 text

Samuele Lilli - DonCallisto VIEW NORM DATA VIEW DATA CREATE VIEW POST TRANSFORM REVERSE TRANSFORM VIEW TRANSFORMER

Slide 103

Slide 103 text

Samuele Lilli - DonCallisto DATE TYPE (TRANSFORM) // Transforms a normalized date into a localized date string/array) public function transform($dateTime) { if (null === $dateTime) { return ''; } if (!$dateTime instanceof \DateTimeInterface) { throw new TransformationFailedException('Expected a \DateTimeInterface.'); } $value = $this->getIntlDateFormatter()->format($dateTime->getTimestamp()); if (intl_get_error_code() != 0) { throw new TransformationFailedException(intl_get_error_message()); } return $value; }

Slide 104

Slide 104 text

Samuele Lilli - DonCallisto // Transforms a localized date string/array into a normalized date. public function reverseTransform($value) { if (!is_string($value)) { throw new TransformationFailedException('Expected a string.'); } if ('' === $value) { return; } $timestamp = $this->getIntlDateFormatter()->parse($value); // …. try { $dateTime = new \DateTime(sprintf('@%s', $timestamp)); } catch (\Exception $e) { throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e); } // …. return $dateTime; } DATE TYPE ( REVERSE TRANSFORM)

Slide 105

Slide 105 text

Samuele Lilli - DonCallisto FORM EVENTS (reprise)

Slide 106

Slide 106 text

Samuele Lilli - DonCallisto PRE SET DATA EVENT Modify data given during pre-population. Don’t modify form data directly but modify event data instead. Add/Remove form fields USED FOR EVENT DATA MODEL DATA

Slide 107

Slide 107 text

Samuele Lilli - DonCallisto ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $e) { /** @var $data User */ $data = $e->getData(); // ← MODEL DATA $form = $e->getForm(); $today = new \DateTime(); $criteria = Criteria::create() ->where(Criteria::expr()->gte('date', $today)); $form->add(‘tickets’, CollectionType::class, [ // …. 'data' => $data->getTickets()->matching($criteria) ]); }); }); PRE_SET_DATA

Slide 108

Slide 108 text

Samuele Lilli - DonCallisto ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $e) { if ($e->getData()->canHandleMail()) { // ← MODEL DATA $e->getForm()->add(‘email’, EmailType::class); } }); }); PRE_SET_DATA

Slide 109

Slide 109 text

Samuele Lilli - DonCallisto Read pre-populated form data Don’t remove fields that you’ve setted “statically” on form building process. Use PRE_SET_DATA and implement the logic about fields. One exception: you are extending from a parent form where you cannot control yourself the logic. USED FOR EVENT DATA MODEL DATA POST SET DATA EVENT

Slide 110

Slide 110 text

Samuele Lilli - DonCallisto class FooType extends BarType { // …. // email field added in BarType ->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $e) { if (!$e->getData()->canHandleMail()) { // ← MODEL DATA $e->getForm()->remove(‘email’); } }); }); POST_SET_DATA

Slide 111

Slide 111 text

Samuele Lilli - DonCallisto Change data from the request Add/Remove form fields USED FOR EVENT DATA REQUEST DATA (ARRAY) PRE SUBMIT EVENT

Slide 112

Slide 112 text

Samuele Lilli - DonCallisto private $canModifyEmail = true; ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $e) { $this->canModifyEmail = $e->getData()->canModifyEmail(); $e->getForm()->add(‘email, EmailType::class, [ // …. ‘attr’ => [ ‘disabled’ => !$this->canModifyEmail ], ]); }) ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $e) { if (!$this->canModifyEmail) { $e->getForm()->remove(‘email’); } }); }); PRE_SUBMIT

Slide 113

Slide 113 text

Samuele Lilli - DonCallisto Modify NormData No real example will be showed EVENT DATA NORM DATA SUBMIT EVENT USED FOR

Slide 114

Slide 114 text

Samuele Lilli - DonCallisto Fetch data after denormalization Even if faster to read “final” data here, don’t implement any business logic → hard to test and break SRP. If you need to modify model data, do it elsewhere just after isValid call. USED FOR EVENT DATA VIEW DATA POST SUBMIT EVENT

Slide 115

Slide 115 text

Samuele Lilli - DonCallisto POST SUBMIT $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($handler) { $delete = $event->getForm()->has('delete') ? $event->getForm()->get('delete')->getData() : false; $entity = $event->getForm()->getParent()->getData(); if (!$delete) { return; } $handler->remove($entity, $event->getForm()->getName()); }); Vich\uploader-bundle

Slide 116

Slide 116 text

Samuele Lilli - DonCallisto VALUE OBJECTS

Slide 117

Slide 117 text

Samuele Lilli - DonCallisto Two object are equal if their all fields are equal, not necessary if they are the same object

Slide 118

Slide 118 text

Samuele Lilli - DonCallisto NO SETTERS

Slide 119

Slide 119 text

Samuele Lilli - DonCallisto CONSTRUCTOR

Slide 120

Slide 120 text

Samuele Lilli - DonCallisto Class FooBar { private $foo; private $bar; public function __construct($foo, $bar) { $this->foo = $foo; $this->bar = $bar; } public function getFoo(){ … } public function getBar(){ … } }

Slide 121

Slide 121 text

Samuele Lilli - DonCallisto Class FooBarType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add(‘foo’) ->add(‘bar’); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ ‘empty_data’ => function (FormInterface $interface) { return new FooBar($interface->get(‘foo’)->getData(), $interface->get(‘bar’)->getData()); }, ]); } }

Slide 122

Slide 122 text

Samuele Lilli - DonCallisto PROPERTY PATH

Slide 123

Slide 123 text

Samuele Lilli - DonCallisto Class User { //…. /* * @ORM\OneToOne(targetEntity=”Address”, inversedBy=”user”) */ protected $address; //…. }

Slide 124

Slide 124 text

Samuele Lilli - DonCallisto Class Address { //…. /** * @ORM\OneToOne(targetEntity=”User”, mappedBy=”address”) */ protected $user; /** * @ORM\Column(type=”string”) */ protected $street; /** * @ORM\Column(type=”string”) */ protected $number; }

Slide 125

Slide 125 text

Samuele Lilli - DonCallisto DON’T CREATE AN EMBEDDED FORM ON PURPOSE

Slide 126

Slide 126 text

Samuele Lilli - DonCallisto DON’T USE UNMAPPED FIELDS AND FORM EVENTS

Slide 127

Slide 127 text

Samuele Lilli - DonCallisto USE PROPERTY PATH!

Slide 128

Slide 128 text

Samuele Lilli - DonCallisto Class UserType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { // …. $builder ->add(‘street’, TextType::class, [‘property_path’ => ‘address.street’]) ->add(‘number’, TextType::class, [‘property_path’ => ‘address.number’]); } }

Slide 129

Slide 129 text

QUESTIONS?

Slide 130

Slide 130 text

Samuele Lilli - DonCallisto THANK YOU