Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

EntityType Checkboxes with ManyToMany

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Guys, we are really good at adding items to our ManyToMany relationship in PHP and via the fixtures. But what about via Symfony's form system? Yea, that's where things get interesting.

Go to /admin/genus and login with a user from the fixtures: weaverryan+1@gmail.com and password iliketurtles. Click to edit one of the genuses.

Planning Out the Form

Right now, we don't have the ability to change which users are studying this genus from the form.

If we wanted that, how would it look? It would probably be a list of checkboxes: one checkbox for every user in the system. When the form loads, the already-related users would start checked.

This will be perfect... as long as you don't have a ton of users in your system. In that case, creating 10,000 checkboxes won't scale and we'll need a different solution. But, I'll save that for another day, and it's not really that different.

EntityType Field Configuration

The controller behind this page is called GenusAdminController and the form is called GenusFormType. Go find it! Step one: add a new field. Since we ultimately want to change the genusScientists property, that's what we should call the field. The type will be EntityType:

... lines 1 - 7
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
... lines 9 - 16
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 22 - 44
->add('genusScientists', EntityType::class, [
... lines 46 - 48
])
;
}
... lines 52 - 58
}

This is your go-to field type whenever you're working on a field that is mapped as any of the Doctrine relations. We used it earlier with subfamily. In that case, each Genus has only one SubFamily, so we configured the field as a select drop-down:

... lines 1 - 7
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
... lines 9 - 16
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... line 22
->add('subFamily', EntityType::class, [
'placeholder' => 'Choose a Sub Family',
'class' => SubFamily::class,
'query_builder' => function(SubFamilyRepository $repo) {
return $repo->createAlphabeticalQueryBuilder();
}
])
... lines 30 - 49
;
}
... lines 52 - 58
}

Back on genusScientists, start with the same setup: set class to User::class. Then, because this field holds an array of User objects, set multiple to true. Oh, and set expanded also to true: that changes this to render as checkboxes:

... lines 1 - 7
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
... lines 9 - 16
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 22 - 44
->add('genusScientists', EntityType::class, [
'class' => User::class,
'multiple' => true,
'expanded' => true,
])
;
}
... lines 52 - 58
}

That's everything! Head to the template: app/Resources/views/admin/genus/_form.html.twig. Head to the bottom and simply add the normal form_row(genusForm.genusScientists):

{{ form_start(genusForm) }}
... lines 2 - 21
{{ form_row(genusForm.genusScientists) }}
... lines 23 - 24
{{ form_end(genusForm) }}

Guys, let's go check it out.

Choosing the Choice Label

Refresh! And... explosion!

Catchable Fatal Error: Object of class User could not be converted to string

Wah, wah. Our form is trying to build a checkbox for each User in the system... but it doesn't know what field in User it should use as the display value. So, it tries - and fails epicly - to cast the object to a string.

There's two ways to fix this, but I like to add a choice_label option. Set it to email to use that property as the visible text:

... lines 1 - 7
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
... lines 9 - 16
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 22 - 44
->add('genusScientists', EntityType::class, [
... lines 46 - 48
'choice_label' => 'email',
])
;
}
... lines 53 - 59
}

Try it again. Nice!

As expected, three of the users are pre-selected. So, does it save? Uncheck Aquanaut3, check Aquanaut2 and hit save. It does! Behind the scenes, Doctrine just deleted one row from the join table and inserted another.

EntityType: Customizing the Query

Our system really has two types of users: plain users and scientists:

... lines 1 - 23
AppBundle\Entity\User:
user_{1..10}:
email: weaverryan+<current()>@gmail.com
plainPassword: iliketurtles
roles: ['ROLE_ADMIN']
avatarUri: <imageUrl(100, 100, 'abstract')>
user.aquanaut_{1..10}:
email: aquanaut<current()>@example.org
plainPassword: aquanote
isScientist: true
firstName: <firstName()>
lastName: <lastName()>
universityName: <company()> University
avatarUri: <imageUrl(100, 100, 'abstract')>

Well, they're really not any different, except that some have isScientist set to true. Now technically, I really want these checkboxes to only list users that are scientists: normal users shouldn't be allowed to study Genuses.

How can we filter this list? Simple! Start by opening UserRepository: create a new public function called createIsScientistQueryBuilder():

... lines 1 - 2
namespace AppBundle\Repository;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
public function createIsScientistQueryBuilder()
{
... lines 11 - 13
}
}

Very simple: return $this->createQueryBuilder('user'), andWhere('user.isScientist = :isScientist') and finally, setParameter('isScientist', true):

... lines 1 - 2
namespace AppBundle\Repository;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
public function createIsScientistQueryBuilder()
{
return $this->createQueryBuilder('user')
->andWhere('user.isScientist = :isScientist')
->setParameter('isScientist', true);
}
}

This doesn't make the query: it just returns the query builder.

Over in GenusFormType, hook this up: add a query_builder option set to an anonymous function. The field will pass us the UserRepository object. That's so thoughtful! That means we can celebrate with return $repo->createIsScientistQueryBuilder():

... lines 1 - 17
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 23 - 45
->add('genusScientists', EntityType::class, [
... lines 47 - 50
'query_builder' => function(UserRepository $repo) {
return $repo->createIsScientistQueryBuilder();
}
])
;
}
... lines 57 - 63
}

Refresh that bad boy! Bam! User list filtered.

Thanks to our ManyToMany relationship, hooking up this field was easy: it just works. But now, let's go the other direction: find a user form, and add a list of genus checkboxes. That's where things are going to go a bit crazy.

Leave a comment!

