Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Deep Field Configuration

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

One other property that we have inside of User is $roles, which actually stores an array of the roles this user should have:

... lines 1 - 15
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 18 - 27
#[ORM\Column(type: Types::JSON)]
private array $roles = [];
... lines 30 - 281
}

That's probably a good thing to include on our admin page. And fortunately, EasyAdmin has an ArrayField!

ArrayField

Check it out! Say yield ArrayField::new('roles'):

... lines 1 - 6
use EasyCorp\Bundle\EasyAdminBundle\Field\ArrayField;
... lines 8 - 14
class UserCrudController extends AbstractCrudController
{
... lines 17 - 21
public function configureFields(string $pageName): iterable
{
... lines 24 - 36
yield ArrayField::new('roles');
}
}

And then head back to your browser. Over on the index page... nice! It renders as a comma-separated list. And on the "Edit" page... oh, that's really cool! It added a nice widget for adding and removing roles!

Adding Help Text to Fields

The only tricky part might be remembering which roles are available. Right now, you have to type each in manually. We can at least help our admins by going back to our array field and implementing a method called ->setHelp(). Add a message that includes the available roles:

... lines 1 - 6
use EasyCorp\Bundle\EasyAdminBundle\Field\ArrayField;
... lines 8 - 14
class UserCrudController extends AbstractCrudController
{
... lines 17 - 21
public function configureFields(string $pageName): iterable
{
... lines 24 - 36
yield ArrayField::new('roles')
->setHelp('Available roles: ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_MODERATOR, ROLE_USER');
}
}

Now when we refresh... much better!

->setFormType() and ->setFormTypeOptins()

But, hmm. Now that I see this, it might look even better if we had check boxes. So let's see if we can change the ArrayField to display check boxes. Hold Cmd and open this core class.

