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

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!

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": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}