symfonyobjectentitychoicefield

Symfony3 : choice type field filled with array of objects


I have an entity Product. My product can have multiple names in different languages. A name in french, a name in english, etc. I don't want to use an automatic translation.

The user will have to write the names in the Product form and select the corresponding language. He can add so many names as he wants thanks to an Add button.

All the languages are created by the admin user (in another form). So, Language is also an Entity which have a name (ex: English) and a code (ex: EN).

I created the Entity ProductName which have a name and a language (that conform to what the user writes in the Product form).

In that case, I don't need to associate Entity ProductName with Entity Language. I just want the language code. So, in my ProductName entity, I have this property :

/**
 * @ORM\Column(name="Language_Code", type="string", length=2)
 */
private $language;

My Product form (ProductType) has a CollectionType field in order to add several names.

// Form/ProductType.php

    ->add('infos',      CollectionType::class, array(
        'entry_type'    => ProductInfosType::class,
        'allow_add'     => true,
        'allow_delete'  => true,
        'prototype'     => true,
        'label'         => false,
        'mapped'        => false
    ))

And ProductInfosType form has 2 fields :

// Form/ProductInfosType.php

        ->add('name',           TextType::class, array(
            'attr'              => array('size' => 40)
        ))
        ->add('language',       EntityType::class, array(
            'placeholder'       => '',
            'class'             => 'AppBundle:Language',
            'choice_label'      => 'code',
            'attr'              => array('class' => 'lang'),
            'query_builder'     => function (EntityRepository $er) {
                return $er->createQueryBuilder('l')->orderBy('l.code', 'ASC');
            }
        ))

So, when I go on my form page, I have a block which contains an input text field (Name) and a select field (language). The select field is like this :

<select id="product_infos_0_language" required="required" name="product[infos][0][language]">
    <option value=""></option>
    <option value="DE">DE</option>
    <option value="EN">EN</option>
    <option value="ES">ES</option>
    <option selected="selected" value="FR">FR</option>
</select> 

At this point, everything works well. I created an add button so that the user can add other names, etc...

But, when I submit the form, when I check form data in my ProductController, I noticed that it does not correspond to what I want to store in database.

print_r($form->get('infos')->getData());

// returns :
Array
(
    [0] => AppBundle\Entity\ProductName Object
        ( 
            [language:AppBundle\Entity\ProductName:private] => AppBundle\Entity\Language Object
                (
                    [code:AppBundle\Entity\Language:private] => FR
                    [name:AppBundle\Entity\Language:private] => Français
                )

            [name:AppBundle\Entity\ProductName:private] => Ceinture lombaire LombaSkin
        )
)

What I would like is :

Array
(
    [0] => AppBundle\Entity\ProductName Object
        ( 
            [language:AppBundle\Entity\ProductName:private] => FR    
            [name:AppBundle\Entity\ProductName:private] => Ceinture lombaire LombaSkin
        )
)

I don't want the language object but directly the language code !

That's why I think that I should not use EntityField in ProductNameType form but ChoiceType field.

How can I load all the languages stored in db in the choice field ? I hope that this explanation is more understandable ;-)


Solution

  • I found the solution thanks to this post : Passing data to buildForm() in Symfony 2.8/3.0

    ProductController.php : pass custom data as an option in the createForm() method.

    // ...
    
    // build the form
    $em = $this->getDoctrine()->getManager();
    $product = new Product();
    $languages = $em->getRepository('AppBundle:Language')->findAllOrderedByCode();
    
    $form = $this->createForm(ProductType::class, $product, array(
        'languages' => $languages
    ));
    

    ProductType form : pass custom data in the options resolver

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Product',
            'languages'  => null
        ));
    }
    

    Then, in the buildForm() function, add an entry_options option in the CollectionType field :

    $builder->add('infos',  CollectionType::class, array(
        'entry_type'    => ProductInfosType::class,
        'entry_options' => array('languages' => $options['languages']),
        'allow_add'     => true,
        'allow_delete'  => true,
        'prototype'     => true,
        'label'         => false,
        'by_reference'  => false
    ));
    

    ProductInfosType form : pass custom data in the options resolver (exactly the same as in the ProductForm)

    public function configureOptions(OptionsResolver $resolver) {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\ProductName',
            'languages'  => null
        ));
    }
    

    Now, you have two alternatives : either you want that your form returns entities or simple strings.

    In my example, I just want the language code (like FR, EN, etc.).

    Case 1 : return only the language code when the form is posted :

    // Form/ProductInfosType.php
    
    // ...
    
    // Convert array of objects in an array of strings
    $choices = array();
    foreach ($options['languages'] as $lang) {
        $code = $lang->getCode();
        $choices[$code] = $code;
    }
    
    $builder->add('language', ChoiceType::class, array(
        'placeholder'       => '',
        'choices'           => $choices
    ));
    
    // returns :
    Array
    (
        [0] => AppBundle\Entity\ProductName Object
            ( 
                [name:AppBundle\Entity\ProductName:private] => Ceinture lombaire LombaSkin
                [language:AppBundle\Entity\ProductName:private] => FR    
            )
    )
    

    Case 2 : return Language Entity when the form is posted :

    // Form/ProductInfosType.php
    
    // ...
    
    $builder->add('language', ChoiceType::class, array(
        'placeholder'       => '',
        'choices'           => $options['languages'],
        'choice_label'      => 'code',
        'choice_value'      => 'code'
    ));
    
    // returns :
    Array
    (
        [0] => AppBundle\Entity\ProductName Object
            ( 
                [name:AppBundle\Entity\ProductName:private] => Ceinture lombaire LombaSkin
                [language:AppBundle\Entity\ProductName:private] => AppBundle\Entity\Language Object
                    (
                        [code:AppBundle\Entity\Language:private] => FR
                        [name:AppBundle\Entity\Language:private] => Français
                    )    
            )
    )
    

    With this solution, we don't need to create our form as a service in order to pass entity manager as an argument. All is managed in the controller and form options.