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 know there are a bunch of built-in actions, like "delete" and "edit. But sometimes you need to manipulate an entity in a different way! Like, how could we add a "publish" button next to each Genus?
There are... two different ways to do that. Click into the show view for a Genus. On show, the actions show up at the bottom. Before we talk about publishing, I want to add a new button down here called "feed"... ya know... because Genuses get hungry. When we click that, it should send the user to a custom controller where we can write whatever crazy code we want.
The first step should feel very natural. We already know how to add actions, remove actions and customize how they look. Under Genus
, add a new show
key and actions
. Use the expanded configuration, with name: genus_feed
and type: route
:
... lines 1 - 80 | |
easy_admin: | |
... lines 82 - 97 | |
entities: | |
Genus: | |
... lines 100 - 118 | |
show: | |
actions: | |
- | |
name: 'genus_feed' | |
type: 'route' | |
... lines 124 - 157 |
There are two different custom action "types": route
and action
. Route is simple: it creates a new link to the genus_feed
route. And you can use any of the normal action-configuring options, like label
, css_class: 'btn btn-info
or an icon
:
... lines 1 - 80 | |
easy_admin: | |
... lines 82 - 97 | |
entities: | |
Genus: | |
... lines 100 - 118 | |
show: | |
actions: | |
- | |
name: 'genus_feed' | |
type: 'route' | |
label: 'Feed genus' | |
css_class: 'btn btn-info' | |
icon: 'cutlery' | |
... lines 127 - 157 |
Next, we need to actually create that route and controller. In src/AppBundle/Controller
, open GenusController
. At the top, add feedAction()
with @Route("/genus/feed")
and name="genus_feed
to match what we put in the config:
... lines 1 - 15 | |
class GenusController extends Controller | |
{ | |
/** | |
* @Route("/genus/feed", name="genus_feed") | |
*/ | |
public function feedAction(Request $request) | |
{ | |
... lines 23 - 36 | |
} | |
... lines 38 - 167 | |
} |
Notice the URL for this is just /genus/feed
. It does not start with /easyadmin
. And so, it's not protected by our access_control
security.
That should be enough to get started. Refresh! There's our link! Click it and... good! Error! I love errors! Our action is still empty.
So here's the question: when we click feed on the Genus
show page... the EasyAdminBundle must somehow pass us the id of that genus... right? Yes! It does it via query parameters... which are a bit ugly! So I'll open up my profiler and go to "Request / Response". Here are the GET parameters. We have entity
and id
!
Now that we know that, this will be a pretty traditional controller. I'll type-hint the Request
object as an argument:
... lines 1 - 12 | |
use Symfony\Component\HttpFoundation\Request; | |
... lines 14 - 15 | |
class GenusController extends Controller | |
{ | |
... lines 18 - 20 | |
public function feedAction(Request $request) | |
{ | |
... lines 23 - 36 | |
} | |
... lines 38 - 167 | |
} |
Then, fetch the entity manager and the $id
via $request->query->get('id')
. Use that to get the $genus
object: $em->getRepository(Genus::class)->find($id)
.
... lines 1 - 15 | |
class GenusController extends Controller | |
{ | |
... lines 18 - 20 | |
public function feedAction(Request $request) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
$id = $request->query->get('id'); | |
$genus = $em->getRepository('AppBundle:Genus')->find($id); | |
... lines 26 - 36 | |
} | |
... lines 38 - 167 | |
} |
Cool! To feed the Genus
, we'll re-use a feed()
method from a previous tutorial. Start by creating a menu of delicious food: shrimp
, clams
, lobsters
and... dolphin
! Then choose a random food, add a flash message and call $genus->feed()
:
... lines 1 - 15 | |
class GenusController extends Controller | |
{ | |
... lines 18 - 20 | |
public function feedAction(Request $request) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
$id = $request->query->get('id'); | |
$genus = $em->getRepository('AppBundle:Genus')->find($id); | |
$menu = ['shrimp', 'clams', 'lobsters', 'dolphin']; | |
$meal = $menu[random_int(0, 3)]; | |
$this->addFlash('info', $genus->feed([$meal])); | |
... lines 31 - 36 | |
} | |
... lines 38 - 167 | |
} |
Now that all this hard work is done, I want to redirect back to the show view for this genus. Like normal, return $this->redirectToRoute()
. And actually, EasyAdminBundle only has one route... called easyadmin
:
... lines 1 - 15 | |
class GenusController extends Controller | |
{ | |
... lines 18 - 20 | |
public function feedAction(Request $request) | |
{ | |
$em = $this->getDoctrine()->getManager(); | |
$id = $request->query->get('id'); | |
$genus = $em->getRepository('AppBundle:Genus')->find($id); | |
$menu = ['shrimp', 'clams', 'lobsters', 'dolphin']; | |
$meal = $menu[random_int(0, 3)]; | |
$this->addFlash('info', $genus->feed([$meal])); | |
return $this->redirectToRoute('easyadmin', [ | |
... lines 33 - 35 | |
]); | |
} | |
... lines 38 - 167 | |
} |
We tell it where to go via query parameters, like action
set to show
, entity
set to $request->query->get('entity')
... or we could just say Genus
, and id
set to $id
:
... lines 1 - 15 | |
class GenusController extends Controller | |
{ | |
... lines 18 - 20 | |
public function feedAction(Request $request) | |
{ | |
... lines 23 - 31 | |
return $this->redirectToRoute('easyadmin', [ | |
'action' => 'show', | |
'entity' => $request->query->get('entity'), | |
'id' => $id | |
]); | |
} | |
... lines 38 - 167 | |
} |
That is it! Refresh the show page! And feed the genus. Got it! We can hit that over and over again. Hello custom action.
There's also another way of creating a custom action. It's a bit simpler and a bit stranger... but has one advantage: it allows you to create different implementations of the action for different entities.
Let's try it! In config.yml
, add another action. This time, set the name to changePublishedStatus
with a css_class
set to btn
:
... lines 1 - 80 | |
easy_admin: | |
... lines 82 - 97 | |
entities: | |
Genus: | |
... lines 100 - 118 | |
show: | |
actions: | |
... lines 121 - 126 | |
- { name: 'changePublishedStatus', css_class: 'btn' } | |
... lines 128 - 158 |
Let's do as little work as possible! So...refresh! We have a button! Click it! Bah! Big error! But, it explains how the feature works:
Warning:
call_user_func_array()
expects parameter 1 to be a valid callback, classAdminController
does not have a methodchangePublishedStatusAction()
.
Eureka! All we need to do is create that method... then celebrate!
To do that, we need to sub-class the core AdminController
. Create a new directory in Controller
called EasyAdmin
. Then inside, a new PHP class called AdminController
. To make this extend the normal AdminController
, add a use
statement for it: use AdminController as BaseAdminController
. Extend that: BaseAdminController
:
... lines 1 - 2 | |
namespace AppBundle\Controller\EasyAdmin; | |
use JavierEguiluz\Bundle\EasyAdminBundle\Controller\AdminController as BaseAdminController; | |
class AdminController extends BaseAdminController | |
{ | |
... lines 9 - 25 | |
} |
Next, create that action method: changePublishedStatusAction()
:
... lines 1 - 6 | |
class AdminController extends BaseAdminController | |
{ | |
public function changePublishedStatusAction() | |
{ | |
... lines 11 - 24 | |
} | |
} |
Notice the config key is just changePublishedStatus
- EasyAdminBundle automatically expects that Action
suffix.
And now that we're in a controller method... we're comfortable! I mean, we could write a killer action in our sleep. But... there's a gotcha. This method is not, exactly, like a traditional controller. That's because it's not called by Symfony's routing system... it's called directly by EasyAdminBundle, which is trying to "fake" things.
In practice, this means one important thing: we cannot add a Request
argument. Actually, all of the normal controller argument tricks will not work.. because this isn't really a real controller.
Instead, the base AdminController
has a few surprises for us: protected properties with handy things like the entity manager, the request and some EasyAdmin configuration.
Let's use this! Get the id query parameter via $this->request->query->get('id')
. Then, fetch the object with $entity =
$this->em->getRepository(Genus::class)->find($id):
... lines 1 - 6 | |
class AdminController extends BaseAdminController | |
{ | |
public function changePublishedStatusAction() | |
{ | |
$id = $this->request->query->get('id'); | |
$entity = $this->em->getRepository('AppBundle:Genus')->find($id); | |
... lines 13 - 24 | |
} | |
} |
Now things are easier. Change the published status to whatever it is not currently. Then, $this->em->flush()
:
... lines 1 - 6 | |
class AdminController extends BaseAdminController | |
{ | |
public function changePublishedStatusAction() | |
{ | |
$id = $this->request->query->get('id'); | |
$entity = $this->em->getRepository('AppBundle:Genus')->find($id); | |
$entity->setIsPublished(!$entity->getIsPublished()); | |
$this->em->flush(); | |
... lines 17 - 24 | |
} | |
} |
Set a fancy flash message that says whether the genus was just published or unpublished:
... lines 1 - 6 | |
class AdminController extends BaseAdminController | |
{ | |
public function changePublishedStatusAction() | |
{ | |
$id = $this->request->query->get('id'); | |
$entity = $this->em->getRepository('AppBundle:Genus')->find($id); | |
$entity->setIsPublished(!$entity->getIsPublished()); | |
$this->em->flush(); | |
$this->addFlash('success', sprintf('Genus %spublished!', $entity->getIsPublished() ? '' : 'un')); | |
... lines 19 - 24 | |
} | |
} |
And finally, at the bottom, I want to redirect back to the show page. Let's go steal that code from GenusController
. The one difference of course is that $request
needs to be $this->request
:
... lines 1 - 6 | |
class AdminController extends BaseAdminController | |
{ | |
public function changePublishedStatusAction() | |
{ | |
$id = $this->request->query->get('id'); | |
$entity = $this->em->getRepository('AppBundle:Genus')->find($id); | |
$entity->setIsPublished(!$entity->getIsPublished()); | |
$this->em->flush(); | |
$this->addFlash('success', sprintf('Genus %spublished!', $entity->getIsPublished() ? '' : 'un')); | |
return $this->redirectToRoute('easyadmin', [ | |
'action' => 'show', | |
'entity' => $this->request->query->get('entity'), | |
'id' => $id, | |
]); | |
} | |
} |
Ok friends. Refresh! It works! Ahem... I mean, we totally get the exact same error! What!?
This is because we haven't told Symfony to use our AdminController yet: it's still using the one from the bundle. The fix is actually in routing.yml
:
... lines 1 - 9 | |
easy_admin_bundle: | |
resource: "@EasyAdminBundle/Controller/" | |
type: annotation | |
prefix: /easyadmin |
This tells Symfony to import the annotation routes from the bundle's AdminController
class... which means that class is used when we go to those routes. Change this to import routes from @AppBundle/Controller/EasyAdmin/AdminController.php
instead:
... lines 1 - 9 | |
easy_admin_bundle: | |
resource: "@AppBundle/Controller/EasyAdmin/AdminController.php" | |
... lines 12 - 14 |
It will still read the same route annotations from the base class, because we're extending it. But now, it will use our class when that route is matched.
That should be all we need. Try it. Boom! Genus published. Do it again! Genus unpublished! The power... it's intoxicating...
Next! We're going to go rogue... and start adding our own custom hooks... like right before or after an entity is inserted or updated.
Hey @Igor
You just need to update the resource key value. It should point to your EasyAdmin controller(s) folder or file (depending if you pretend to have more than one controller to manage EasyAdmin actions) and it should work
// config/routes/easy_admin.yaml
easy_admin_bundle:
resource: "../../Controller/EasyAdmin/AdminController.php" # or "../../Controller/EasyAdmin/"
Cheers!
Hello there !
Firstly, a huge thanks for this gorgeous cast ! :)
I'm actually building a custom form in which there are custom fields that contains 2 thing :
a list of entity to link to the main entity
a form to add a new entity that will be listed when registered
i do have 3 of those (and one of them contains itself a couple of those).
looks like:
MainForm
- normal fields
- custom field 1 :
---- entitylist1
---- customFieldForEntitylist1
- custom field 2 :
---- entitylist2
---- customFieldForEntitylist2
- custom field 3 :
---- entitylist3
---- customFieldForEntitylist3
------- entitylist4
------- customFieldForEntitylist4 (inside customFieldForEntitylist3)
Form End
(if it's not clear enough i'll add a link to a picture)
My actual problem is when i submit the main form, i don't have those custom data.
So i tried submiting using your 'custom action' (genus_feed) and i don't have any data at all...
Is there a way to 'customize' the basic EasyAdmin insertEntity so i can there use my custom data fields ?
thanks again :D
Hey @Duri3l
Looks like you are already a pro on EasyAdmin! Congrats!
Thanks for sharing your case and solution to others
Cheers!
Thanks !
I must say i'm trying to do something nice and tidy for about 2 or 3 week so... i'm starting to get some clues on what's going on :P
Now i'm ok with both new and edit form, but i changed a bit my database : an adress can be "owned" by many users, but only one adress at a time can be the "primary adress" for a user. I just don't really know how i can design the form for this...
I first had a thought about a list whith checkbox to add adress for this user, and a couple radio to set them all "notPrimary" except for one, but i'm pretty sure this will be real big and ugly if i have a couple hundred adress in database... So i don't really know how i can do this...........
if you have any idea about this, i'd be happy :D
Did it a bit after i said it there....
i did add a js event listener on the adress select change (and call it on page load if it is edit page) to get all adress selected and add them in another select (right under it) so i can select One (and only one) adress to be the primary adress for this user !
it's fully functionnal now, just need to implement password encryption and some other stuff (like email validation and every little functionnality like these) and i'll be good for this one ^^
Nice job! You've been beating us to replying :). Thanks for posting all of your solutions - I'm sure it will be helpful to others!
Hem, i forgot...
I'm using 'normal forms' to feed my database with each other entities so... Can i tell EasyAdmin to use a custom Controller only for this custom entity form and only this one ?
I finally found what i was looking for. I did put the entity controller in easy_admin.yaml and then i customized the new action so it is now a route (user_new), then i created a route named user_new and inside it i call
return $this->executeDynamicMethod('render<entityname>Template', array('new', $this->entity['templates']['new'], $parameters));
so i do have my customized template and all that goes within.
Then inside this same function i check if the form is set and valid, and if it is i grab all the info i need and i put it into my new entity.
Here it shows how to add a new action button, but I would like to know how to remove the delete button action when I'm editing a record from an entity. I already deactivate the delete button from the list of items from the entity as it shows in the video number 9 "Dynamically Remove the delete Action Link" but if I still having this button when I edit is not useful at all. Thank you for advance!
Hey Apr
If what you want is to disable the "delete" action entirely for an entity, then you can do it by specifying that action as disabled
easy_admin:
entities:
YourEntity:
disabled_actions: ['delete']
If not, then you will have to remove the action from the "edit" action as well (just as you did it for the "list" action)
Cheers!
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.3.*", // v3.3.18
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.10.3
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.5
"symfony/swiftmailer-bundle": "^2.3", // v2.6.7
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.17.0
"sensio/distribution-bundle": "^5.0", // v5.0.25
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
"incenteev/composer-parameter-handler": "^2.0", // v2.1.4
"knplabs/knp-markdown-bundle": "^1.4", // 1.7.1
"doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
"stof/doctrine-extensions-bundle": "^1.2", // v1.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"javiereguiluz/easyadmin-bundle": "^1.16" // v1.17.21
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.7
"symfony/phpunit-bridge": "^3.0", // v3.4.40
"nelmio/alice": "^2.1", // v2.3.5
"doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
}
}
Hi guys, I have an issue.
When I set a new "resource" in easy_admin.yaml :
easy_admin_bundle:
resource: "@App/Controller/EasyAdmin/AdminController.php"
prefix: /easyadmin
type: annotation
Getting this Error:
An exception has been thrown during the rendering of a template ("Bundle "App" does not exist or it is not enabled. Maybe you forgot to add it in the registerBundles() method of your App\Kernel.php file? in @App/Controller/EasyAdmin/AdminController.php (which is being imported from "/var/www/site.loc/config/routes/easy_admin.yaml"). Make sure the "App" bundle is correctly registered and loaded in the application kernel class. If the bundle is registered, make sure the bundle path "@App/Controller/EasyAdmin/AdminController.php" is not empty.").
And the whole project doesn't work.
<b>official documentation:</b>
<a href="#">https://symfony.com/doc/master/bundles/EasyAdminBundle/book/complex-dynamic-backends.html</a>
<i>The default Symfony routing config loads first the annotations of controllers defined in src/Controller/. If you override the AdminController in that directory, the routing config defined in config/routes/easy_admin.yaml will be ignored. You can comment the contents of that file and use instead the @Route annotation of AdminController to configure that route.</i>
I changed the controller directory(src/Admin), but that didn't help either.
Any ideas what this could be?
And of course my controller:
`
namespace App\Controller\EasyAdmin;
use EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController as BaseAdminController;
class AdminController extends BaseAdminController
{
}
`