Samuele Lilli - DonCallisto FORM COMPONENT Standalone component (install it via composer/packagist or github) Provides twig facilities for render labels/fields/errors
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, …)
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
Samuele Lilli - DonCallisto SUMMARY EntityType CollectionType Form Data Filtering on Entity / Collection Form Events Form Data Types Data Transformers Value Objects Property Path
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.
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.
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); } }
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.
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)
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’);
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’);
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
Samuele Lilli - DonCallisto Set orphanRemoval to true on the attribute /** * @ORM\OneToMany(targetEntity="Ticket", mappedBy="user", cascade={"persist"}, orphanRemoval=true) */ protected $tickets; NOT RECOMMENDED
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
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); } }
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); } }
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
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
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
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!)
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
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.
Samuele Lilli - DonCallisto public function reverseTransform($tagsString) { // transform the string back to an array if (!$tagsString) { return []; } return explode(',', $tagsString); } REVERSE TRANSFORM
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; }
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)
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
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
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
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
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(){ … } }