Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The Field Configurator System

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

Let's finish configuring a few more fields and then talk more about a crazy-cool important system that's working behind the scenes: field configurators.

Disabling a Form Field only on Edit

One other field that I want to render in the question section is "slug": yield Field::new('slug')... and then ->hideOnIndex():

... lines 1 - 12
class QuestionCrudController extends AbstractCrudController
{
... lines 15 - 19
public function configureFields(string $pageName): iterable
{
... lines 22 - 24
yield Field::new('slug')
->hideOnIndex();
... lines 27 - 49
}
}

This will just be for the forms.

Now, when we go to Questions... it's not there. If we edit a question, it is there. Slugs are typically auto-generated... but occasionally it is nice to control them. However, once a question has been created and the slug set, it should never change.

And so on the edit page, I want to disable this field. We could remove it entirely by adding ->onlyWhenCreating()... but pff. That's too easy! Let's show it, but disable it.

How? We already know that each field has a form type behind it. And each form type in Symfony has an option called disabled. To control this, we can say ->setFormTypeOption() and pass disabled:

... lines 1 - 13
class QuestionCrudController extends AbstractCrudController
{
... lines 16 - 20
public function configureFields(string $pageName): iterable
{
... lines 23 - 25
yield Field::new('slug')
... line 27
->setFormTypeOption(
'disabled',
... line 30
);
... lines 32 - 54
}
}

But we can't just set this to "true" everywhere... since that would disable it on the new page. This is where the $pageName argument comes in handy! It'll be a string like index or edit or details. So we can set disabled to true if $pageName !==... and I'll use the Crud class to reference its PAGE_NEW constant:

... lines 1 - 13
class QuestionCrudController extends AbstractCrudController
{
... lines 16 - 20
public function configureFields(string $pageName): iterable
{
... lines 23 - 25
yield Field::new('slug')
... line 27
->setFormTypeOption(
'disabled',
$pageName !== Crud::PAGE_NEW
);
... lines 32 - 54
}
}

Let's do this! Over here on the edit page... it's disabled. And if we go back to Questions... and create a new question... we have a not disabled slug field!

Ok, enough with the question section! Close QuestionCrudController and open AnswerCrudController. Uncomment configureFields()... and then I'll paste in some fields. Oh! I just need to retype the end of these classes and hit Tab to auto-complete them... to get the missing use statements:

... lines 1 - 6
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
class AnswerCrudController extends AbstractCrudController
{
... lines 14 - 18
public function configureFields(string $pageName): iterable
{
yield IdField::new('id')
->onlyOnIndex();
yield Field::new('answer');
yield IntegerField::new('votes');
yield AssociationField::new('question')
->hideOnIndex();
yield AssociationField::new('answeredBy');
yield Field::new('createdAt')
->hideOnForm();
yield Field::new('updatedAt')
->onlyOnDetail();
}
}

Perfect There's nothing special here. You might want to add autocomplete to the question and answeredBy fields, but I'll leave that up to you.

If we refresh... the Answers page looks awesome! And if we edit one, we get our favorite error:

Object of class Question could not be converted to string

This comes from the AssociationField. The solution is to go into Question.php and add public function __toString(): string... and return $this->name:

... lines 1 - 13
class Question
{
... lines 16 - 59
public function __toString()
{
return $this->name;
}
... lines 64 - 211
}

And now... that page works too!

Globally Changing a Field

Back on the main Answers page... sometimes this text might be too long to fit nicely in the table. Let's truncate it if it's longer than a certain length. Doing this is... really easy. Head over to the answer field, use TextField... and then leverage a custom method ->setMaxLength():

public function configureFields(string $pageName): iterable
{
    // ...
    yield TextField::new('answer')
        // ...
        ->setMaxLength(50);
}

If we set this to 50, that will truncate any text that's longer than 50 characters!

But, I'm going to undo that. Why? Because I want us to do something more interesting!

Right now, I'm using Field which tells EasyAdmin to guess the best field type. This is printing as a textarea... so its field type is really TextareaField... and we can use that if we want to.

More about Field Configurators

Here's the new goal: I want to set a max length for every TextareaField across our entire app. How can we change the behavior of many fields at the same time? With a field configurator.

We talked about these a bit earlier. Scroll down: I already have /vendor/easycorp/easyadmin-bundle/ opened up. One of the directories is called Field/... and it has a subdirectory called Configurator/. After your field is created, it's passed through this configurator system. Any configurator can then make changes to any field. There are two "common" configurators. CommonPreConfigurator is called when your field is created, and it does a number of different things to your field, including building the label, setting whether it's required, making it sortable, setting its template path, etc.

There's also a CommonPostConfigurator, which runs after your field is created.

But mostly, these configurators are specific to one or just a few field types. And if you're ever using a field and something "magical" is happening behind the scenes, there's a good chance that it's coming from one of these. For example, the AssociationConfigurator is a bit complex... but it sets up all kinds of stuff to get that field working.

Knowing about these is important because it's a great way to understand what's going on under the hood, like why some field is behaving in some way or how you can extend it. But it's also great because we can create our own custom field configurator!

Let's do just that. Up in src/... here we go... create a new directory called EasyAdmin/ and, inside, a new PHP class called... how about TruncateLongTextConfigurator. The only rule for these classes is that they need to implement a FieldConfiguratorInterface:

... lines 1 - 2
namespace App\EasyAdmin;
... lines 4 - 5
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface;
... lines 7 - 10
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
... lines 13 - 21
}

Go to "Code"->"Generate" or Cmd+N on a Mac, and select "Implement Methods" to implement the two that we need:

... lines 1 - 4
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
... line 6
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
... lines 9 - 10
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
public function supports(FieldDto $field, EntityDto $entityDto): bool
{
... line 15
}
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
... line 20
}
}

Here's how this works. For every field that we return in configureFields() for any CRUD section, EasyAdmin will call the supports() method on our new class and basically ask:

Does this configurator want to operate on this specific field?

These typically return $field->getFieldFqcn() === a specific field type. In our case, we're going to target textarea fields: TextareaField::class:

... lines 1 - 8
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
public function supports(FieldDto $field, EntityDto $entityDto): bool
{
return $field->getFieldFqcn() === TextareaField::class;
}
... lines 17 - 21
}

If the field that's being created is a TextareaField, then we do want to modify it. Next, if we return true from supports, EasyAdmin calls configure(). Inside, just for now, dd() the $field variable:

... lines 1 - 10
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
... lines 13 - 17
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
dd($field);
}
}

Let's see if it triggers! Find your browser. It doesn't matter where I go, so I'll just go to the index page. And... boom! It hits! This FieldDto is full of info and full of ways to change it.

Let's dive into it next, including how this FieldDto relates to the Field objects that we return from configureFields().

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
    }
}