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 SubscribeRight now, we have one CRUD controller per entity. But we can create more than one CRUD controller for the same entity. Why would this be useful? Well, for example, we're going to create a separate "Pending Approval" questions section that only lists questions that need to be approved.
Ok, so, we need a new CRUD controller. Instead of generating it this time, let's create it by hand. Call the class QuestionPendingApprovalCrudController
. We're making this by hand because, instead of extending the normal base class for a CRUD controller, we'll extend QuestionCrudController
. That way, it inherits all the normal QuestionCrudController
config and logic.
... lines 1 - 2 | |
namespace App\Controller\Admin; | |
... line 4 | |
class QuestionPendingApprovalCrudController extends QuestionCrudController | |
{ | |
} |
Done! Step two: whenever we add a new CRUD controller, we need to link to it from our dashboard. Open DashboardController
... duplicate the question menu item... say "Pending Approval"... and I'll tweak the icon.
... lines 1 - 24 | |
class DashboardController extends AbstractDashboardController | |
{ | |
... lines 27 - 57 | |
public function configureMenuItems(): iterable | |
{ | |
... lines 60 - 62 | |
yield MenuItem::linkToCrud('Pending Approval', 'far fa-question-circle', Question::class) | |
->setPermission('ROLE_MODERATOR') | |
... lines 65 - 69 | |
} | |
... lines 71 - 130 | |
} |
If we stopped now, you might be thinking:
Wait a second! Both of these menu items simply point to the
Question
entity. How will EasyAdmin know which controller to go to?
This definitely is a problem. The truth is that, when we have multiple CRUD controllers for the same entity, EasyAdmin guesses which to use. To tell it explicitly, add ->setController()
and then pass it QuestionPendingApprovalCrudController::class
.
... lines 1 - 57 | |
public function configureMenuItems(): iterable | |
{ | |
... lines 60 - 62 | |
yield MenuItem::linkToCrud('Pending Approval', 'far fa-question-circle', Question::class) | |
... line 64 | |
->setController(QuestionPendingApprovalCrudController::class); | |
... lines 66 - 69 | |
} | |
... lines 71 - 132 |
Do we need to set the controller on the other link to be safe? Absolutely. And we'll do that in a few minutes.
But let's try this. Refresh. We get two links... and each section looks absolutely identical, which makes sense. Let's modify the query for the new section to only show non-approved questions. And... we already know how to do that!
Over in the new controller, override the method called createIndexQueryBuilder()
. Then we'll just modify this: ->andWhere()
and we know that our entity alias is always entity
. So entity.isApproved
(that's the field on our Question
entity) = :approved
... and then ->setParameter('approved', false)
.
... lines 1 - 4 | |
use Doctrine\ORM\QueryBuilder; | |
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection; | |
use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection; | |
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto; | |
use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto; | |
class QuestionPendingApprovalCrudController extends QuestionCrudController | |
{ | |
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder | |
{ | |
return parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters) | |
->andWhere('entity.isApproved = :approved') | |
->setParameter('approved', false); | |
} | |
} |
Let's try it! We go from a bunch question to... just five. It works! Except that if you go to the original Question section... that also only shows five!
Yup, it's guessing the wrong CRUD controller. So in practice, as soon as you have multiple CRUD controllers for an entity, you should always specify the controller when you link to it. For this one, use QuestionCrudController::class
.
... lines 1 - 24 | |
class DashboardController extends AbstractDashboardController | |
{ | |
... lines 27 - 57 | |
public function configureMenuItems(): iterable | |
{ | |
... line 60 | |
yield MenuItem::linkToCrud('Questions', 'fa fa-question-circle', Question::class) | |
->setController(QuestionCrudController::class) | |
... lines 63 - 70 | |
} | |
... lines 72 - 131 | |
} |
If we head over and refresh this page... there's no difference! That's because we modified the link... but we're already on the page for the new CRUD controller. So click the link and... much better!
Let's tweak a few things on our new CRUD controller. Override configureCrud()
. Most importantly, we should ->setPageTitle()
to set the title for Crud::PAGE_INDEX
to "Questions Pending Approval".
... lines 1 - 7 | |
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; | |
... lines 9 - 11 | |
class QuestionPendingApprovalCrudController extends QuestionCrudController | |
{ | |
public function configureCrud(Crud $crud): Crud | |
{ | |
return parent::configureCrud($crud) | |
->setPageTitle(Crud::PAGE_INDEX, 'Questions pending approval'); | |
} | |
... lines 19 - 25 | |
} |
Now... it's much more obvious which page we're on.
Oh, and when we set the page title, we can actually pass a callback if we want to use the Question
object itself in the name... assuming you're setting the page title for the detail or edit pages where you're working with a single entity.
Check it out: call ->setPageTitle()
again, and set this one for Crud::PAGE_DETAIL
. Then, instead of a string, pass a callback: a static function
that will receive the Question
object as the first argument. Inside, we can return whatever we want: how about return sprintf()
with #%s %s
... passing $question->getId()
and $question->getName()
as the wildcards.
... lines 1 - 14 | |
public function configureCrud(Crud $crud): Crud | |
{ | |
return parent::configureCrud($crud) | |
... line 18 | |
->setPageTitle(Crud::PAGE_DETAIL, static function (Question $question) { | |
return sprintf('#%s %s', $question->getId(), $question->getName()); | |
}); | |
} | |
... lines 23 - 31 |
Let's check it! Head over to the detail page for one of these questions and... awesome! Dynamic data in the title.
And while we're here, I also want to add a "help" message to the index page:
Questions are not published to users until approved by a moderator
... lines 1 - 14 | |
public function configureCrud(Crud $crud): Crud | |
{ | |
return parent::configureCrud($crud) | |
... lines 18 - 21 | |
->setHelp(Crud::PAGE_INDEX, 'Questions are not published to users until approved by a moderator'); | |
} | |
... lines 24 - 32 |
When we refresh... our message shows up right next to the title!
Okay, there's one more subtle problem that having two CRUD controllers has just created. To see it, jump into AnswerCrudController
. Find the AssociationField
for question
... and add ->autocomplete()
... which it needs because there's going to be a lot of questions in our database.
... lines 1 - 12 | |
class AnswerCrudController extends AbstractCrudController | |
{ | |
... lines 15 - 19 | |
public function configureFields(string $pageName): iterable | |
{ | |
... lines 22 - 26 | |
yield AssociationField::new('question') | |
->autocomplete() | |
... lines 29 - 36 | |
} | |
} |
If we look at our main Questions page... this first question is probably an approved question - since most are - so I'll copy part of its name. Now go to Answers, edit an answer... and go down to the Question field. This uses autocomplete, which is cool! But if I paste the string, it says "No results found"?
The reason is subtle. Go down to the web debug toolbar and open the profiler for one of those autocomplete AJAX requests. Look at the URL closely... part of it says "crudController = QuestionPendingApprovalCrudController"!
When an autocomplete AJAX request is made for an entity (in this case, it's trying to autocomplete Question), that AJAX request is done by a CRUD controller. If you jump into AbstractCrudController
... there's actually an autocomplete()
action. This is the action that's called to create the autocomplete response. It's done this way so that the autocomplete results can reuse your index query builder. Unfortunately, just like with our dashboard links, the autocomplete system is guessing which of our two CRUD controllers to use for Question... and it's guessing wrong.
To fix this, once again, we just need to be explicit. Add ->setCrudController(QuestionCrudController::class)
.
... lines 1 - 20 | |
public function configureFields(string $pageName): iterable | |
{ | |
... lines 23 - 27 | |
yield AssociationField::new('question') | |
->autocomplete() | |
->setCrudController(QuestionController::class) | |
... lines 31 - 38 | |
} | |
... lines 40 - 41 |
This time, I'll refresh... go down to the Question field, search for the string and... it finds it!
Next, what if we want to run some code before or after an entity is updated, created, or deleted? EasyAdmin has two solutions: Events and controller methods.
You could try dot notation as property name argument for new field in configureFields
method. (IDK, but I think there is PropertyAccess
under the hood):
yield Field::new('topic.name');
// or in your case
yield Field::new('certificate.testResults');
I've just tried, and I can change Topic name when editing Question. But I think there are some limitations exists. Maybe not.
Hey Klaus,
EasyAdmin relies on the Symfony Form component under the hood, so basically, you can do whatever you want as long as the Symfony Form component allows it. In this case, I think you'll need to set up an embedded form into the new/edit actions.
Cheers!
Hello, how can I add the "isApproved" field in the "QuestionPendingApprovalCrudController" crud and not in the "QuestionCrudController" crud. Basically get the fields from the "QuestionCrudController" but add the "isApproved" field.
Hey HR!
Since QuestionPendingApprovalCrudController
extends QuestionCrudController
, you could override configureFields()
in QuestionPendingApprovalCrudController
- something like this:
public function configureFields(string $pageName): iterable
{
$fields = iterator_to_array(parent::configureFields($pageName));
$fields[] = Field::new('isApproved');
return $fields;
}
Let me know if that works!
Cheers!
It works perfectly thanks weaverryan! I just replaced the Field::new()
withBooleanField::new()
to have better functionality.
Hi, is it possible to several CRUD operation on the same page. For example, edit values directly on the index page. It would be really convinient:
I have entities with only 1 text property
Hey Mofogasy,
Unfortunately, it's not possible out of the box, but I think you can write your custom code to achieve this, nothing is impossible :) You can take a look at the BooleanField and how it works - it allows you to edit boolean fields with a nice JS switcher right from the index page. I suppose you need to write something similar, though it will be a bit more complex as you would need also a text field for editing probably.
I hope this helps and good luck!
Cheers!
Hey Mofogasy,
I think the bundle has other features with a higher priority, but feel free to contribute to the bundle if you want! :) But first, I'd recommend you discuss this feature in an issue.
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
}
}
Now, I understand how to edit one entity with two controllers, but is there any way to edit two (one2one related) entities within one crud controller?
I have two entities (certificate ans testResults in a one2one relation) and I want to edit or add the test results together with the certificate.
Thank you for your help.