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 SubscribeLet'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.
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!
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.
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()
.
// 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
}
}
Hello good evening.
How can I work with many-to-many relationship fields?