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

A journey into Symfony form component

Samuele
October 28, 2016

A journey into Symfony form component

Presentation from italian Symfony day 2016, Rome.
Quick examples to help us understand form behavior.

Samuele

October 28, 2016
Tweet

More Decks by Samuele

Other Decks in Programming

Transcript

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

    View full-size slide

  2. Samuele Lilli - DonCallisto
    WHO AM I?

    View full-size slide

  3. Samuele Lilli - DonCallisto
    Samuele Lilli
    DonCallisto

    View full-size slide

  4. Samuele Lilli - DonCallisto

    View full-size slide

  5. Samuele Lilli - DonCallisto

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. Samuele Lilli - DonCallisto
    My first talk

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  15. 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, …)

    View full-size slide

  16. 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

    View full-size slide

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

    View full-size slide

  18. Samuele Lilli - DonCallisto
    ENTITY
    TYPE

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  21. 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

    View full-size slide

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

    View full-size slide

  23. 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,
    ]);
    }
    //….

    View full-size slide

  24. Samuele Lilli - DonCallisto
    Products are not associated to categories

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  27. 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.

    View full-size slide

  28. 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.

    View full-size slide

  29. 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);
    }
    }

    View full-size slide

  30. 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.

    View full-size slide

  31. 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

    View full-size slide

  32. 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)

    View full-size slide

  33. 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’);

    View full-size slide

  34. 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’);

    View full-size slide

  35. Samuele Lilli - DonCallisto
    COLLECTION
    TYPE

    View full-size slide

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

    View full-size slide

  37. 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);
    }

    View full-size slide

  38. 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

    View full-size slide

  39. Samuele Lilli - DonCallisto

    View full-size slide

  40. Samuele Lilli - DonCallisto

    View full-size slide

  41. Samuele Lilli - DonCallisto
    Element not deleted!

    View full-size slide

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

    View full-size slide

  43. 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

    View full-size slide

  44. 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);
    }
    }
    }

    View full-size slide

  45. 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);
    }
    }
    }

    View full-size slide

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

    View full-size slide

  47. 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

    View full-size slide

  48. 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);
    }
    }

    View full-size slide

  49. 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);
    }
    }

    View full-size slide

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

    View full-size slide

  51. 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();
    }
    ]);
    }

    View full-size slide

  52. 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

    View full-size slide

  53. 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()
    ]);
    }

    View full-size slide

  54. 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()
    ]);
    }

    View full-size slide

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

    View full-size slide

  56. Samuele Lilli - DonCallisto
    NO

    View full-size slide

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

    View full-size slide

  58. Samuele Lilli - DonCallisto
    FORM
    EVENTS
    (overview)

    View full-size slide

  59. 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

    View full-size slide

  60. 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

    View full-size slide

  61. 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);
    }

    View full-size slide

  62. 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);
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  69. 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

    View full-size slide

  70. 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

    View full-size slide

  71. 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

    View full-size slide

  72. 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

    View full-size slide

  73. 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

    View full-size slide

  74. 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

    View full-size slide

  75. 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

    View full-size slide

  76. 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

    View full-size slide

  77. 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

    View full-size slide

  78. 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

    View full-size slide

  79. 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

    View full-size slide

  80. Samuele Lilli - DonCallisto
    BECAUSE OF THIS

    View full-size slide

  81. Samuele Lilli - DonCallisto
    NEVER SET
    PARENT DATA
    FROM CHILD FORM

    View full-size slide

  82. Samuele Lilli - DonCallisto
    EVER!

    View full-size slide

  83. Samuele Lilli - DonCallisto
    DATA TYPES

    View full-size slide

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

    View full-size slide

  85. 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!)

    View full-size slide

  86. 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

    View full-size slide

  87. 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.

    View full-size slide

  88. Samuele Lilli - DonCallisto
    DATA
    TRANSFORMERS

    View full-size slide

  89. Samuele Lilli - DonCallisto
    MODEL
    TRANSFORMER

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  101. Samuele Lilli - DonCallisto
    VIEW
    TRANSFORMER

    View full-size slide

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

    View full-size slide

  103. 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;
    }

    View full-size slide

  104. 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)

    View full-size slide

  105. Samuele Lilli - DonCallisto
    FORM
    EVENTS
    (reprise)

    View full-size slide

  106. 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

    View full-size slide

  107. 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

    View full-size slide

  108. 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

    View full-size slide

  109. 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

    View full-size slide

  110. 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

    View full-size slide

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

    View full-size slide

  112. 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

    View full-size slide

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

    View full-size slide

  114. 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

    View full-size slide

  115. 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

    View full-size slide

  116. Samuele Lilli - DonCallisto
    VALUE
    OBJECTS

    View full-size slide

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

    View full-size slide

  118. Samuele Lilli - DonCallisto
    NO SETTERS

    View full-size slide

  119. Samuele Lilli - DonCallisto
    CONSTRUCTOR

    View full-size slide

  120. 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(){ … }
    }

    View full-size slide

  121. 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());
    },
    ]);
    }
    }

    View full-size slide

  122. Samuele Lilli - DonCallisto
    PROPERTY
    PATH

    View full-size slide

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

    View full-size slide

  124. 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;
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  127. Samuele Lilli - DonCallisto
    USE PROPERTY PATH!

    View full-size slide

  128. 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’]);
    }
    }

    View full-size slide

  129. Samuele Lilli - DonCallisto
    THANK YOU

    View full-size slide