This is really interesting, because you can actually see how the field is configured inside of its new() method. It sets the template name (we'll talk about templates later), but it also sets the form type. Behind the scenes, the ArrayField uses a CollectionType. If you're familiar with the Symfony Form Component, you know that, to render check boxes, you need the ChoiceType. I wonder if we can use ArrayField... but override its form type to be ChoiceType.

Let's... give it a try!

First, above this, add $roles = [] and list our roles. Then, down here, after ->setHelp(), one of the methods we can call is ->setFormType()... there's also ->setFormTypeOptions(). Select ->setFormType() and set it to ChoiceType::class:

... lines 1 - 13
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class UserCrudController extends AbstractCrudController
{
... lines 18 - 22
public function configureFields(string $pageName): iterable
{
... lines 25 - 38
$roles = ['ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_MODERATOR', 'ROLE_USER'];
yield ArrayField::new('roles')
->setFormType(ChoiceType::class)
... lines 42 - 46
}
}

Then ->setFormTypeOptions()... because one of the options that you must pass to this form type is choices. Set this to array_combine() and pass $roles twice:

... lines 1 - 15
class UserCrudController extends AbstractCrudController
{
... lines 18 - 22
public function configureFields(string $pageName): iterable
{
... lines 25 - 38
$roles = ['ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_MODERATOR', 'ROLE_USER'];
yield ArrayField::new('roles')
->setFormType(ChoiceType::class)
->setFormTypeOptions([
'choices' => array_combine($roles, $roles),
... lines 44 - 45
]);
}
}

I love rolls!

I know, that looks weird. This will create an array where these are both the keys and the values. The result is that these will be both the values that are saved to the database if that field is checked and what is displayed to the user. Lastly, set multiple to true - because we can select multiple roles - and expanded to true... which is what makes the ChoiceType render as check boxes:

... lines 1 - 15
class UserCrudController extends AbstractCrudController
{
... lines 18 - 22
public function configureFields(string $pageName): iterable
{
... lines 25 - 38
$roles = ['ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_MODERATOR', 'ROLE_USER'];
yield ArrayField::new('roles')
->setFormType(ChoiceType::class)
->setFormTypeOptions([
'choices' => array_combine($roles, $roles),
'multiple' => true,
'expanded' => true,
]);
}
}

Alrighty! Let's see what happens. Refresh and... it... explodes! Exciting!

An error occurred resolving the options of ChoiceType: The options allow_add, allow_delete, delete_empty, entry_options and entry_type do not exist.

Hmm... I recognize these options as options that belong to the CollectionType, which is the type that the ArrayField was originally using. This tells me that something, somewhere is trying to add these options to our form type... which we don't want because... we're not using CollectionType anymore!

So... who is setting those options? This is tricky. You might expect to see them set inside of ArrayField. But... it's not here! What mysterious being is messing with our field?

Hello Field Configurators

The answer is something called a Configurator.

Scroll back down to vendor/. I've already opened easycorp/easyadmin-bundle/src/. Earlier, we were looking at the Field/ directory: these are all the built-in fields.

After a field is created, EasyAdmin runs each through a Configurator system that can make additional changes to it. This Configurator/ directory holds those. There are a couple of them - like CommonPreConfigurator - that are applied to every field. It returns true from supports()... and does various normalizations on the field. CommonPostConfigurator is another that applies to every field.

But then, there are also a bunch of configurators that are specific to just one... or maybe a few... field types, including ArrayConfigurator. This configurator does its work when the $field is an ArrayField. The $field->getFieldFqcn() is basically helping to ask:

Hey, is the current field that's being configured an ArrayField? If it is, then call my configure() method so I can do some stuff!

And... yup! Here is where those options are being added. The Configurator system is something we're going to look at more later. Heck we're even going to create our own! For now, just be aware it exists.

Refactoring to ChoiceField

So, hmm. In our situation, we don't want the ArrayConfigurator to do its work. But, unfortunately, we don't really have a choice! The Configurator is always going to apply its logic if we're dealing with an ArrayField.

And actually, that's fine! Back in UserCrudController.php, I didn't realize it at first, but there's also a ChoiceField!

... lines 1 - 7
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
... lines 9 - 14
class UserCrudController extends AbstractCrudController
{
... lines 17 - 21
public function configureFields(string $pageName): iterable
{
... lines 24 - 38
yield ChoiceField::new('roles')
... lines 40 - 42
}
}

Hold Cmd or Ctrl to open it. Yup, we can see that it already uses ChoiceType. So, we don't need to take ArrayField and try to turn it into a choice... there's already a built-in ChoiceField made for this!

And now we don't need to set the form type... and we don't need the help or the form type options. I probably could set the choices that way, but the ChoiceField has a special method called ->setChoices(). Pass that same thing: array_combine($roles, $roles). For the other options, we can say ->allowMultipleChoices() and ->renderExpanded():

... lines 1 - 14
class UserCrudController extends AbstractCrudController
{
... lines 17 - 21
public function configureFields(string $pageName): iterable
{
... lines 24 - 37
$roles = ['ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_MODERATOR', 'ROLE_USER'];
yield ChoiceField::new('roles')
->setChoices(array_combine($roles, $roles))
->allowMultipleChoices()
->renderExpanded();
}
}

How nice is that?

Let's try this thing. Refresh and... that is what I was hoping for! Back on the index... ChoiceType still renders as a nice comma-separated list.

Oh, and by the way: if you want to see the logic that makes ChoiceType render as a comma-separated list, there a ChoiceConfigurator.php. If you open that... and scroll to the bottom - beyond a lot of normalization code - here it is: $field->setFormattedValue() where it implodes the $selectedChoices with a comma.

Rendering ChoiceList as Badges

Oh, and speaking of this type - let me close some core classes - one other method we can call is ->renderAsBadges():

... lines 1 - 14
class UserCrudController extends AbstractCrudController
{
... lines 17 - 21
public function configureFields(string $pageName): iterable
{
... lines 24 - 38
yield ChoiceField::new('roles')
... lines 40 - 42
->renderAsBadges();
}
}

That affects the "formatted value" that we just saw... and turns it into these little guys. Cute!

Next, let's handle our user's $avatar field, which needs to be an upload field!

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/doctrine-bundle": "^2.1", // 2.5.5
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
        "doctrine/orm": "^2.7", // 2.10.4
        "easycorp/easyadmin-bundle": "^4.0", // v4.0.2
        "handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
        "knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
        "knplabs/knp-time-bundle": "^1.11", // 1.17.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.5
        "stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.1
        "symfony/console": "6.0.*", // v6.0.2
        "symfony/dotenv": "6.0.*", // v6.0.2
        "symfony/flex": "^2.0.0", // v2.0.1
        "symfony/framework-bundle": "6.0.*", // v6.0.2
        "symfony/mime": "6.0.*", // v6.0.2
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.0
        "symfony/security-bundle": "6.0.*", // v6.0.2
        "symfony/stopwatch": "6.0.*", // v6.0.0
        "symfony/twig-bundle": "6.0.*", // v6.0.1
        "symfony/ux-chartjs": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.7", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.7
        "twig/twig": "^2.12|^3.0" // v3.3.7
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
        "symfony/debug-bundle": "6.0.*", // v6.0.2
        "symfony/maker-bundle": "^1.15", // v1.36.4
        "symfony/var-dumper": "6.0.*", // v6.0.2
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.2
        "zenstruck/foundry": "^1.1" // v1.16.0
    }
}