Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trouble mapping an existing entity to an embedded form in symfony2

I have an entity which is on the inverse side of a three one-to-one mappings. Entity FittingStep is mapped by FittingStepSingleValue, etc. FittingStep has a field fittingStepType which identifies which one of the three entities the FittingStep should look for. I want to embed that object in the FittingStep edit form.

I have defined forms as services for each of the subforms:

services:
    ihear.form.fitting_step_single_value:
        class: Ihear\FittingBundle\Form\FittingStepSingleValueType
        arguments: [@security.context]
        tags:
            -   
                name: form.type
                alias: ihear_fittingbundle_fittingstepsinglevaluetype
    ihear.form.fitting_step_double_value:
        class: Ihear\FittingBundle\Form\FittingStepDoubleValueType
        arguments: [@security.context]
        tags:
            -   
                name: form.type
                alias: ihear_fittingbundle_fittingstepdoublevaluetype
    ihear.form.fitting_step_option:
        class: Ihear\FittingBundle\Form\FittingStepOptionType
        arguments: [@security.context]
        tags:
            -   
                name: form.type
                alias: ihear_fittingbundle_fittingstepoptiontype

These service classes look like this (pretty basic)

class FittingStepSingleValueType extends AbstractType
{
    private $securityContext;

    public function __construct(SecurityContext $securityContext)
    {   
        $this->securityContext = $securityContext;
    }   

    public function buildForm(FormBuilderInterface $builder, array $options)
    {   
        $builder
            ->add('max1')
            ->add('description1')
            ->add('fittingStep', 'hidden')
        ;   
    }   

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Ihear\FittingBundle\Entity\FittingStepSingleValue'
        ));
    }

    public function getName()
    {
        return 'ihear_fittingbundle_fittingstepsinglevaluetype';
    }
}

My Form uses an EventListener on PRE_SET_DATA to add the appropriate embedded form field:

class FittingStepType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {   
        $builder
            ->add('name')
            ->add('abbreviation')
            ->add('description')
            ->add('fittingStepType',
                'choice',
                ['choices' => ['SingleValue' => 'SingleValue',
                               'DoubleValue' => 'DoubleValue',
                               'Option' => 'Option'],
                 'empty_value' => 'select one please'])
        ;   

        $formModifier = function(FormInterface $form, $fittingStepType) {
            switch ($fittingStepType) {
                case 'SingleValue':
                    $form->add('fittingStepSingleValue',
                        'ihear_fittingbundle_fittingstepsinglevaluetype');
                    if ($form->has('fittingStepDoubleValue'))
                        $form->remove('fittingStepDoubleValue');
                    if ($form->has('fittingStepOption'))
                        $form->remove('fittingStepOption');
                    break;
                case 'DoubleValue':
                    $form->add('fittingStepDoubleValue',
                        'ihear_fittingbundle_fittingstepdoublevaluetype');
                    if ($form->has('fittingStepSingleValue'))
                        $form->remove('fittingStepSingleValue');
                    if ($form->has('fittingStepOption'))
                        $form->remove('fittingStepOption');
                    break;
                case 'Option':
                    $form->add('fittingStepOption',
                        'ihear_fittingbundle_fittingstepoptiontype');
                    if ($form->has('fittingStepSingleValue'))
                        $form->remove('fittingStepSingleValue');
                    if ($form->has('fittingStepDoubleValue'))
                        $form->remove('fittingStepDoubleValue');
                    break;
            }
        };

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function(FormEvent $event) use ($formModifier) {
                $form = $event->getForm();
                // this is the FittingStep
                $data = $event->getData();
                // this is the Entity that contains the value(s)
                // i.e. FittingStepSingleValue
                $fittingStepType = $data->getFittingStepType();
                switch ($fittingStepType) {
                    case 'SingleValue':
                        $formModifier($form, $fittingStepType);
                        break;
                    case 'DoubleValue':
                        $formModifier($form, $fittingStepType);
                        break;
                    case 'Option':
                        $formModifier($form, $fittingStepType);
                        break;
                }
            }
        );
    }
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Ihear\FittingBundle\Entity\FittingStep'
        ));
    }

    public function getName()
    {
        return 'ihear_fittingbundle_fittingsteptype';
    }
}

My controller for the edit action:

/** 
 * Displays a form to edit an existing FittingStep entity.
 *
 * @Route("/{id}/edit", name="admin_fittingstep_edit")
 * @Method("GET")
 * @Template()
 */
public function editAction($id)
{   
    $em = $this->getDoctrine()->getManager();

    $entity = $em->getRepository('IhearFittingBundle:FittingStep')->find($id);

    if (!$entity) {
        throw $this->createNotFoundException(
            'Unable to find FittingStep entity.');
    }   

    $editForm = $this->createForm(new FittingStepType(), $entity);
    $deleteForm = $this->createDeleteForm($id);

    return array(
        'entity'      => $entity,
        'edit_form'   => $editForm->createView(),
        'delete_form' => $deleteForm->createView(),
    );  
}   

When I try to load the form, I get an exception:

The form's view data is expected to be of type scalar, array or an instance of \ArrayAccess, but is an instance of class Ihear\FittingBundle\Entity\FittingStep. You can avoid this error by setting the "data_class" option to "Ihear\FittingBundle\Entity\FittingStep" or by adding a view transformer that transforms an instance of class Ihear\FittingBundle\Entity\FittingStep to scalar, array or an instance of \ArrayAccess.

What am I doing wrong? It seems like there is some disconnect between the object mapping between the entities and the mapping on the form. As a side note, I'm using the $formModifier closure successfully in my create form, so it works fine when creating a new entity with the embedded form.

like image 495
mattexx Avatar asked Nov 26 '13 07:11

mattexx


2 Answers

You have to update the fittinStep from hidden to Collection

->add('fittingStep', 'hidden')

Collection doc: http://symfony.com/doc/current/reference/forms/types/collection.html

->add('fittingStep', 'collection', array('type'=>new FittingStepType(), 'label'=>false))
like image 165
albert Avatar answered Oct 17 '22 01:10

albert


I figured out the problem. The relationship between the entities was referenced by the embedded entity, so I had added a hidden field for the parent entity to the embedded form. This little rascal did not cause any trouble with the creation form, but caused the error above when I embedded the form to edit an existing entity. Thanks @albert for pointing me to that line of code!

class FittingStepSingleValueType extends AbstractType
{
    private $securityContext;

    public function __construct(SecurityContext $securityContext)
    {   
        $this->securityContext = $securityContext;
    }   

    public function buildForm(FormBuilderInterface $builder, array $options)
    {   
        $builder
            ->add('max1')
            ->add('description1')
            // this line was the culprit
            //->add('fittingStep', 'hidden')
        ;   
    }   

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Ihear\FittingBundle\Entity\FittingStepSingleValue'
        ));
    }

    public function getName()
    {
        return 'ihear_fittingbundle_fittingstepsinglevaluetype';
    }
}
like image 27
mattexx Avatar answered Oct 16 '22 23:10

mattexx