Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Field Configurator Logic

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

We just bootstrapped a field configurator: a super-hero-like class where we get to modify any field in any CRUD section from the comfort of our home. We really do live in the future.

At this point in the process, what EasyAdmin gives us is something called a FieldDto, which, as you can see, contains all the info about this field, like its value, formatted value, form type, template path and much more.

FieldDto vs Field

One thing you might have noticed is that this is a FieldDto. But when we're in our CRUD controllers, we're dealing with the Field class. Interesting. This is a pattern that EasyAdmin follows a lot. When we're configuring things, we use an easy class like Field... where Field gives us a lot of nice methods to control everything about it.

But behind the curtain, the entire purpose of the Field class - or any of the other field classes - is to take all of the info we give it and create a FieldDto. I'll call ->formatValue() temporarily and hold Cmd or Ctrl to jump into that. This moved us into a FieldTrait that Field uses.

And check it out! When we call formatValue(), what that really does is say $this->dto->setFormatValueCallable(). That Dto is the FieldDto. So we call nice methods on Field, but in the background, it uses all of that info to craft this FieldDto. This means that the FieldDto contains the same info as the Field objects, but its data, structure and methods are all a bit different.

Truncating the Formatted Value

Ok: back to our goal of truncating long textarea fields. Add a private const MAX_LENGTH = 25 to keep track of our limit:

... lines 1 - 11
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
private const MAX_LENGTH = 25;
... lines 15 - 30
}

Then, below, if (strlen($field->getFormattedValue())) is less than or equal to self::MAX_LENGTH, then just return:

... lines 1 - 11
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
... lines 14 - 20
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
if (strlen($field->getFormattedValue()) <= self::MAX_LENGTH) {
return;
}
... lines 26 - 29
}
}

And yes, I totally forgot about the <= self::MAX_LENGTH part. I'll add that later. You should add it now.

Anyways, assuming you wrote this correctly, it says that if the formatted value is already less than 25 characters, don't bother changing it: just let EasyAdmin render like normal.

Below, let's truncate: $truncatedValue =... and I'll use the u() function. Hit Tab to autocomplete that. Just like with a class, it added a use statement on top:

... lines 1 - 9
use function Symfony\Component\String\u;
... lines 11 - 32

The u function gives us a UnicodeString object from Symfony's String component.

Pass this $field->getFormattedValue() and call ->truncate() with self::MAX_LENGTH, ... and false:

... lines 1 - 11
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
... lines 14 - 20
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
if (strlen($field->getFormattedValue()) <= self::MAX_LENGTH) {
return;
}
$truncatedValue = u($field->getFormattedValue())
->truncate(self::MAX_LENGTH, '...', false);
... line 29
}
}

The last argument just makes truncate a little cleaner. Oh, and I forgot a colon right there. That's better. Finally, call $field->setFormattedValue() and pass it $truncatedValue to override what the formatted value would be:

... lines 1 - 11
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
... lines 14 - 20
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
if (strlen($field->getFormattedValue()) <= self::MAX_LENGTH) {
return;
}
$truncatedValue = u($field->getFormattedValue())
->truncate(self::MAX_LENGTH, '...', false);
$field->setFormattedValue($truncatedValue);
}
}

Let's try it! Move over, refresh and... absolutely nothing happens! All of the items in this column still have the same length as before. What's happening? It's not the bug in my code... something else is going on. But what?

Field Configurator Order

When we create a class and make it implement FieldConfiguratorInterface, Symfony's autoconfigure feature adds a special tag to our service called ea.field_configurator. That's the key to getting your field into the configurator system.

At your terminal, run symfony console debug:container. And we can actually list all the services with that tag by saying --tag=ea.field_configurator:

symfony console debug:container --tag=ea.field_configurator

Beautiful! This shows, as expected, a bunch of services: all the core field configurators plus our configurator. A few of these, like CommonPreConfigurator and CommonPostConfigurator have a priority, which controls the order in which they're called.

If you look closely, our TruncateLongTextConfigurator has a priority of 0, like most of these. But, apparently by chance, our TruncateLongTextConfigurator is being called before a different configurator that is then overriding our formatted value! I believe it's TextConfigurator. Let's go see if that's the case. Search for TextConfigurator.php and make sure to look in "All Places". Here it is!

And... yep! The TextConfigurator operates on TextField and TextareaField. And one of the things it does is set the formatted value! So our class is called first, we set the formatted value... and then a second later, this configurator overrides that. Rude!

Setting a Configurator Priority

