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

Making the most out of Symfony Forms

D18aebbeae1c23e596016286e106f10c?s=47 Mihai Nica
November 16, 2017

Making the most out of Symfony Forms

All web applications have forms, either simple or complex. This talk will address some of the complex scenarios with forms that change depending on the data entered and have dynamically generated fields (server and client side). Using form events and taking advantage of the form rendering flexibility can create some impressive results. The examples are based on real-life scenarios I have encountered in the five years of using the Symfony Framework.

D18aebbeae1c23e596016286e106f10c?s=128

Mihai Nica

November 16, 2017
Tweet

More Decks by Mihai Nica

Other Decks in Programming

Transcript

  1. Making the most out of Symfony Forms Mihai Nica @redecs

  2. 2

  3. I’m going to talk about • Custom Form Types •

    Data Transformers • Form Events • Validation Groups • Collections and Form Themes 3
  4. Custom Form Types Reuse common parts of your forms 4

  5. <?php namespace AppBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormTypeInterface; use

    Symfony\Component\OptionsResolver\OptionsResolver; use Doctrine\Common\Persistence\ObjectManager; use AppBundle\DataTransformer\EntityToIdTransformer; class EntityHiddenType extends AbstractType implements FormTypeInterface { protected $objectManager; public function __construct(ObjectManager $objectManager) { $this->objectManager = $objectManager; } public function buildForm(FormBuilderInterface $builder, array $options) { $transformer = new EntityToIdTransformer($this->objectManager, $options['class']); $builder->addModelTransformer($transformer); } public function setDefaultOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'class' => null, 'invalid_message' => 'The entity does not exist.', )); } } 5
  6. Form Events Dynamically change your form depending on data 6

  7. <?php namespace AppBundle\Form; use AppBundle\Entity\User; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use

    Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; class UserForm extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('email', 'email', array( 'required' => false, 'translation_domain' => 'FOSUserBundle' )) ->add('plainPassword', 'repeated', array( 'required' => false, 'type' => 'password', 'options' => ['translation_domain' => 'FOSUserBundle'], )) ->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) { $data = $event->getData()->getData(); $form = $event->getForm(); $object = $form->getData(); if($object) { if($data['plainPassword'] && $object->getId()) { $formData->setUpdatedAt(new \DateTime()); } } }) ; } } 7
  8. <?php namespace AppBundle\Form; use AppBundle\Entity\Company; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use

    Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class CompanyForm extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { // some fields $builder ->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) { /** @var Company $company */ $company = $event->getData(); $form = $event->getForm(); if(empty($company->getId())) { $form->add('user', UserForm::class, ['label' => false]); } }); } } 8
  9. Validation Groups Callback Validation FTW! 9

  10. <?php namespace AppBundle\Form; use AppBundle\Entity\Company; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use

    Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class CompanyForm extends AbstractType { // build form here public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => Company::class, 'membershipTypes' => array(), 'validation_groups' =>function(FormInterface $form) { $data = $form->getData(); $validationGroups = ['Default', 'registrationCompany', 'registrationUser']; if(!$data->getId()) { $validationGroups[] = 'registrationUser'; } return $validationGroups; }, )); } } 10
  11. Data Transformers View or Model Transformer 11

  12. <?php namespace AppBundle\DataTransformer; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; use Doctrine\Common\Persistence\ObjectManager; class

    EntityToIdTransformer implements DataTransformerInterface { protected $objectManager; protected $class; public function __construct(ObjectManager $objectManager, $class) { $this->objectManager = $objectManager; $this->class = $class; } public function transform($entity) { if (null === $entity) { return null; } return $entity->getId(); } public function reverseTransform($id) { if (!$id) { return null; } $entity = $this->objectManager->getRepository($this->class)->find($id); if (null === $entity) { throw new TransformationFailedException(); } return $entity; } } 12
  13. <?php namespace AppBundle\Form; use AppBundle\Entity\User; use Doctrine\ORM\EntityManager; use Symfony\Component\Form\AbstractType; use

    Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormTypeInterface; class UserTaskType extends AbstractType implements FormTypeInterface { public function buildForm(FormBuilderInterface $builder, array $options) { /** @var EntityManager $em */ $em = $options['em']; $builder ->add('title', TextType::class) ->add('user', HiddenType::class) ; $builder->get('user')->addModelTransformer(new CallbackTransformer( function ($user) { return $user instanceof User ? $user->getId() : 0; }, function ($id) use ($em){ return $em->find(User::class, $id); } )) ; } } 13
  14. Collection “magic” 14

  15. <?php namespace AppBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use AppBundle\Entity\Shoot; class

    ShootType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder // some other fields here ->add('shootContacts', 'collection', array( 'type' => new ShootContactType(), 'allow_add' => true, 'allow_delete' => true, 'prototype_name' => '__KEY__', 'label' => false, )); } } 15
  16. <?php namespace AppBundle\Form; use App\Entity\ShootContact; use AppBundle\Entity\Shoot; use Symfony\Component\Form\AbstractType; use

    Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use AppBundle\Entity\Contact; class ShootContactType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('shoot', 'entity_hidden', array( 'class' => Shoot::class )) ->add('contact', 'entity_hidden', array( 'class' => Contact::class )) ->add('isMainContact', 'checkbox', array( 'required' => false, )) ->add('role', 'choice', array( 'empty_value' => 'none', 'choices' => Contact::getRolesList(), 'required' => false, )); } public function setDefaultOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => ShootContact::class, 'csrf_protection' => false, )); } } 16
  17. {% form_theme form _self %} {% block _frontendbundle_shoot_shootContacts_row %} {{

    form_widget(form) }} {% endblock %} {% block _frontendbundle_shoot_shootContacts_widget %} {% spaceless %} {% for collection_form in form %} {{ block('contact_collection_item') }} {% endfor %} {% endspaceless %} {% endblock %} {% block contact_collection_item %} {% spaceless %} {% set shoot = collection_form is defined ? collection_form.vars.value.shoot : null %} {% set contact = collection_form is defined ? collection_form.vars.value.contact : null %} {% set form = collection_form is defined ? collection_form : form.shootContacts.vars.prototype %} <tr id="contact-{{ contact ? contact.id : '__CONTACT_ID__' }}"> <td class="contact-name-full">{{ contact ? contact.nameFull : '__NAME_FULL__' }}</td> <td class="contact-main-label"> {{ form_widget(form.children.isMainContact) }} </td> <td class="contact-role-label"> {{ form_widget(form.children.role) }} </td> <td> {{ form_widget(form.shoot, {'value': shoot ? shoot.id : '__SHOOT_ID__'}) }} {{ form_widget(form.contact, {'value': contact ? contact.id : '__CONTACT_ID__'}) }} <a class="btn-primary btn-outline btn-sm btn shoot-contact edit" href="#" data- url="{{ path('frontend_contacts_save', {'id': contact ? contact.id : '__CONTACT_ID__'}) }}">{{ 'form.label.edit'| trans }}</a>&nbsp; <a class="btn-danger btn-outline btn-sm btn shoot-contact delete" href="#" data-contact-id="{{ contact ? contact.id : '__CONTACT_ID__' }}">{{ 'form.label.remove'|trans }}</a> </td> </tr> {% endspaceless %} {% endblock %} 17
  18. 18

  19. Take-away • Symfony Forms are very powerful and flexible •

    Form Events, Data Transformers and Validation Groups enable you do amazing things • Form themes and block overriding can be customised any way you want. • Don’t be discourage by the learning curve. The results are well worth the effort 19
  20. Questions? Feedback is welcome! https://joind.in/talk/8148e 20

  21. Thank you! Mihai Nica mihai@wisesystems.co https://twitter.com/redecs https://www.linkedin.com/in/redecs

  22. Reference • https://symfony.com/doc/current/form/data_transformers.html • https://symfony.com/doc/current/form/dynamic_form_modification.html • https://symfony.com/doc/current/form/data_based_validation.html • https://symfony.com/doc/current/form/form_themes.html#form-fragment-naming •

    https://symfony.com/doc/current/components/form.html#learn-more • https://github.com/Glifery/EntityHiddenTypeBundle