Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

EasyAdmin! For an Awesomely Powerful Admin Area

4:19:16

What you'll be learning

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

So... your site needs an admin area. Do yourself a favor and skip all that custom code and jump straight into EasyAdmin bundle. Why #1? Because it'll take you a fraction of the time to build what you need. Why #2? Because it'll be even better than what you would build by hand, including built-in widgets for auto-completion and toggling boolean fields. It's... pretty sweet.

In this tutorial, we'll learn how to admin interfaces that are highly customized:

  • Install & Bootstrapping the bundle
  • Dashboards! CRUD controllers!
  • All about Fields
  • Customize everything: what properties to display, how they render, help messages, sorting, filters... and more!
  • Override templates... at many different levels
  • Take control of your forms
  • Handling security
  • Adding custom actions (and removing others)
  • Updating and configuring the menu (like adding a link to kitten videos!)
  • Hooking into events to do things before or after an entity is saved
  • Adding custom CSS/JS behaviors to the page with Webpack Encore
  • ... and more

So let's do a little bit of work for a lotta bit results (note: "lotta bit" is a term I just made up).

Tip

Love EasyAdmin? Consider sponsoring its maintainer Javier Eguiluz!


Your Guides

Victor Bocharsky Ryan Weaver

Buy Access
Login or register to track your progress!

Join the Conversation?

49
Login or Register to join the conversation
Nina-P Avatar
Nina-P Avatar Nina-P | posted 8 months ago | edited

Hello

Great tutorial. Thank you.

Please could you add an episode with

CollectionFields->useEntryCrudForm() and AssociationFields->renderAsEmbeddedForm()

or how to embed a form from another Entity with a to-many Association or point to detailed documentation?
Or maybe that's the limit of easy admin?

Looking forward for your answer.
Thank you in advance.

2 Reply

Hey Nina!

Thank you for your interest in SymfonyCasts tutorials! Unfortunately, we cannot add new chapters to the tutorials we've already released, but we can consider this as a good topic to cover for our next tutorial about EasyAdmin. We don't have specific plans to make it in a nearest feature, but I'll add it to our ideas pool.

If you want to know more about useEntryCrudForm() and renderAsEmbeddedForm() - take a look at the official docs:

Unfortunately, I personally haven't used those features yet. You can try to handle this task with that feature, but if it does not work well - you can always create a custom controller/action and write any custom logic you want there. I hope that helps!

Cheers!

Reply

Hi Knp lecturers,
thank you for this great tutorial.

I'm starting to learn symfony 6.
Looking forward for next symfony 6 track tutorial

1 Reply

Hey Suabahasa!

Thank you for your interest in SymfonyCasts tutorials and your feedback! Good time to start learning it ;) We're going to release a few more Symfony 6 tutorials shortly, and then we will create a Sf6 track - we just need at least a few courses there :) Thank you for your patience!

Cheers!

1 Reply
Thomas-S Avatar
Thomas-S Avatar Thomas-S | posted 13 days ago | edited

Hello,
great tutorial!

I have one more question though. It would be great if you could help me.

I have overridden the createIndexQueryBuilder function in a CRUD controller. This works. However, I can't set a limit for the results.

    public function __construct(EntityRepository $entityRepository)
    {
        $this->entityRepository = $entityRepository;
    }

    public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection     $filters): ORMQueryBuilder
    {
        $response = $this->entityRepository->createQueryBuilder($searchDto, $entityDto, $fields, $filters);
        $response
            ->andWhere('entity.isDeleted = 1')
            ->setMaxResults($limit) // doesn't work?
        ;

        return $response;
    }

How can I limit the results?

Reply

Hey Thomas,

I think that's not how the "index query builder" is supposed to work because it uses a paginator behind the scenes. What you can do is to reduce/increase the number of items shown on a single page. You can do it like this:

