If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
We 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.
// 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
}
}