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

Making the most out of Symfony Forms

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.

Mihai Nica

November 16, 2017
Tweet

More Decks by Mihai Nica

Other Decks in Programming

Transcript

  1. 2

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

    Data Transformers • Form Events • Validation Groups • Collections and Form Themes 3
  3. <?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
  4. <?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
  5. <?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
  6. <?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
  7. <?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
  8. <?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
  9. <?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
  10. <?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
  11. {% 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
  12. 18

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