phpsymfonydiscriminator

Symfony: Forms with inherited classes


I'm trying to figure out how to handle forms when using inherited class types with Symfony (2.8.6).

I have created a [very] simple example case of what I'm trying to do below. There are problems with it, but it's only to illustrate my question.

  1. How can I supply just one form going from the controller to a twig template so that there can be a field to choose what "type" (discriminator) should be used? Should I simply create another variable, such as "type" that is hardcoded in each class?
  2. Once a form is submitted, how can I figure out which class should be used in the controller, in either a "new" or "edit" action? I've tried plucking it out of the ParameterBag, creating an appropriate entity and form, then using $form->handleRequest($request); ...but it doesn't seem to work when there are extra fields that might belong to another type.

If someone could even point me to a Github repo or something that shows something like this happening, I'd be very grateful. Thanks for your time.

If these are my classes:

 /**
 * @ORM\Entity
 * @ORM\InheritanceType("SINGLE_TABLE")
 * @ORM\DiscriminatorColumn(name="discr", type="string")
 * @ORM\DiscriminatorMap("truck" = "Truck", "Car" = "Car", "suv" = "SUV")
 */
abstract class Vechicle {
    private $make;
    private $model;
    private $numberOfDoors;

    // getters and setters //
}

class Truck extends Vehicle {
    private $offRoadPackage;
    private $bedSize;

    // getters and setters //
}

class Car extends Vehicle {
    private $bodyType;
}

class SUV extends Vehicle {
    // no additional fields //
}

then something like these would be my form types:

class VehicleType extends AbstractType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder
            ->add('make')
            ->add('model')
            ->add('numberOfDoors');
    }

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults(array(
            'data_class' => 'MyBundle\Entity\Vehicle'
        ));
    }
}

class TruckType extends VehicleType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        parent::buildForm($builder, $options);
        $builder
            ->add('offRoadPackage')
            ->add('bedSize');
    }

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults(array(
            'data_class' => 'MyBundle\Entity\Truck'
        ));
    }
}

class CarType extends VehicleType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        parent::buildForm($builder, $options);
        $builder
            ->add('bodyType')
    }

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults(array(
            'data_class' => 'MyBundle\Entity\Car'
        ));
    }
}

class SUVType extends VehicleType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        parent::buildForm($builder, $options);
    }

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults(array(
            'data_class' => 'MyBundle\Entity\SUV'
        ));
    }
}

Solution

  • This is going to be a bit of a lengthy one but bear with me. The gist of this idea is that you deal with your forms in an array. You create a list of types which you iterate to build the actual form objects. This way the only thing you edit is the list of form types if you wish to add more.

    In your template you iterate over all the forms to render them and wrap them in a div you can hide. Next you can add a select element that controls a piece of javascript showing/hiding the form of the type the user has selected.

    After sbumission you can test if the action has been POSTed to and reiterate the forms to check which one of them has been submitted and handle it appropriately.

    Below is a crude untested code snippet:

    The controller/action:

    class SomeController
    {
        public function addAction()
        {
            $types = [
                'Form1' => Form1::class,
                'Form2' => Form2::class,
                'Form3' => Form3::class,
            ];
    
            // create the forms based on the types indicated in the types arary
            $forms = [];
            foreach ($types as $type) {
                $forms[] = $this->createForm($type);
            }
    
            if ($request->isMethod('POST')) {
                foreach ($forms as $form) {
                    $form->handleRequest($request);
    
                    if (!$form->isSubmitted()) continue; // no need to validate a form that isn't submitted
    
                    if ($form->isValid()) {
                        // handle the form of your type
    
                        break; // stop processing as we found the form we have to deal with
                    } 
                }
            }
    
            $views = [];
            foreach ($forms as $form) {
                $views = $form->createView();
            }
    
            $this->render('template.html.twig', ['forms' => $views, 'types' => $types]);
        }
    
    }
    

    The template:

    <select id="types">
        {% for type in types|keys %}
            <option value="vehicle_type_{{ loop.index }}">{{ type }}</option>
        {% endfor %}
    </select>
    {% for form in forms %}
        <div class="form hidden" id="vehicle_type_{{ loop.index }}">
            {{ form_start(form) }}
            {{ form_widget(form) }}
            {{ form_end(form) }}
        </div>
    {% endfor %}
    

    And finally the piece of javascript controlling what form is shown/hidden:

    <script>
        // On select change hide all forms except for the on that was just selected
        $('#types').on('change', function () {
            $('.form').addClass('hidden');
            $('#' + $(this).val()).removeClass('hidden');
        });
    </script>