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 SubscribeOk, new challenge! I only want this edit button to be visible and accessible if the user has ROLE_SUPERADMIN
. This turns out to be a bit complicated... in part because there are two sides to it.
First, we need truly block access to that action... so that a clever user can't just hack the URL and start editing! And second, we need to actually hide the link... so that our less-than-super-admin users don't get confused.
First, let's lock down the actual controller action. How? Now we know two ways: by overriding the editAction()
in UserController
and adding a security check or by adding a PRE_EDIT
event listener. Let's use events!
Subscribe to a second event: EasyAdminEvents::PRE_EDIT
set to onPreEdit
:
... lines 1 - 5 | |
use JavierEguiluz\Bundle\EasyAdminBundle\Event\EasyAdminEvents; | |
... lines 7 - 12 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
... lines 15 - 23 | |
public static function getSubscribedEvents() | |
{ | |
return [ | |
EasyAdminEvents::PRE_EDIT => 'onPreEdit', | |
... line 28 | |
]; | |
} | |
... lines 31 - 59 | |
} |
Once again, I'll hit Alt
+Enter
as a shortcut to create that method for me:
... lines 1 - 7 | |
use Symfony\Component\EventDispatcher\GenericEvent; | |
... lines 9 - 12 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
... lines 15 - 31 | |
public function onPreEdit(GenericEvent $event) | |
{ | |
... lines 34 - 37 | |
} | |
... lines 39 - 59 | |
} |
And just like before... we don't really know what the $event
looks like. So, dump it!
Now, as soon as I hit edit... we see the dump! Check this out: this time, the subject
property is actually an array. But, it has a class
key set to the User
class. We can use that to make sure we only run our code when we're editing a user.
In other words, $config = $event->getSubject()
and if $config['class']
is equal to our User
class, then we want to check security:
... lines 1 - 12 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
... lines 15 - 31 | |
public function onPreEdit(GenericEvent $event) | |
{ | |
$config = $event->getSubject(); | |
if ($config['class'] == User::class) { | |
... line 36 | |
} | |
} | |
... lines 39 - 59 | |
} |
Let's call a new method... that we'll create in a moment: $this->denyAccessUnlessSuperAdmin()
:
... lines 1 - 12 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
... lines 15 - 31 | |
public function onPreEdit(GenericEvent $event) | |
{ | |
$config = $event->getSubject(); | |
if ($config['class'] == User::class) { | |
$this->denyAccessUnlessSuperAdmin(); | |
} | |
} | |
... lines 39 - 59 | |
} |
At the bottom, add that: private function denyAccessUnlessSuperAdmin()
:
... lines 1 - 12 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
... lines 15 - 53 | |
private function denyAccessUnlessSuperAdmin() | |
{ | |
... lines 56 - 58 | |
} | |
} |
Now... we just need to check to see if the current user has ROLE_SUPERADMIN
. How? Via the "authorization checker" service. To get it, type-hint a new argument with AuthorizationCheckerInterface
. Hit Alt
+Enter
to create and set that property:
... lines 1 - 9 | |
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; | |
... lines 11 - 12 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
... line 15 | |
private $authorizationChecker; | |
public function __construct(TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $authorizationChecker) | |
{ | |
... line 20 | |
$this->authorizationChecker = $authorizationChecker; | |
} | |
... lines 23 - 59 | |
} |
Then, back down below, if (!$this->authorizationChecker->isGranted('ROLE_SUPERADMIN')
, then throw a new AccessDeniedException()
:
... lines 1 - 10 | |
use Symfony\Component\Security\Core\Exception\AccessDeniedException; | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
... lines 15 - 53 | |
private function denyAccessUnlessSuperAdmin() | |
{ | |
if (!$this->authorizationChecker->isGranted('ROLE_SUPERADMIN')) { | |
throw new AccessDeniedException(); | |
} | |
} | |
} |
Make sure you use the class from the Security component:
... lines 1 - 10 | |
use Symfony\Component\Security\Core\Exception\AccessDeniedException; | |
... lines 12 - 61 |
Oh, and don't forget the new
!
... lines 1 - 12 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
... lines 15 - 53 | |
private function denyAccessUnlessSuperAdmin() | |
{ | |
if (!$this->authorizationChecker->isGranted('ROLE_SUPERADMIN')) { | |
throw new AccessDeniedException(); | |
} | |
} | |
} |
See, normally, in a controller, we call $this->denyAccessUnlessGranted()
. When we do that, this is actually the exception that is thrown behind the scenes. In other words, we're really doing the same thing that we normally do in a controller.
And... we're done! The service is set to be autowired, so Symfony will know to pass us the authorization checker automatically. Refresh!
Great news! Access denied! Woohoo! I've never been so happy to get kicked out of something. Our user does not have ROLE_SUPERADMIN
- just ROLE_ADMIN
and ROLE_USER
. To double-check our logic, open app/config/security.yml
, and, temporarily, for anyone who has ROLE_ADMIN
, also give them ROLE_SUPERADMIN
:
security:
role_hierarchy:
ROLE_ADMIN: [ROLE_MANAGE_GENUS, ROLE_ALLOWED_TO_SWITCH, ROLE_SUPERADMIN]
Now we should have access. Try it again!
Access granted! Comment-out that ROLE_SUPERADMIN
.
Time for step 2! On the list page, we need to hide the edit link, unless I have the role. This is trickier: there's no official hook inside of EasyAdminBundle to conditionally hide or show actions. But don't worry! Earlier, we overrode the list template so that we could control exactly what actions are displayed. Our new filter_admin_actions()
filter lives in EasyAdminExtension
:
... lines 1 - 2 | |
{% block item_actions %} | |
{% set _list_item_actions = _list_item_actions|filter_admin_actions(item) %} | |
... lines 5 - 6 | |
{% endblock %} |
And we added logic there to hide the delete action for any published genuses:
... lines 1 - 6 | |
class EasyAdminExtension extends \Twig_Extension | |
{ | |
... lines 9 - 18 | |
public function filterActions(array $itemActions, $item) | |
{ | |
if ($item instanceof Genus && $item->getIsPublished()) { | |
unset($itemActions['delete']); | |
} | |
return $itemActions; | |
} | |
} |
In other words, we added our own hook to control which actions are displayed. We rock!
To hide the edit action, we'll need the authorization checker again. No problem! Add public function __construct()
with one argument: AuthorizationCheckerInterface
. Set that on a new property:
... lines 1 - 6 | |
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; | |
class EasyAdminExtension extends \Twig_Extension | |
{ | |
private $authorizationChecker; | |
public function __construct(AuthorizationCheckerInterface $authorizationChecker) | |
{ | |
$this->authorizationChecker = $authorizationChecker; | |
} | |
... lines 17 - 39 | |
} |
Then, down below, we'll add some familiar code: if $item instanceof User
and !$this->authorizationChecker->isGranted('ROLE_SUPERADMIN')
, then unset the edit
action:
... lines 1 - 8 | |
class EasyAdminExtension extends \Twig_Extension | |
{ | |
... lines 11 - 27 | |
public function filterActions(array $itemActions, $item) | |
{ | |
... lines 30 - 33 | |
if ($item instanceof User && !$this->authorizationChecker->isGranted('ROLE_SUPERADMIN')) { | |
unset($itemActions['edit']); | |
} | |
... lines 37 - 38 | |
} | |
} |
Phew! It's not the easiest thing ever... EasyAdminBundle... but it does get the job done!
Except for one... minor problem... there is also an edit button on the show page. Oh no! It looks like we need to repeat all of this for the show template!
But don't worry! With all our knowledge, this should be quick and painless.
Inside of the bundle, find the show template. And inside it, search for "actions". Here we go: block item_actions
. To control the actions, we can do a very similar thing as the list template. In fact, copy the list template, and paste it as show.html.twig
. Because it's in the right location, it should automatically override the one from the bundle.
Extend that base show.html.twig
template:
{% extends '@EasyAdmin/default/show.html.twig' %} | |
... lines 2 - 15 |
Before, we overrode the _list_item_actions
variable and then called the parent()
function to render the parent block.
But... that actually won't work here! Bananas! Why not? In this case, the variable we need to override is called _show_actions
. And... well... it's set right inside the block. That's different from list.html.twig
, where the variable was set above the block. This means that if we override _show_actions
and then call the parent block, the parent block will re-override our value! Lame!!!
No worries, it just means that we need to override the entire block, and avoid calling parent. Copy the block and, in show.html.twig
, paste:
... lines 1 - 2 | |
{% block item_actions %} | |
{% set _show_actions = easyadmin_get_actions_for_show_item(_entity_config.name) %} | |
... line 5 | |
{% set _request_parameters = { entity: _entity_config.name, referer: app.request.query.get('referer') } %} | |
{{ include('@EasyAdmin/default/includes/_actions.html.twig', { | |
actions: _show_actions, | |
request_parameters: _request_parameters, | |
translation_domain: _entity_config.translation_domain, | |
trans_parameters: _trans_parameters, | |
item_id: _entity_id | |
}, with_context = false) }} | |
{% endblock item_actions %} |
Next, add our filter: set _show_actions = _show_actions|filter_admin_actions
:
... lines 1 - 2 | |
{% block item_actions %} | |
{% set _show_actions = easyadmin_get_actions_for_show_item(_entity_config.name) %} | |
{% set _show_actions = _show_actions|filter_admin_actions(entity) %} | |
{% set _request_parameters = { entity: _entity_config.name, referer: app.request.query.get('referer') } %} | |
... lines 7 - 14 | |
{% endblock item_actions %} |
Remember, we need to pass the entity object as an argument to filter_admin_actions
... and that's another difference between show and list. Since this template is for a page that represents one entity, the variable is not called item
, it's called entity
.
As crazy as that looks, it should do it! Hold you breath, do a dance, and refresh!
Hey! No edit button! Go back to security.yml
and re-add ROLE_SUPERADMIN
:
security:
role_hierarchy:
ROLE_ADMIN: [ROLE_MANAGE_GENUS, ROLE_ALLOWED_TO_SWITCH, ROLE_SUPERADMIN]
Refresh now. Edit button is back. And we can even use it. One of the least-easy things in EasyAdminBundle is now done!
Hey screon
Do you need to remove the delete action only if certain logic applies to the given object or is it more general ? Because you could remove the action in the config.yml file. Something like this:
entities:
your_entity:
edit:
actions: ['-delete']
I hope it helps you, cheers!
Thanks for the suggestion! But I need to apply some logic on user role basis. In the video we've conditionally hidden the delete action on the list and show views, but it didn't mention anything about the edit view, where the delete button is still visible. So basically: if user doesn't have role_admin, hide the delete button. But only for that specific entity.
Hey screon ,
The way is the same as we did with EasyAdminExtension::filterActions() in this video. Actually, you just need to find a proper Twig template, override it and implement whatever crazy logic you need. You can do it just with is_granted() Twig function in the template, or create a custom Twig filter/function, move complex checking logic there and call it from the overridden template (similar to ours filterActions()).
Or, you know, take a look at JavierEguiluz\Bundle\EasyAdminBundle\Controller\AdminController::createDeleteForm(). Probably you just can to override this method and hide the button there.
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
}
}
Hey Ryan, great work on the videos!
I was wondering how you can dynamically remove the delete action from the edit view? It's seems to be baked into the form itself...