The fix is to get our configurator to be called after this. To do that, it needs a negative priority.

Open up config/services.yaml. This is a rare moment when we need to configure a service manually. Add App\EasyAdmin\TruncateLongTextConfigurator:. We don't need to worry about any potential arguments: those will still be autowired. But we do need to add tags: with name: ea.field_configurator and priority: -1:

... lines 1 - 7
services:
... lines 9 - 30
App\EasyAdmin\TruncateLongTextConfigurator:
tags:
- { name: 'ea.field_configurator', priority: -1 }

Autoconfiguration normally add this tag for us... but with a priority of zero. By setting the tag manually, we can control that.

Whew! Testing time! Refresh and... it still doesn't work? Ok, now this is my fault. In the configurator, add the missing < self::MAX_LENGTH:

... lines 1 - 11
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
... lines 14 - 20
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
if (strlen($field->getFormattedValue()) <= self::MAX_LENGTH) {
... line 24
}
... lines 26 - 29
}
}

To fully test this... and prove the priority was needed, I'll comment out my configurator service. And... yup! The strings still aren't truncated. But if I put that back... and try it... yes! It shortened!

Over on the detail page, it also truncates here. Could we... truncate on the index page but not on the details page? Totally! It's just a matter of figuring out what the current page is from inside the configurator.

The All Powerful AdminContext

One of the arguments passed to us is AdminContext:

... lines 1 - 4
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
... lines 6 - 11
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
... lines 14 - 20
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
... lines 23 - 29
}
}

We're going to talk more about this later, but this object holds all the information about your admin section. For example, we can say $crud = $context->getCrud() to fetch a CRUD object that's the result of the configureCrud() method in our CRUD controllers and dashboard. Use this to say: if ($crud->getCurrentPage() === Crud::PAGE_DETAIL), then return and do nothing:

... lines 1 - 4
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
... lines 6 - 12
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
... lines 15 - 21
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
$crud = $context->getCrud();
if ($crud?->getCurrentPage() === Crud::PAGE_DETAIL) {
return;
}
... lines 28 - 34
}
}

Go refresh. Yes! We get the full text on the detail page. Btw, it's not too important, but there are some edge cases where $context->getCrud() could return null... so I'll code defensively:

... lines 1 - 12
class TruncateLongTextConfigurator implements FieldConfiguratorInterface
{
... lines 15 - 21
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
... line 24
if ($crud?->getCurrentPage() === Crud::PAGE_DETAIL) {
... line 26
}
... lines 28 - 34
}
}

If you hold Cmd or Ctrl to open getCrud(), yup! It returns a nullable CrudDto... though in practice, I think this is always set as long as you're on an admin page.

Next: changing the formatted value for a field is great, but limited. What if you want to render something totally different? Including custom markup and logic? To do that, we can override the field template.

Leave a comment!

7
Login or Register to join the conversation
Estelle G. Avatar
Estelle G. Avatar Estelle G. | posted 4 months ago

Hi just wanted to point out, when running

symfony console debug:container --tag=ea.field_configurator

or simply


symfony console debug:container

I get the following error in my terminal :

In CheckDefinitionValidityPass.php line 59:

The definition for "tags" has no class. If you intend to inject this service dynamically at runtime, please mark it as synthetic=true. If this is an abstract definition solely used by child definitions, please add abstract=true, otherwise specify a class to get rid of this error.

Any idea where this is coming from? TIA.

Reply
Estelle G. Avatar

Oups, my bad, wrong indentation in services.yaml was causing the issue !

Reply

Hey Estelle G.

Nice that you found how to solve issue ;)

Keep learning! Cheers!

Reply
Kevin B. Avatar
Kevin B. Avatar Kevin B. | posted 6 months ago

FYI, I believe the AsTaggedItem attribute can be used for the TruncateLongTextConfigurator class to avoid adding it in your services.yaml: #[AsTaggedItem('ea.field_configurator', -1)].

Reply

Hey Kevin B.

Yes, you're right, thanks for bringing this up. I think that's just matter of taste of how you like to handle this config stuff. My advice here is to pick one style and be consistent throughout the source code.

Cheers!

1 Reply
Kevin B. Avatar

Made a mistake above, the first argument of AsTaggedItem is the "index by" not the tag name. The tag name should already be autoconfigured so the correct usage of the attribute is (I believe): #[AsTaggedItem(priority: -1)].

Reply

Yeah, I got confused too. If you want to define the tag of a class using PHP attributes you need to use the AutoconfigureTag attribute

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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