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 SubscribeSo far, we've added behavior to our code by overriding methods in our controllers. And that is a great approach, and will be what you should use in most cases. But there is another possibility: events.
Over in QuestionCrudController
, up in configureFields()
... let's return one more field: yield AssociationField::new('updatedBy')
.
This field - that lives on the Question
entity - is a ManyToOne
to User
. The idea is that, whenever someone updates a Question
, this field will be set to the User
object that just updated it. Let's make this only show up on the detail page: ->onlyOnDetail()
.
... lines 1 - 19 | |
class QuestionCrudController extends AbstractCrudController | |
{ | |
... lines 22 - 46 | |
public function configureFields(string $pageName): iterable | |
{ | |
... lines 49 - 92 | |
yield AssociationField::new('updatedBy') | |
->onlyOnDetail(); | |
} | |
... lines 96 - 104 | |
} |
Right now, in our fixtures, we are not setting that field. So if we go to any question... it says "Updated By", "Null". Our goal is to set that field automatically when a question is updated.
A great solution for this would be to use the doctrine extensions library and its "blameable" feature. Then, no matter where this entity is updated - inside the admin or not - the field would automatically be set to whoever is logged in.
But let's see if we can achieve this just inside our EasyAdmin section via events. EasyAdmin has a bunch of events that it dispatches and the best way to find them is to go into the source code. In EasyAdmin's vendor code, open the src/Event/
directory. Most of these are... pretty self explanatory! BeforeCrudAction
is dispatched at the start when any CRUD action is executed, "after" would be at the end of that action.. and we also have a bunch of things related to entities, like BeforeEntityUpdatedEvent
or BeforeEntityPersistedEvent
, where "persisted" means "created".
For our case, the one I'm looking at is BeforeEntityUpdatedEvent
. If we could run code before an entity is updated, we could set this updatedBy
field and then let it save naturally. Let's do that.
Open up BeforeEntityUpdatedEvent
and copy its namespace. Then, over on our terminal, run:
symfony console make:subscriber
Let's call it BlameableSubscriber
. It then asks us which event we want to listen to, and it suggests a bunch from the core of Symfony. The one from EasyAdminBundle won't be here, so, instead, I'll paste its namespace, then go grab its class name... and paste that too.
And... perfect! We have a new BlameableSubscriber
class! Go open that up: src/EventSubscriber/BlameableSubscriber.php
.
... lines 1 - 4 | |
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | |
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityUpdatedEvent; | |
class BlameableSubscriber implements EventSubscriberInterface | |
{ | |
public function onBeforeEntityUpdatedEvent(BeforeEntityUpdatedEvent $event) | |
{ | |
// ... | |
} | |
public static function getSubscribedEvents() | |
{ | |
return [ | |
BeforeEntityUpdatedEvent::class => 'onBeforeEntityUpdatedEvent', | |
]; | |
} | |
} |
This is a normal Symfony event subscriber and, thanks to auto configuration, Symfony will instantly see this and start using it. In other words, whenever EasyAdmin dispatches BeforeEntityUpdatedEvent
, it will call our method.
This $event
object is packed with useful info. For example, if I just say $event->
, one method is called getEntityInstance()
, which is exactly what we want.
To be able to set the updatedBy
property on our question, we're going to need the current user object, which we get via the security service. Let's autowire that: add public function __construct()
- with a Security $security
argument. Hit "alt" + "enter" and go "Initialize properties" to create that property and set it.
... lines 1 - 6 | |
use Symfony\Component\Security\Core\Security; | |
... line 8 | |
class BlameableSubscriber implements EventSubscriberInterface | |
{ | |
private Security $security; | |
public function __construct(Security $security) | |
{ | |
$this->security = $security; | |
} | |
... lines 17 - 28 | |
} |
Love it. Below, start with $question = $event->getEntityInstance()
. And then if (!$question instanceof Question)
, just return
... because this is going to be called when every entity is saved across our entire system. Next, $user = $this->security->getUser()
and if (!$user instanceof User)
, let's throw a new: LogicException()
... the exception class doesn't matter. This is a situation that will never actually happen: we only have one User
class in our app. So if you're logged in, you will definitely have this User
instance. But, this helps our editor and static analysis tools.
... lines 1 - 19 | |
public function onBeforeEntityUpdatedEvent(BeforeEntityUpdatedEvent $event) | |
{ | |
$question = $event->getEntityInstance(); | |
if (!$question instanceof Question) { | |
return; | |
} | |
$user = $this->security->getUser(); | |
// We always should have a User object in EA | |
if (!$user instanceof User) { | |
throw new \LogicException('Currently logged in user is not an instance of User?!'); | |
} | |
... lines 32 - 33 | |
} | |
... lines 35 - 43 |
Down here... we can now say $question->setUpdatedBy()
, and pass $user
.
... lines 1 - 19 | |
public function onBeforeEntityUpdatedEvent(BeforeEntityUpdatedEvent $event) | |
{ | |
... lines 22 - 32 | |
$question->setUpdatedBy($user); | |
} | |
... lines 35 - 43 |
Let's try it. This question's "Updated By" is "Null". Edit something (make sure you actually make a change so it saves), hit "Save changes" and... got it! "Updated By" is populated! And that is my current user. Sweet!
So events are a powerful concept in EasyAdmin. However, they're a little bit less important in EasyAdmin 3 and 4 than they used to be. And that's because most of our configuration is now written in PHP in our controller. So instead of leveraging events, there's often an easier way: we can just override a method in our controller.
Event subscribers still have their place, because they are a great way to do an operation on multiple entities in your system. But if you only need to do something on one entity... it's easier to override a method inside that entity's controller.
Let's try it. I'll got to the bottom of my controllerclass and override yet another method. The methods that we can override are almost a README of all the different ways that you can extend things. There's a createEntity()
method, a createEditForm()
method, and the one we want is called updateEntity()
. This is the method that actually updates and saves the entity. Before that happens, we want to set the property.
... lines 1 - 20 | |
class QuestionCrudController extends AbstractCrudController | |
{ | |
... lines 23 - 106 | |
/** | |
* @param Question $entityInstance | |
*/ | |
public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void | |
{ | |
parent::updateEntity($entityManager, $entityInstance); | |
} | |
} |
Go steal the code from our subscriber... close that event class... paste that in... and hit "OK" to add that use statement. And now we'll tweak some code: $user = $this->getUser()
... and then $question
is actually going to be $entityInstance
. So we can say $entityInstance->setUpdatedBy()
.
... lines 1 - 110 | |
public function updateEntity(EntityManagerInterface $entityManager, $entityInstance): void | |
{ | |
$user = $this->getUser(); | |
if (!$user instanceof User) { | |
throw new \LogicException('Currently logged in user is not an instance of User?!'); | |
} | |
$entityInstance->setUpdatedBy($user); | |
parent::updateEntity($entityManager, $entityInstance); | |
} | |
... lines 121 - 122 |
If you want to code defensively, since there's no type-hint on $entityInstance
, we could do another check where we say if (!$entityInstance instanceof Question)
then throw an exception. But in practice, this will always be a Question
object.
Ok: let's see if this works. Go into BlameableSubscriber
... and comment out the listener. The subscriber is still here, but it won't do anything anymore. Then go back to Questions... and edit a different question. Actually, before I do that, go look at the details to make sure there's no "Updated By". Perfect! Now edit, make a change, save your changes, and... it still works!
Next, let's do a little bit more with our admin menu, like adding sections to make this whole thing better organized.
I answered my own question, but I've got another one. The entity I'm trying to update doesn't update according to the dump of the entity after I've set the properties I want to update. Do I have to persist the entity?
Hey @Courtney-T ,
All new entities you created in the current request should be persisted first, i.e. you should call persist($yourNewEntityHere)
on them. Otherwise, you won't be saved on flush()
. Try to persist it.
Cheers!
// composer.json
{
"require": {
"php": ">=8.1.0",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/doctrine-bundle": "^2.1", // 2.5.5
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
"doctrine/orm": "^2.7", // 2.10.4
"easycorp/easyadmin-bundle": "^4.0", // v4.0.2
"handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
"knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
"knplabs/knp-time-bundle": "^1.11", // 1.17.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.5
"stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
"symfony/asset": "6.0.*", // v6.0.1
"symfony/console": "6.0.*", // v6.0.2
"symfony/dotenv": "6.0.*", // v6.0.2
"symfony/flex": "^2.0.0", // v2.0.1
"symfony/framework-bundle": "6.0.*", // v6.0.2
"symfony/mime": "6.0.*", // v6.0.2
"symfony/monolog-bundle": "^3.0", // v3.7.1
"symfony/runtime": "6.0.*", // v6.0.0
"symfony/security-bundle": "6.0.*", // v6.0.2
"symfony/stopwatch": "6.0.*", // v6.0.0
"symfony/twig-bundle": "6.0.*", // v6.0.1
"symfony/ux-chartjs": "^2.0", // v2.0.1
"symfony/webpack-encore-bundle": "^1.7", // v1.13.2
"symfony/yaml": "6.0.*", // v6.0.2
"twig/extra-bundle": "^2.12|^3.0", // v3.3.7
"twig/twig": "^2.12|^3.0" // v3.3.7
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
"symfony/debug-bundle": "6.0.*", // v6.0.2
"symfony/maker-bundle": "^1.15", // v1.36.4
"symfony/var-dumper": "6.0.*", // v6.0.2
"symfony/web-profiler-bundle": "6.0.*", // v6.0.2
"zenstruck/foundry": "^1.1" // v1.16.0
}
}
Where are you hooking in the eventSubscriber? Does it work as a standalone, or do you have to call it somewhere?