If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe 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.
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.
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?
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!
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.
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.
Hey Ruslan,
You can leverage bin/console debug:container
Symfony command by passing a service name as an argument. E.g., if you run:
bin/console debug:container ArrayConfigurator
To see info about the ArrayConfigurator
service - it will show you in the output that it ea.field_configurator
tag.
Also, another useful command would be bin/console debug:container --tags
that will print all tags and related to them services.
I hope this helps!
Cheers!
Can you tell me what is u() function in TruncateLongTextConfigurator.php ?
PHPOStorm show mi message: Undefined function 'u' :)
And Symfony show: Attempted to call function "u" from namespace "App\EasyAdmin".
Hey Lechu, I believe you forgot to add this use statement after installing the string componentuse function Symfony\Component\String\u;
The string component helps handle Unicode strings and other encoding formats. You can learn more about it here: https://symfony.com/doc/current/components/string.html#what-is-a-string
i installed string componet but it stil; not work :)
I used this command: composer require symfony/string
Hi just wanted to point out, when running <br />symfony console debug:container --tag=ea.field_configurator<br />
or simply
<br />symfony console debug:container<br />
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.
FYI, I <i>believe</i> the AsTaggedItem
attribute can be used for the TruncateLongTextConfigurator
class to avoid adding it in your services.yaml
: #[AsTaggedItem('ea.field_configurator', -1)]
.
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!
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)]
.
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!
// composer.json
{
"require": {
"php": ">=8.1.0",
"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
}
}
Hi!
How do you know that this tag =
ea.field_configurator
? And how to know the tag of our service?