Buy

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:

60 lines src/AppBundle/Form/GenusFormType.php
... 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:

60 lines src/AppBundle/Form/GenusFormType.php
... 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:

60 lines src/AppBundle/Form/GenusFormType.php
... 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):

26 lines app/Resources/views/admin/genus/_form.html.twig
{{ 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:

61 lines src/AppBundle/Form/GenusFormType.php
... 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:

38 lines src/AppBundle/DataFixtures/ORM/fixtures.yml
... 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():

16 lines src/AppBundle/Repository/UserRepository.php
... 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):

16 lines src/AppBundle/Repository/UserRepository.php
... 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():

65 lines src/AppBundle/Form/GenusFormType.php
... 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!

  • 2017-03-29 Thomas

    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 :)

  • 2017-03-21 Thomas

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

  • 2017-03-10 weaverryan

    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 <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/wea.... 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!

  • 2017-03-09 Thomas

    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