Simple Multiple Choice With Arrays


The ChoiceType form field is versatile, but as a result, can be a little overwhelming when first getting started using Symfony forms. So far we have seen how we can make use of the ChoiceType to offer our end users a selection of multiple choices, letting them select from drop downs or radio buttons, but we haven't allowed them to chose more than one option at any given time.

Obviously this is very limiting.

Fortunately, offering multiple choice is really not that different from only allowing a single choice.

First, we must update our form field to set the multiple option to true:

<?php

// /src/AppBundle/Form/Type/ProductType.php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ProductType extends AbstractType
{
    const AWING = 'awing';
    const BWING = 'bwing';
    const XWING = 'xwing';
    const YWING = 'ywing';

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('choice', ChoiceType::class, [
                'choices' => [
                    'A-Wing' => self::AWING,
                    'B-Wing' => self::BWING,
                    'X-Wing' => self::XWING,
                    'Y-Wing' => self::YWING,
                ],
                'expanded'  => true,
                'multiple'  => true,
            ])
            ->add('save', SubmitType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'AppBundle\Entity\Product'
        ]);
    }
}

And this will immediately 'work', in so much as it will render out a form with two checkboxes that can be submitted back.

Assuming we haven't updated the choice property on our Product entity:

// /src/AppBundle/Entity/Product.php

    * snip *

    /**
     * @ORM\Column(type="string")
     */
    protected $choice;

Then when we actually go ahead and submit our form, we are going to have a bad time:

Symfony - Notice: Array to string conversion, 500 Internal Server Error ContextErrorException

What might be initially confusing is that this error occurs whether you select zero, one, or more of the available checkboxes.

This does make sense though if you look at the underlying HTML:

<input type="checkbox" id="product_choice_0" name="product[choice][]" value="awing" /> A-Wing
<input type="checkbox" id="product_choice_1" name="product[choice][]" value="bwing" /> B-Wing
<input type="checkbox" id="product_choice_2" name="product[choice][]" value="xwing" /> X-Wing
<input type="checkbox" id="product_choice_3" name="product[choice][]" value="ywing" /> Y-Wing

Note: I have stripped off a lot of the extra CSS and styling related HTML from this output

Essentially we are seeing that on our product form, we have a choice field, and that field will return an array.

Whether that array has zero, one, or multiple entries in it is irrelevant. The data coming to the server will not be in a format that can be saved off as a string. We must update our entity appropriately.

Doctrine handily provides us with the array, simple_array, and json_array basic mapping types. You may wish to instead use and save relational data from your database, which is also possible and will be covered in the very next video.

All we need to do is change our entity mapping information to one of the above, and remember to update our database schema after doing so:

// /src/AppBundle/Entity/Product.php

    * snip *

    /**
     * @ORM\Column(type="array")
     */
    protected $choice;

Then:

php bin/console doctrine:schema:update --force

And upon refreshing the form, we should now be able to save off data to the database.

The 'downside' here is that we are being very literal about our saved data. Assuming you followed the example above, you would find some serialised data inside your database table:

a:2:{i:0;s:5:"awing";i:1;s:5:"xwing";}

Hmmm. I mean, it's technically correct, but it's also pretty difficult to use this data directly. As far as MySQL is concerned, we've saved ourselves off some LONGTEXT... which again, is fine, but effectively we are going to lock ourselves in to using Doctrine (or similar) to be able to use this data. Directly querying this data will be difficult.

We could change off the mapping type to take JSON instead:

// /src/AppBundle/Entity/Product.php

    * snip *

    /**
     * @ORM\Column(type="json_array")
     */
    protected $choice;

And remember to update your database accordingly... oh, and don't do this if you already have some data in your database as this too will be stored as LONGTEXT inside MySQL, and so Doctrine won't alert you to the fact that you just made your data inconsistent:

Inconsistent data ahoy

Not that big of a deal in development, but not something you'd get promoted for in production.

The benefits of using the json_array type, aside from looking much nicer, would be that - I believe - you gain more benefits in MySQL 5.7 due to its improved handling of JSON. However, I have no first hand experience of this, so if this sounds interesting then please do your own research accordingly (ctrl+f 'JSON support').

Repopulating Your ChoiceType Form Field

Just like we have covered in previous videos, re-loading data (or setting default field values) is simple enough:

<?php

// /src/AppBundle/Controller/FormExampleController.php

namespace AppBundle\Controller;

use AppBundle\Entity\Product;
use AppBundle\Form\Type\ProductType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class FormExampleController extends Controller
{
    /**
     * @Route("/", name="form_add_example")
     */
    public function formAddExampleAction(Request $request)
    {
        $product = new Product();
        // $product->setChoice(ProductType::YWING);
        // becomes
        // $product->setChoice(array(ProductType::YWING));
        // or
        $product->setChoice([ProductType::YWING, ProductType:AWING]);

        $form = $this->createForm(ProductType::class, $product);

        // * snip *

Remember, all our form is doing is showing a HTML representation of the state of the given object - $product in our example. This is really easy to overthink.

In the very next video we will go one further than this, looking at how we can use entities from our database as the choice options, and how we can save these values back to our database.

Each of these steps stack nicely on top of each other. You now know the essentials, so learning the slightly more advanced stuff is just building on your already learned foundational understanding.

Code For This Course

Get the code for this course.

Episodes