class DashboardController extends AbstractDashboardController
{
    public function configureCrud(): Crud
    {
        // ...
        $crud->setPaginatorPageSize(5);
    }
}
1 Reply
WB Avatar
WB Avatar WB | posted 2 months ago | edited

I have two tables: Authors and Posts. I was hoping to show a table with posts on the Auther's detail page. I can show a table through:

yield CollectionField::new('posts')
        ->setTemplatePath('admin/fields/posts.html.twig')
        ->onlyOnDetail();

And in that twig template, I can loop through the posts through: {% for post in field.value %}

However, I was hoping to make that table function like the tables EasyAdmin has on the Crud Index pages. Is there any way to neatly do this?

Reply

Hey WB,

Unfortunately, there's no way to do this... it's something custom you would need to implement yourself. That EA tables are tied to the EA interface and logic, so reuse it would be way to hard if ever possible I think. Probably could take a look at DataTables, see https://datatables.net/ - you can try to integrate them instead, it should be easier I think in case you don't want to render it like a simple list but add some functionality like sorting, search, etc. Otherwise you would need to implement it yourself.

I hope this helps!

Cheers!

Reply
Luis-MCM-925 Avatar
Luis-MCM-925 Avatar Luis-MCM-925 | posted 3 months ago

Hello I need help, when i try to delete a user i get this error:
You cannot refresh a user from the EntityUserProvider that does not contain an identifier.
The user object has to be serialized with its own identifier mapped by Doctrine.
the strange thing is that sometimes works and on other occasions delete more than one user

Reply

Hey @Luis-MCM-925!

Hmm, that is strange. The EntityUserProvider is a class that's involved with security. Its job is to, at the start of each request, load the curerntly-logged-in User object from the session, grab its id, and issue a query for a fresh version of that User from the database.

What's strange about your situation is that, if you delete another user, it shouldn't affect YOUR user at all... and so your User should be refreshed from the session like normal. Based on the error, for some reason, it seems that, during the request to delete, YOUR User object is losing its id before it's being stored in the session at the end of that request. It is almost like YOUR user is being deleted.

So, I can't explain what's going on - but maybe this will give you some hints.

Cheers!

Reply
Hariharasuthan-M Avatar
Hariharasuthan-M Avatar Hariharasuthan-M | posted 4 months ago

Really useful steps to going forward with minimal efforts

Reply
tupolev Avatar

Hi! I am following this tutorial, hoping that I can use it for a project I'm working it. Everythig's cool, but I can't figure out one thing:

Is the questions application open at least for reading or does it need the user to be registered before accessing to the questions&answers?
I am trying to create a simple app with the same philosofy as any blog: user can browse anonymously, but can post if registered and then admins use admin area, but I can't find the proper way to make it all in the same app: if I secure, everything needs login. If I open, no security is applied.
The maximum I got is unsecure the main page but then there is no chance to login and use user & session data in the main page.

How is it done here with symfony and easyadmin? it´s not such a weird case right? :)

Reply

Hey Tupolev,

In this chapter: https://symfonycasts.com/screencast/easyadminbundle/dashboard - we configured the minimal role that's required to access the EasyAdmin pages. As you can see, we configured it for ROLE_ADMIN, so not every user will have access the admin interface, only those who has that role. In your case, if you want to allow adding posts to all users who is registered and logged in on your website - you need to use the ROLE_USER probably. Everyone, who logged in to your site will have that simple (default) role: ROLE_USER - and so will have access to the admin interface and can create new posts. Though they will have access to remove as well unless you forbid removing manually. I would suggest to watch this course till the end coding along with the author to know more about EasyAdmin and its features to achieve this ;)

Cheers!

Cheers!

1 Reply
tupolev Avatar

Got it! it worked with the course code. It was about the minimal role and the customAuthenticator. Now I have my public area and my admin area coexisting and the next challenge is creating a new dashboard for ROLE_USER so they can manage their own profile and CRUD places and pictures under their own ownership.

Is it a good approach?