16
Login or Register to join the conversation
Default user avatar

Can you post your controller code ?

Reply

Hey there!

All the code is available below each video, just look for a proper code block. Our code blocks are expandable, so you can easily expand the code block to see the *full* code.

Cheers!

1 Reply
Default user avatar

Is there any way to customize with css the checkboxes that represent an Entity Type?

Reply

Hey @mike

Yes, that's totally doable, you just have to load your custom CSS in the template. If you want to add a CSS class into the checkbox, well, there are many ways to do it (you can check our Symfony forms tutorial), but here is one: https://symfony.com/doc/cur...

Cheers!

Reply
Gaston P. Avatar
Gaston P. Avatar Gaston P. | posted 4 years ago

I can't make it work on Symphony 4.
Warning: count(): Parameter must be an array or an object that implements Countable
in vendor\symfony\symfony\src\Symfony\Component\Validator\Validator\RecursiveContextualValidator.php at line 712
Thnaks!

Reply

Hey Gaston,

What exactly code causes this error?

Cheers!

Reply
Gaston P. Avatar

Hi Victor! it happens when trying to login at http://127.0.0.1:8000/login (after submit the user-pass). I've followed all this tutorial from the code without problem till this chapter.
Thank you!

Reply

Hey Gaston,

Ah, now I see. That was a bug with PHP 7.2 that is fixed in further versions of dependencies. You can update all your dependencies with "composer up", or do a specific update at least: "composer up symfony/symfony twig/twig" - this helped me to get rid of this error.

Cheers!

1 Reply

Hey Yuriy,

I'm glad it was helpful for you :) Thanks for letting us know!

Cheers!

1 Reply
Thomas L. Avatar
Thomas L. Avatar Thomas L. | posted 5 years ago

Hi Ryan,

I've set up ManyToMany Relationship. But as you mentioned before in my case the selectfield is filled up with a huge amount of data (which is a performance impact) I've tried to filter the form query with a custom query but I failed ... because I have no idea to fetch only the data which is really used. It is possible to fetch only the "selected" fields? In example case only showing up the users who are Genus scientists?

For adding / removing I would later build an ajax-query, so if user is typing in a part of username doctrine is quering the matching names out of database instead of loading all the huge bunch of data.

Thanks Ryan!

Regards,
Thomas

Reply

Ok, no problem! The issue is that, even if your user will only have, for example, 3 GenusScientists selected, Symfony needs to build the entire select field so that it's displayed to the user. In other words, Symfony needs to query for ALL GenusScientist objects in order to build that select field. So, there's no simple way around that. Of course, by the time you have this problem, you probably also have a select field that is SO long, it's kind of unusable :).

So, we need to "graduate" to the next solution! Unfortunately, I wish Symfony had a little bit more in "core" to help with this next solution - but there are some great libraries that can help. Here's the general idea - the specifics will vary: instead of rendering a select field with all of the scientists, we will instead render an &lt;input type="hidden" field, where the value is a comma-separated list of the id's selected, e.g. value="10, 5, 11". Then, you will use some JavaScript widget to build a fancy select box. You'll configure an AJAX auto-complete with this widget that, when you type, will send an AJAX request to a new Symfony endpoint that will return all the matching scientists based on what is typed. This is a classic AJAX auto-complete situation. However, when the user does select a scientist, you'll need to configure that widget to actually add this as another CSV item to the hidden input field - so we now would have value="10, 5, 11, 6". Now, the only really tricky part is how to get this to work well with the form system. Because, when these values are submitted - 10, 5, 11, 6 - we need to transform these into the 4 related entities, before setting them on the Genus object. Honestly, we need to create a little tutorial for this - it comes up all the time, and it's a little bit tricky. I've created a gist which is loosely based off of some code we have in our repository. The code is quite old, and it's honestly a bit messy, but it might help give you the right idea: https://gist.github.com/weaverryan/bb95f063895c546c471ad609ee8bf6cd. The trickiest part is creating a new custom form field that is able to transform the CSV is ids into an array of object on submit. This field basically works a lot like the EntityType field, except that it renders as a CSV text field instead (and so doesn't have the performance problem, since it doesn't need to load all the options).

I hope this helps!

1 Reply
Thomas L. Avatar

Me again Ryan :) I had time to cover this, just went through the simple way. First I filled form with a hidden field with id (in my case it is only 1 id, but it doesn't matter if 1 ore more). If user now is start typing within form field live search pops up and doctrine is catching the most suitable entries. Now if entry is clicked the hidden form field is filled with the new id.

After submitting the form doctrine catch up the entity and setting it to the object.

I am not complete with all your tutorials but if I find a better solution I will keep you informed :)

Reply
Thomas L. Avatar

Hi Ryan, thanks a lot for giving this great hint. I will check this out and come back to you with my results.

Reply
Bertin Avatar

I've someone gets the error:
Type error: Argument 1 passed to AppBundle\Form\GenusFormType::AppBundle\Form\{closure}() must be an instance of AppBundle\Repository\SubFamilyRepository, instance of Doctrine\ORM\EntityRepository given.

Then they must change the following line in the repository User class
@ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")

Maybe i missed it somewhere in the video's

Reply

Yo Bertin!

Ah, this is probably my fault actually :). I made a few changes *before* this tutorial, including adding some more fields to User and adding this code for the repository. So, if you're coding straight through from the previous tutorial, you'll be missing these things. You should have them download the fresh start code for this tutorial. I *usually* mention this very clearly at the beginning of the tutorial ("Hey! I made some updates, to download the fresh start code"), but I apparently forgot in this case. Sorry for the confusion!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice