Buy

Symfony 4 Forms: Build, Render & Conquer!

0%
Buy

EntityType: Drop-downs from the Database

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

Login Subscribe

On submit, we set the author to whoever is currently logged in. I want to change that: sometimes the person who creates the article, isn't the author! They need to be able to select the author.

ChoiceType: Maker of select, radio & checkboxes

Go to the documentation and click back to see the list of form field types. One of the most important types in all of Symfony is the ChoiceType. It's kind of the loud, confident, over-achiever in the group: it's able to create a select drop-down, a multi-select list, radio buttons or checkboxes. It even works on weekends! Phew!

If you think about it, that makes sense: those are all different ways to choose one or more items. You pass this type a choices option - like "Yes" and "No" - and, by default, it will give you a select drop-down. Want radio buttons instead? Brave choice! Just set the expanded option to true. Need to be able to select "multiple" items instead of just one? Totally cool! Set multiple to true to get checkboxes. The ChoiceType is awesome!

But... we have a special case. Yes, we do want a select drop-down, but we want to populate that drop-down from a table in the database. We could use ChoiceType, but a much easier, ah, choice, is EntityType.

Hello EntityType

EntityType is kind of a "sub-type" of choice - you can see that right here: parent type ChoiceType. That means it basically works the same way, but it makes it easy to get the choices from the database and has a few different options.

Head over to ArticleFormType and add the new author field. I'm calling this author because that's the name of the property in the Article class. Well, actually, that doesn't matter. I'm calling this author because this class has setAuthor() and getAuthor() methods: they are what the form system will call behind the scenes.

... lines 1 - 10
class ArticleFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 16 - 22
->add('author')
... line 24
}
... lines 26 - 32
}

As soon as we add this field, go try it! Refresh! Hello drop-down! It is populated with all the users from the database... but... it might look a little weird. By default, the EntityType queries for all of the User objects and then uses the __toString() method that we have on that class to figure out what display value to use. So, firstName. If we did not have a __toString() method, we would get a huge error because EntityType wouldn't know what to do. Anyways, we'll see in a minute how we can control what's displayed here.

Set the Type, Options are Not Guessed

So... great first step! It looks like the form guessing system correctly sees the Doctrine relation to the User entity and configured the EntityType for us. Go team!

But now, pass the type manually: EntityType::class. That should make no difference, right? After all, the guessing system was already setting that behind the scenes!

... lines 1 - 23
->add('author', EntityType::class)
... lines 25 - 35

Well... we're programmers. And so, we know to expect the unexpected. Try it! Surprise! A huge error!

The required option class is missing

But, why? First, the EntityType has one required option: class. That makes sense: it needs to know which entity to query for. Second, the form type guessing system does more than just guess the form type: it can also guess certain field options. Until now, it was guessing EntityType and the class option!

But, as soon as you pass the field type explicitly, it stops guessing anything. That means that we need to manually set class to User::class. This is why I often omit the 2nd argument if it's being guessed correctly. And, we could do that here.

... lines 1 - 24
->add('author', EntityType::class, [
'class' => User::class,
])
... lines 28 - 38

Try it again. Got it!

Controlling the Option Display Value

Let's go see what else we can do with this field type. Because EntityType's parent is ChoiceType, they share a lot of options. One example is choice_label. If you're not happy with using the __toString() method as the display value for each option... too bad! I mean, you can totally control it with this option!

Add choice_label and set it to email, which means it should call getEmail() on each User object. Try this. I like it! Much more obvious.

... lines 1 - 24
->add('author', EntityType::class, [
... line 26
'choice_label' => 'email',
])
... lines 29 - 39

Want to get fancier? I thought you would. You can also pass this option a callback, which Symfony will call for each item and pass it the data for that option - a User object in this case. Inside, we can return whatever we want. How about return sprintf('(%d) %s') passing $user->getId() and $user->getEmail().

... lines 1 - 24
->add('author', EntityType::class, [
... line 26
'choice_label' => function(User $user) {
return sprintf('(%d) %s', $user->getId(), $user->getEmail());
}
])
... lines 31 - 41

Cool! Refresh that! Got it!

The "Choose an Option" Empty Value

Another useful option that EntityType shares with ChoiceType is placeholder. This is how you can add that "empty" option on top - the one that says something like "Choose your favorite color". It's... a little weird that we don't have this now, and so the first author is auto-selected.

Back on the form, set placeholder to Choose an author. Try that: refresh. Perfecto!

... lines 1 - 24
->add('author', EntityType::class, [
... lines 26 - 29
'placeholder' => 'Choose an author'
])
... lines 32 - 42

With all of this set up, go back to our controller. And... remove that setAuthor() call! Woo! We don't need it anymore because the form will call that method for us and pass the selected User object.

We just learned how to use the EntityType. But... well... we haven't talked about the most important thing that it does for us! Data transforming. Let's talk about that next and learn how to create a custom query to select and order the users in a custom way.