-Public area (see places, register user)
-Admin console (manage all users, places, place_pictures and settings)
-Registered user console (manage own user profile, manage own places, place_pictures)

Thanks a lot!

Reply

Hey Tupolev

Registered user console (manage own user profile, manage own places, place_pictures)

Unless you mean to do this via EasyAdmin too - this sounds good. Because EasyAdmin is something internal and meant to be used by admins only. It has no nice URLs, etc. So, I would not recommend you to expose this to users just to edit their profiles. I would prefer to create a separate custom controller that will handle only this and keep the EasyAdmin help admins only.

Cheers!

1 Reply
tupolev Avatar

Yeah a lot has happened in this time. I have implemented the public area and user panel alone and left easyadmin for admin. And it works great!

Thank you!

Reply

Hey Tupolev,

Awesome, that's the best solution I think :)

Cheers!

Reply

Hello, please help me, how to change redirect to route after save or edit row. T_T

Reply

Hey Yoelkj,

You can override the getRedirectResponseAfterSave() method in your custom CRUD controller and add the logic you need.

Cheers!

Reply

Hello, thanks for answering.

I have also seen that solution but I have not been able to override that method when it is protected. as I would do it

In the end I had to force it another way. I know it's not the best solution.

    public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void
    {
        parent::updateEntity($entityManager, $entityInstance);

        $adminUrlGenerator = $this->container->get(AdminUrlGenerator::class);
        $url = $adminUrlGenerator
                ->setRoute('admin_show_user')
                ->generateUrl();

        die(header('Location: '.$url));

    }
Reply

Hey yoelkj,

I'm afraid I didn't get why you could not override a protected method. That's the purpose of the protected scope, to be overridden (and executed) by any of its derivates

Cheers!

Reply
Nick-F Avatar

Hello I'm having an issue trying to figure out rendering association fields as embedded forms in edit and new views.
The documentation here https://symfony.com/doc/4.x/EasyAdminBundle/fields/AssociationField.html
says that there is a method on AssociationField called renderAsEmbeddedForm() that is supposed to do exactly what I need, and I have the latest easyadmin version installed but that method does not exist...
I can't find anything on the internet about it missing either, the only google search result is the documentation page that says the method should be there.

Reply

Hey Nick!

Hmmm, yup! I see it! That feature is brand new - it has been merged - https://github.com/EasyCorp/EasyAdminBundle/pull/5353 - but not released yet. It should be part of 4.5.0 and Javier (the maintainer) is pretty good about not waiting too long to release. But if you need this now, you'll need to update your composer.json to use the 4.x branch.

Cheers!

Reply
Christina-V Avatar
Christina-V Avatar Christina-V | posted 10 months ago | edited

Hi everyone,

Easyadmin is great, no doubt about it.
But...

I can't believe that there is no way to create dependent select fields, like describe here for symfony: https://symfony.com/doc/current/form/dynamic_form_modification.html#dynamic-generation-for-submitted-forms

I mean this is a really natural behaviour, an admin should provide this.
Anyway, now that my project is really advanced, I realise I'm in need of this so...

So what will be the best way to try to "hack' this and make it work ?

Reply

Hey Christina-V!

I can't believe that there is no way to create dependent select fields, like describe here for symfony: https://symfony.com/doc/current/form/dynamic_form_modification.html#dynamic-generation-for-submitted-forms

I agree. This is probably THE thing in Symfony in general that is much harder than it should be. In general, my way of solving this these days is by using LiveComponents. However, I haven't used live components in EasyAdmin yet - it would require some research to figure out how to reuse the form built by EasyAdmin from inside of your component. A compelling thing to try, but I just haven't had a chance to look into it yet.

So, for this, the process would look something like this comment - https://github.com/EasyCorp/EasyAdminBundle/issues/4716#issuecomment-1127979067 - but more specifically, I would:

A) In configureCrud(), I would call:

$crud->setFormOptions(['attr' => [
    'data-controller' => 'admin-dependent-fields',
    'data-admin-dependent-fields-url-value' => $this->container->get(AdminUrlGenerator::class)->setAction(Action::NEW)->generateUrl()
]])

This should initialize a Stimulus controller called admin-dependent-fields and pass a "value" called url to it.

B) In configureFields(), we're going to add an "action" to the first field:

public function configureFields(string $pageName): iterable
{
    yield ChoiceField::new('state')->setFormTypeOption('attr', ['data-action' => 'admin-dependent-fields#changeState']);
}

C) Create the corresponding Stimulus controller:

// assets/controllers/admin-dependent-fields-controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static values = {
        url: String
    }

    changeState(event) {
        const countryElement = event.currentTarget;
        const data = {};
        data[countryElement.name] = countryElement.value;
        fetch(this.urlValue, { method: 'POST' })
            .then(async (response) => {
                const currentStateElement = document.getElementById('the-id-attribute-on-state-field');
                const newElement = document.createElement('div');
                newElement.innerHTML = await response.text();
                const newStateElement = newElement.querySelector('#the-id-attribute-on-state-field');

                // I'm using parentElement because I'm assuming we might want to replace the entire
                // parent around the select, not just the select
                currentStateElement.parentElement.replaceWith(newStateElement.parentElement);
            });
    }
}

D) Then you will STILL need the form modification stuff found here - https://github.com/EasyCorp/EasyAdminBundle/issues/4716#issuecomment-1127979067

Phew! Probably I made some mistakes, but hopefully this will help. But yes, I hate this solution - that is WAY WAY too much work to accomplish this - I agree completely

Cheers!

Reply
Christina-V Avatar

Thanks for the reply Ryan!

It's duable I think. I'll have a look probably this week, and let you know the results ;-0

Reply
Default user avatar
Default user avatar unknown | posted 11 months ago | edited
Comment was deleted.

Hey Christina,

It depends on what you're trying to achieve but I think a good way would be to override the createIndexQueryBuilder() method

Cheers!

Reply
Christina-V Avatar

Hi Mollokan,

Indeed, my question was too "open", sorry for that.
Ok, so I'm gonna try to give some context.

I got this Registration entity, linked to some other entities (User, Activity ...).

Usually, in my CRUD controller, I'll use the default createIndexQueryBuilder, and in configureFields:

public function configureFields(string $pageName): iterable
{
        return [
            IdField::new('id')
                ->onlyOnIndex(),
            AssociationField::new('user')
            AssociationField::new('activity'),
        ];
}

And I'll have, for the index page, a table with;

ID | user | activity
1 username1 activity_x
2 username4 activity_y

But the final user of the website I'm building wants something different for this INDEX page, to have easy access to the informations they need the most:

ID | Activity.title | Activity.startDate | User.firstname | User.lastname | User.phonenumber

So I need to "choose" some specific fields, for Association.

Is it more clear ?

Thanks !

Reply

Hey Christina,

You can access that information via Doctrine relationships. For example, if User has a relationship with Activity, you can access the Activity fields by doing something like this

public function configureFields(string $pageName): iterable
{
    yield Field::new('activity.id', 'Activity ID');
}

Here you can learn more the EasyAdmin association field https://symfony.com/doc/current/EasyAdminBundle/fields/AssociationField.html

Cheers!

1 Reply
mofogasy Avatar
mofogasy Avatar mofogasy | posted 1 year ago

Hello,
your explanation is great. I want to extend this overriding possibility for my project.
I have 1 entity for article joined to another entity for images. 1 article can have many images.

when adding images to an article, I'd like to display the images and selecting them instead of just displaying their filename.
I have been trying with AssociationField but I can't get it.
Could you help me please ?

Reply
Huib B. Avatar
Huib B. Avatar Huib B. | posted 1 year ago

Symfony redirects to login page after successful login on chrome and logs out. This behavior is in some versions of chrome but not in Firefox, Edge or Safari although chrome does work in private mode and chrome (Version 105.0.5145.0) works even without private mode, any idea why login fails in chrome?

