Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Multiple Cruds for a Single Entity?

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

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

Linking to the Controller and setController()

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!

Including Entity Data in the Page Title

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!

Autocomplete() and Multiple CRUD Controllers

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.

Leave a comment!

4
Login or Register to join the conversation
mofogasy Avatar
mofogasy Avatar mofogasy | posted 1 month ago

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

Reply

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!

Reply
mofogasy Avatar

Hi ok , too bad as I would have used it for many pages.
Thanks!

Reply

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!

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