Leave a comment!

  • 2019-01-21 Diego Aguiar

    Hey Radu Barbu

    So, the case is that you are passing a String when what you really want is to pass an Array, in this case what you need is to apply a DataTransformer into the desired field.
    You can see how Ryan implements his own DataTransformer here: https://symfonycasts.com/sc...

    Cheers!

  • 2019-01-21 Radu Barbu

    I have an issue while trying to save the user role(s) to the database. I get the famous Expected argument of type "array", "string" given at property path "roles". The issue occurs when I actually hit the save button. The initial form rendering works as expected.

    While setting the 'multiple' => true, as many suggest, this is not the way I need my option to be set. I just want to be able to pick an option from the dropdown.

    I thought of creating an Entity for the roles but this would not make sense since S4 has all the basic roles in the core already.

    My code looks like this:

    the FormType: OrganizationUserType.php


    ->add('roles', ChoiceType::class, [
    'label' => 'Role',
    'placeholder' => 'Choose an option',
    'required' => 'false',
    'choices' => [
    'Member' => 'ROLE_USER',
    'Admin' => 'ROLE_ADMIN',
    ]
    ])

    the Entity: OrganizationUser.php


    /**
    * @ORM\Column(type="json")
    */
    private $roles = [];

    /**
    * @see UserInterface
    */
    public function getRoles(): array
    {
    $roles = $this->roles;
    // guarantee every user at least has ROLE_USER
    $roles[] = 'ROLE_USER';

    return array_unique($roles);
    }

    public function setRoles(array $roles): self
    {
    $this->roles = $roles;

    return $this;
    }

    Any thoughts on how could I handle this one?

    Cheers!

  • 2018-12-13 Diego Aguiar

    Hmm, I think that's because of the custom matcher function scope level (woh, that was long), it might be at another level where it cannot access to the "stripDiacritic" function. To be honest I'm not sure how to properly fix it but probably you are able to replicate that function and load it at a scope level where you can actually use it.

    Cheers and sorry for the late replay (again!).

  • 2018-12-10 Dirk J. Faber

    I hope you are feeling better! I don't think the CHAR set is the problem, because with the regular matching code everything works fine. This is (I think) because of a method called 'stripDiacritics'. The problem I have is that when I use a custom matcher (to find also the abbreviation) I cannot seem to access this method stripDiacritic.

  • 2018-11-26 Diego Aguiar

    Hey Dirk J. Faber!

    Sorry for the late reply. I've been sick (I damn you flu!), about your problem, I believe that's a problem related to the CHAR set. Have you tried to apply a UTF8 encoding?

    Cheers!

  • 2018-11-19 weaverryan

    Hey Roman!

    Interesting! This tells me that the Form system is calling getAuthor() on your User object... which is odd. Are you passing an Article object when you call createForm()?

    Cheers!

  • 2018-11-18 Roman

    Return value of App\Entity\Article::getAuthor() must be an instance of App\Entity\User, null returned

  • 2018-11-16 Dirk J. Faber

    It took me a while, but I managed to make it work, thanks to your suggestion. The reason it took me so long is because in all honesty I don't really understand JavaScript.
    I have this Entity called 'Institution' that has the attribute 'internationalName' (used in the __toString() ) and 'abbreviation'. I wanted users to be able to search not only on the name, but also the abbreviation. So to the IntitutionType i added:

    'choice_attr' => function(Institution $institution) {
    // adds a class 'data-abbreviation'.
    return ['data-abbreviation' => $institution->getAbbreviation()];
    }

    And then some JS:


    function abbreviationMatcher(params, data) {

    // If there are no search terms, return all of the data
    if ($.trim(params.term) === '') {
    return data;
    }

    // Do not display the item if there is no 'text' property
    if (typeof data.text === 'undefined') {
    return null;
    }

    // Check if the international name occurs
    if (data.text.toLowerCase().indexOf(params.term.toLowerCase()) > -1){
    return data;
    }

    // Check if the data of second attribute 'abbreviation' occurs
    if ($(data.element).data('abbreviation').toLowerCase().indexOf(params.term.toLowerCase()) > -1 ) {
    return data;
    }

    // Return `null` if the term should not be displayed
    return null;
    }

    $('select[data-select="true"]').each(function () {

    $(this).select2(
    { matcher: abbreviationMatcher, width: '100%' , placeholder: $(this).data('placeholder') || $(this).attr('data-placeholder' )}

    )
    });

    The only thing I cannot get working is to strip the diacritics, like Select2 does.

  • 2018-11-16 Dirk J. Faber

    Hi Diego,

    Thank you once again for your help. I am gonna give it a shot with your suggestion, and if I manage to succeed, of course I will let you know!

  • 2018-11-16 Diego Aguiar

    Hey Dirk J. Faber

    That's a nice question, you made me wonder about it for a moment :)

    What I would do if I were on your case, I would add the exact text that you want to use for searching in a data attribute by defining the "choice_attr" field (https://symfony.com/doc/cur..., then, you can change the logic of how "Select2" searches for things (https://select2.org/searching). I don't have a real example but I hope it gives you an idea of how to achieve your goal :)

    Cheers!

  • 2018-11-16 Dirk J. Faber

    This is very nice, thank you. I am using the 'choice_label' now to combine two attributes of an entity I want to have in that label. I wonder (and I don't know if this is even possible or has anything to do with symfony forms in particular), would it be possible to have the names of both attributes in the label, but only have one of them visible? You might wonder why I would want such a thing, but this is because I also use Select2 to search for the options, and I would like to be able to search for my second attribute, but not show it. Thx!

  • 2018-11-12 Diego Aguiar

    Hey Yahya A. Erturan

    You almost have it, just add all other attributes into the `attr` array:


    $resolver->setDefaults(array(
    'attr' => [
    'class' => 'select2'
    'data-something' => 'some value'
    ]
    ));

    Cheers!

  • 2018-11-09 Yahya A. Erturan

    One question: I want to extend ChoiceType to MySelect2Type. In MySelect2Type;


    public function configureOptions(OptionsResolver $resolver)
    {
    $resolver->setDefaults(array(
    'expanded'=> false,
    'multiple' => false,
    'attr' => ['class' => 'select2']
    ));
    ...

    It works but in Form if I add attr for example to add a data-attribute, it overrides 'attr' => ['class' => 'select2'].

    How to overcome this?