Reply

Hey Huib,

That's weird... especially the fact that it works in Chrome incognito mode but doesn't work in Chrome regular mode. It makes me think that you may have some extensions installed that causes this problem somehow. Most probably those Chrome extensions do not work in incognito mode (the default behavior) and that's why it works there - the only logical explanation about what's going on. I'd recommend you to turn off all the chrome extensions and try again. Also, try to upgrade your Chrome to the latest first. If the issue still persist - then I'm not sure. Well, you may want to open the Chrome dev toolbar, then long click on "reload" button (about 3 seconds) and choose "Empty cache and hard reload" - it might help sometimes.

I hope this helps!

Cheers!

Reply

Thanks for these tutorials. I am awaiting for your latest videos every single day!

Reply
sadikoff Avatar sadikoff | SFCASTS | Lubna | posted 1 year ago | edited

Hey Lubna,

Thanks for the feedback! We are working to get it as soon as we can :-)

Thanks for staying with us!

Cheers!

3 Reply
Ruslan Avatar

26th December in five hours. We are waiting for! :)
Thank you for your casts, it's very helpful.

Reply

Hey Ruslan!

Sorry for some delays... Unfortunately, Christmas and new year schedule made some adjustments in the release schedule on SymfonyCasts, but this course will be the next for sure. Thank you for your patience! :)

Cheers!

Reply
Peter A. Avatar
Peter A. Avatar Peter A. | posted 1 year ago

Hi,
Do we have a date for release ? It's mentioned below that it'd be ready around December 2021..?

Thanks
Peter

Reply

Hey Peter,

Yes, it should be started releasing right after the Symfony 5 Security course which should be fully released in about a week... but considering some Christmas / new year eve holidays it might be started in early January I think. But this course will be definitely the next one we're going to release ;)

Thank you for your patience!

Cheers!

Reply
Peter A. Avatar

thanks much, looking forward.

Reply

Hey Peter!

I have more top-secret news for you! We're super close to start releasing this course - yay! And we even prepared a little surprise in this course - you will see it when we start releasing :) Btw, I think it may happen on the next week, or in 2 weeks max from now. We're finishing releasing our Symfony 5 Security course in full this week, and then will switch to EA completely :)

Thanks a lot for your patience!

Cheers!

Reply
Fabrice Avatar

Hey ! So, what's about this surprise? :D

Reply

Hey Kiuega!

Ha, good question, and I'm happy you remembered about it :) So, basically, as you might notice already, we're going to cover EasyAdminBundle v4 instead of v3 as it was planned, that's based on the newest Symfony 6! So, that's the little surprise I was talking about, and I hope you're not disappointed about it ;)

Cheers!

Reply
Kiuega Avatar

Hello ! I intend to take advantage of the black friday offer to subscribe for a month to take advantage of the training on EasyAdmin 3. Do you think that it will be fully published before December 26, 2021?

Reply

Hey Kiuega,

We do plan to start releasing it in December, but I'm not sure it will be completely published until December 26. It depends on how many chapters it will have, and when exactly we will start, but most probably it will be completely released in January only.

Cheers!

Reply
Abdul M. Avatar
Abdul M. Avatar Abdul M. | posted 1 year ago

any tutorial series coming on CQRS, DDD, Microservices with Symfony and Redis with Symfony Framework!

Reply

Hey Abdul M.

For the moment we do not have any plans on releasing a tutorial about those topics but thanks for letting us know what you'd like to learn next

Cheers!

Reply
Kiuega Avatar

Damn ! This tutorial that has been requested for so long! We can't wait for him to come out!

Reply

Hey Kiuega!

I'm happy to let you know that we're actively working on this tutorial right now :) Thank you for your interest in SymfonyCasts tutorials and your patience!

Cheers!

Reply
Cat in space

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

userVoice