Buy

There'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:

23 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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():

23 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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:

23 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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":

23 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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:

23 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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:

32 lines app/config/services.yml
... 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():

41 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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:

41 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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:

281 lines src/AppBundle/Entity/User.php
... 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:

41 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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:

41 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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:

41 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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):

41 lines src/AppBundle/Event/EasyAdminSubscriber.php
... 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.

Leave a comment!