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. Making the most out
    of Symfony Forms
    Mihai Nica

    @redecs

    View Slide

  2. 2

    View Slide

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

    • Data Transformers

    • Form Events

    • Validation Groups

    • Collections and Form Themes
    3

    View Slide

  4. Custom Form Types
    Reuse common parts of your forms
    4

    View Slide

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

    View Slide

  6. Form Events
    Dynamically change your form depending on data
    6

    View Slide

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

    View Slide

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

    View Slide

  9. Validation Groups
    Callback Validation FTW!
    9

    View Slide

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

    View Slide

  11. Data Transformers
    View or Model Transformer
    11

    View Slide

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

    View Slide

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

    View Slide

  14. Collection “magic”
    14

    View Slide

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

    View Slide

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

    View Slide

  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 %}

    {{ contact ? contact.nameFull : '__NAME_FULL__' }}

    {{ form_widget(form.children.isMainContact) }}


    {{ form_widget(form.children.role) }}


    {{ form_widget(form.shoot, {'value': shoot ? shoot.id : '__SHOOT_ID__'}) }}
    {{ form_widget(form.contact, {'value': contact ? contact.id : '__CONTACT_ID__'}) }}
    url="{{ path('frontend_contacts_save', {'id': contact ? contact.id : '__CONTACT_ID__'}) }}">{{ 'form.label.edit'|
    trans }} 
    {{ 'form.label.remove'|trans }}


    {% endspaceless %}
    {% endblock %}
    17

    View Slide

  18. 18

    View Slide

  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

    View Slide

  20. Questions?
    Feedback is welcome!

    https://joind.in/talk/8148e
    20

    View Slide

  21. Thank you!
    Mihai Nica
    [email protected]
    https://twitter.com/redecs

    https://www.linkedin.com/in/redecs

    View Slide

  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

    View Slide