Event Hooks
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThere's one other major way to hook into things with EasyAdminBundle... and it's my favorite! Go back to the base AdminController
and search for "event". You'll see a lot in here! Whenever EasyAdminBundle does, well, pretty much anything... it dispatches an event: PRE_UPDATE
, POST_UPDATE
, POST_EDIT
, PRE_SHOW
, POST_SHOW
... yes we get the idea already!
And this means that we can use standard Symfony event subscribers to totally kick EasyAdminBundle's butt!
Creating an Event Subscriber
Create a new Event
directory... though, this could live anywhere. Then, how about, EasyAdminSubscriber
. Event subscribers always implement EventSubscriberInterface
:
// ... lines 1 - 2 | |
namespace AppBundle\Event; | |
// ... lines 4 - 5 | |
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | |
// ... lines 7 - 8 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 11 - 21 | |
} |
I'll go to the "Code"->"Generate" menu - or Command
+N
on a Mac - and choose "Implement Methods" to add the one required method: getSubscribedEvents()
:
// ... lines 1 - 8 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
public static function getSubscribedEvents() | |
{ | |
// ... lines 13 - 15 | |
} | |
// ... lines 17 - 21 | |
} |
EasyAdminBundle dispatches a lot of events... but fortunately, they all live as constants on a helper class called EasyAdminEvents
. We want to use PRE_UPDATE
. Set that to execute a new method onPreUpdate
that we will create in a minute:
// ... lines 1 - 4 | |
use JavierEguiluz\Bundle\EasyAdminBundle\Event\EasyAdminEvents; | |
// ... lines 6 - 8 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
public static function getSubscribedEvents() | |
{ | |
return [ | |
EasyAdminEvents::PRE_UPDATE => 'onPreUpdate', | |
]; | |
} | |
// ... lines 17 - 21 | |
} |
But first, I'll hold Command
and click into that class. Dude, this is cool: this puts all of the possible hook points right in front of us. There are a few different categories: most events are either for customizing the actions and views or for hooking into the entity saving process.
That difference is important, because our subscriber method will be passed slightly different information based on which event it's listening to.
Back in our subscriber, we need to create onPreUpdate()
. That's easy, but it's Friday and I'm so lazy. So I'll hit the Alt
+Enter
shortcut and choose "Create Method":
// ... lines 1 - 6 | |
use Symfony\Component\EventDispatcher\GenericEvent; | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 11 - 17 | |
public function onPreUpdate(GenericEvent $event) | |
{ | |
// ... line 20 | |
} | |
} |
Thank you PhpStorm Symfony plugin!
Notice that it added a GenericEvent
argument. In EasyAdminBundle, every event passes you this same object... just with different data. So, you kind of need to dump it to see what you have access to:
// ... lines 1 - 8 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 11 - 17 | |
public function onPreUpdate(GenericEvent $event) | |
{ | |
dump($event);die; | |
} | |
} |
Since we're using Symfony 3.3 and the new service configuration, my event subscriber will automatically be loaded as a service and tagged as an event subscriber:
// ... lines 1 - 5 | |
services: | |
# default configuration for services in *this* file | |
_defaults: | |
autowire: true | |
autoconfigure: true | |
public: false | |
AppBundle\: | |
resource: '../../src/AppBundle/*' | |
exclude: '../../src/AppBundle/{Entity,Repository,Tests}' | |
AppBundle\Controller\: | |
resource: '../../src/AppBundle/Controller' | |
public: true | |
tags: ['controller.service_arguments'] | |
// ... lines 21 - 32 |
If that just blew your mind, check out our Symfony 3.3 series!
This means we can just... try it! Edit a user and submit. Bam!
Fetching Info off the Event
For this event, the important thing is that we have a subject
property on GenericEvent
... which holds the User
object. We can get this via $event->getSubject()
:
// ... lines 1 - 10 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 13 - 26 | |
public function onPreUpdate(GenericEvent $event) | |
{ | |
$entity = $event->getSubject(); | |
// ... lines 30 - 38 | |
} | |
} |
Remember though, this PRE_UPDATE
event will be fired for every entity - not just User
. So, we need to check for that: if $entity instanceof User
, then we know it's safe to work our magic:
// ... lines 1 - 10 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 13 - 26 | |
public function onPreUpdate(GenericEvent $event) | |
{ | |
$entity = $event->getSubject(); | |
if ($entity instanceof User) { | |
// ... lines 32 - 37 | |
} | |
} | |
} |
Since we already took care of setting the updatedAt
in the controller, let's do something different. The User
class also has a lastUpdatedBy
field, which should be a User
object:
// ... lines 1 - 16 | |
class User implements UserInterface | |
{ | |
// ... lines 19 - 87 | |
/** | |
* @ORM\OneToOne(targetEntity="User") | |
* @ORM\JoinColumn(name="last_updated_by_id", referencedColumnName="id", nullable=true) | |
*/ | |
private $lastUpdatedBy; | |
// ... lines 93 - 279 | |
} |
Let's set that here.
That means we need to get the currently-logged-in User
object. To get that from inside a service, we need to use another service. At the top, add a constructor. Then, type-hint the first argument with TokenStorageInterface
. Watch out: there are two of them... and oof, it's impossible to know which is which. Choose either of them for now. Then, name the argument and hit Alt
+Enter
to create and set a new property:
// ... lines 1 - 8 | |
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
private $tokenStorage; | |
public function __construct(TokenStorageInterface $tokenStorage) | |
{ | |
$this->tokenStorage = $tokenStorage; | |
} | |
// ... lines 19 - 39 | |
} |
Back on top... this is not the right use
statement. I'll re-add TokenStorageInterface
: make sure you choose the one from Security\Core\Authentication
:
// ... lines 1 - 8 | |
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 13 - 39 | |
} |
In our method, fetch the user with $user = $this->tokenStorage->getToken()->getUser()
. And if the User
is not an instanceof
our User
class, that means the user isn't actually logged in. In that case, set $user = null
:
// ... lines 1 - 10 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 13 - 26 | |
public function onPreUpdate(GenericEvent $event) | |
{ | |
$entity = $event->getSubject(); | |
if ($entity instanceof User) { | |
$user = $this->tokenStorage->getToken()->getUser(); | |
if (!$user instanceof User) { | |
$user = null; | |
} | |
// ... lines 36 - 37 | |
} | |
} | |
} |
Then, $entity->setLastUpdatedBy($user)
:
// ... lines 1 - 10 | |
class EasyAdminSubscriber implements EventSubscriberInterface | |
{ | |
// ... lines 13 - 26 | |
public function onPreUpdate(GenericEvent $event) | |
{ | |
$entity = $event->getSubject(); | |
if ($entity instanceof User) { | |
$user = $this->tokenStorage->getToken()->getUser(); | |
if (!$user instanceof User) { | |
$user = null; | |
} | |
$entity->setLastUpdatedBy($user); | |
} | |
} | |
} |
Woohoo! Thanks to the new auto-wiring stuff in Symfony 3.3, we don't need to configure anything in services.yml
. Yep, with some help from the type-hint, Symfony already knows what to pass to our $tokenStorage
argument.
So go back, refresh and... no errors! It's always creepy when things work on the first try. Go to the show page for the User id 20. Last updated by is set!
Next, we're going to hook into the bundle further and learn how to completely disable actions based on security permissions.
(Note for some reason I can't see the entire post disqus is being weird)
So I am having a bit of a complicated problem (It is related I assure you), I had a thought what if I had a base entity that implemented a UpdatedByUser trait for all the entities, then I a global event within easy admin to update this field when ever an admin user updates a table.
Pretty cool, well, auditing has been my favourite things of all time, also being able to blame the person has merit too.
I am getting the
`
Call to a member function setLastUpdatedBy() on array
`
error, and I am not sure why nor do I understand it.
So I created a trait:
Then in my entities I simply used the trait - awesome.
So I come to the event:
And try it then BOOM..
I get an error I simply do NOT understand, Yes I did some hunting around just to try and understand it. Ideas?