Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony2 double nested dynamic form fields

Tags:

forms

php

symfony

I have a Symfony2 Form with two layered dynamic fields. The first Layer is no problem implementing the documented way with form events: http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#dynamic-generation-for-submitted-forms

But then comes a third field, which depends on the second field, which is already a dynamic field.

To demonstrate the problem, here is my stripped code:

<?php
class ServiceeventType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('park', 'entity', array(
                'class' => 'AppBundle:Park',
                'property' => 'identifyingName',
                'label' => 'Park',
                'required' => true,
                'invalid_message' => 'Choose a Park',
                'placeholder' => 'Please choose',
            ))
            // just a placeholder for the $builder->get('facility')->addEventListener to have something to bind to
            // I'm aware, that this is just a symptom of my problem
            ->add('facility', 'choice', array(
                'choices' => array(),
                'expanded' => true,
                'multiple' => false,
                'label' => 'Facility',
                'required' => false,
                'invalid_message' => 'Choose a Park first',
                'placeholder' => 'Please choose a Park first',
            ))
            // other fields
        ;

        $formModifierPark = function (FormInterface $form, Park $park = null) {
            // overwrite the facility field with the desired entity type
            $form->add('facility', 'entity', array(
                'class' => 'AppBundle:Facility',
                'property' => 'identifyingName',
                'choices' => null === $park ? array() : $park->getFacilities(),
                'label' => 'Facility',
                'required' => true,
                'invalid_message' => 'Choose a Facility',
                'placeholder' => null === $park ? 'Please choose a Park first' : 'Please choose',
            ));
        };

        $formModifierFacility = function (FormInterface $form, Facility $facility = null) {
            $form->add('facilityStatuscode', 'entity', array(
                'class' => 'AppBundle:FacilityStatuscode',
                'property' => 'identifyingName',
                'choices' => null === $facility ? array() : $facility->getFacilityStatuscodeType()->getFacilityStatuscodes(),
                'label' => 'Statuscode',
                'required' => null === $facility ? false : true,
                'invalid_message' => 'Choose a Statuscode',
                'placeholder' => null === $facility ? 'Please choose a Facility first' : 'Please choose',
            ));
        };

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formModifierPark) {
                $formModifierPark($event->getForm(), $event->getData()->getPark());
            }
        );
        $builder->get('park')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($formModifierPark) {
                $formModifierPark($event->getForm()->getParent(), $event->getForm()->getData());
            }
        );

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formModifierFacility) {
                $formModifierFacility($event->getForm(), $event->getData()->getFacility());
            }
        );
        $builder->get('facility')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($formModifierFacility) {
                $formModifierFacility($event->getForm()->getParent(), $event->getForm()->getData());
            }
        );
    }

    // more code
}

The problem is now:

The event-listener set with $builder->get('facility')->addEventListener(FormEvents::POST_SUBMIT,… gets lost at the moment, the facility-field is overwritten by the other event-listener.

I tried several workarounds, but it turns out, that form field options cannot be overridden and form later added fields don't accept new event listeners, once the builder is ready (i.e. when added inside an event listener).

I really have to solve this. Am I missing something? Is Symfony2's Form engine not able to handle two layered dynamic form field dependencies?

Any suggestions?

like image 272
spackmat Avatar asked Apr 02 '15 14:04

spackmat


1 Answers

Thanks to dmnptr's link from his first comment (http://showmethecode.es/php/symfony/symfony2-4-dependent-forms/), I could solve the problem for my case. The trick is, to bind the events to the whole form and not to certain fields (and to PRE_SUBMIT instead of POST_SUBMIT). So my form class now looks like this:

<?php
class ServiceeventType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('park', 'entity', array(
                'class' => 'AppBundle:Park',
                'property' => 'identifyingName',
                'label' => 'Park',
                'required' => true,
                'invalid_message' => 'Choose a Park',
                'placeholder' => 'Please choose',
            ))
            // other fields
        ;

        $addFacilityForm = function (FormInterface $form, $park_id) {
            // it would be easier to use a Park entity here,
            // but it's not trivial to get it in the PRE_SUBMIT events
            $form->add('facility', 'entity', array(
                'class' => 'AppBundle:Facility',
                'property' => 'identifyingName',
                'label' => 'Facility',
                'required' => true,
                'invalid_message' => 'Choose a Facility',
                'placeholder' => null === $park_id ? 'Please choose a Park first' : 'Please Choose',
                'query_builder' => function (FacilityRepository $repository) use ($park_id) {
                    // this does the trick to get the right options
                    return $repository->createQueryBuilder('f')
                        ->innerJoin('f.park', 'p')
                        ->where('p.id = :park')
                        ->setParameter('park', $park_id)
                    ;
                }
            ));
        };
        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($addFacilityForm) {
                $park = $event->getData()->getPark();
                $park_id = $park ? $park->getId() : null;
                $addFacilityForm($event->getForm(), $park_id);
            }
        );
        $builder->addEventListener(
            FormEvents::PRE_SUBMIT,
            function (FormEvent $event) use ($addFacilityForm) {
                $data = $event->getData();
                $park_id = array_key_exists('park', $data) ? $data['park'] : null;
                $addFacilityForm($event->getForm(), $park_id);
            }
        );

        $addFacilityStatuscodeForm = function (FormInterface $form, $facility_id) {
            $form->add('facilityStatuscode', 'entity', array(
                'class' => 'AppBundle:FacilityStatuscode',
                'property' => 'identifyingName',
                'label' => 'Statuscode',
                'required' => true,
                'invalid_message' => 'Choose a Statuscode',
                'placeholder' => null === $facility_id ? 'Please choose a Facility first' : 'Please Chosse',
                'query_builder' => function (FacilityStatuscodeRepository $repository) use ($facility_id) {
                    // a bit more complicated, that's how this model works
                    return $repository->createQueryBuilder('fs')
                        ->innerJoin('fs.facilityStatuscodeType', 'fst')
                        ->innerJoin('AppBundle:Facility', 'f', 'WITH', 'f.facilityStatuscodeType = fst.id')
                        ->where('f.id = :facility_id')
                        ->setParameter('facility_id', $facility_id)
                    ;
                }
            ));
        };
        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($addFacilityStatuscodeForm) {
                $facility = $event->getData()->getFacility();
                $facility_id = $facility ? $facility->getId() : null;
                $addFacilityStatuscodeForm($event->getForm(), $facility_id);
            }
        );
        $builder->addEventListener(
            FormEvents::PRE_SUBMIT,
            function (FormEvent $event) use ($addFacilityStatuscodeForm) {
                $data = $event->getData();
                $facility_id = array_key_exists('facility', $data) ? $data['facility'] : null;
                $addFacilityStatuscodeForm($event->getForm(), $facility_id);
            }
        );


    }

    // more code
}

The AJAX-stuff then works like suggested in the article link above

like image 188
spackmat Avatar answered Oct 16 '22 07:10

spackmat