Events
Saving and deleting rows often triggers more work: filling in a timestamp, checking a rule, deleting child rows, or sending a notification. The ORM dispatches events around each write, so you can keep this logic with the entity that owns it, instead of repeating it in every controller.
Minimal Setup
Add an event attribute to a public static method of the entity. The method receives the event as its first argument.
use Windwalker\ORM\Event\AfterSaveEvent;
#[Table('sakuras')]
class Sakura implements EntityInterface
{
use EntityTrait;
// ...
#[AfterSaveEvent]
public static function afterSave(AfterSaveEvent $event): void
{
// Runs after each save
}
}The method must be static, because the ORM can run an event without creating an entity object.
Showcase
The following entity fills in a slug before it is saved, and deletes its child rows after it is deleted:
use Windwalker\ORM\Event\AfterDeleteEvent;
use Windwalker\ORM\Event\BeforeSaveEvent;
#[Table('sakuras')]
class Sakura implements EntityInterface
{
use EntityTrait;
// ...
#[BeforeSaveEvent]
public static function beforeSave(BeforeSaveEvent $event): void
{
if (empty($event->data['slug'])) {
$event->data['slug'] = strtolower(
str_replace(' ', '-', $event->data['title'] ?? '')
);
}
}
#[AfterDeleteEvent]
public static function afterDelete(AfterDeleteEvent $event): void
{
$event->orm->deleteBatch(
SakuraImage::class,
['sakura_id' => $event->entity->id],
);
}
}$event->data holds the values that are about to be written. Changing it changes what is stored, so the listener can normalize a value before the row is saved.
Event Types
The following events are dispatched around the write operations:
BeforeSaveEventruns before a row is created or updated. Use it to prepare the data.BeforeStoreEventruns just before the statement is sent to the database, after the save events.AfterSaveEventruns after a row is created or updated. Use it to write related rows.BeforeDeleteEventruns before a row is deleted. Use it to refuse a deletion that would lose important data.AfterDeleteEventruns after a row is deleted. Use it to delete related rows.BeforeCopyEventandAfterCopyEventrun aroundcopy(). Use them to clear identifiers or to copy related rows.BeforeUpdateWhereEventandAfterUpdateWhereEventrun around bulk updates.BeforeCreateBulkEventandAfterCreateBulkEventrun around bulk inserts.EntitySetupEventruns while the entity metadata is built. Thesetup()method receives it.EnergizeEventruns when an entity is energized by the ORM. See Energize.
INFO
Bulk writes dispatch only the bulk-level events. A bulk update does not run BeforeSaveEvent or AfterSaveEvent for each row.
Event Properties
Every entity event inherits the data array, and it exposes the ORM through $event->orm, the mapper through $event->entityMapper, and the database adapter through $event->db. The other properties depend on the event. The list below is a summary. The complete signatures are in the Windwalker\ORM\Event namespace.
Save and Copy Events
These properties are shared by BeforeSaveEvent, BeforeStoreEvent, AfterSaveEvent, BeforeCopyEvent, and AfterCopyEvent:
data: the values to write, keyed by column name.source: the input that the caller passed, as an array or an object.type:create,update, orcopy.isCreateandisUpdateare shortcuts for it.options: theORMOptionsof the call.extra: an array that carries values from one save event to the next. It is passed on from the before events to the after event.oldData: the stored row before an update. It isnullon create, and whenignoreOldDatais set.oldEntity: an entity built fromoldData. It isnullon create.tempEntity: an entity built fromdata. See the note below.
The events differ in the following:
BeforeSaveEventandBeforeStoreEventhave noentity, because the row is not written yet.AfterSaveEventaddsentity, the entity after the write, with its new primary key. It also hasfullData, the values of the row after the write.AfterCopyEventaddsentityandfullData, likeAfterSaveEvent.
WARNING
Changing tempEntity has no effect on the saved row. It is a new object built from data each time you read it, and the ORM does not read it back. Change $event->data, because the ORM writes the values in data.
#[BeforeSaveEvent]
public static function beforeSave(BeforeSaveEvent $event): void
{
// Has no effect on the saved row
// $event->tempEntity->title = 'Changed';
// Changes the saved row
$event->data['title'] = strtoupper($event->data['title'] ?? '');
}TIP
Read tempEntity to check values through the casts of the entity, for example to compare a typed date. Write data to change them.
Delete Events
BeforeDeleteEvent and AfterDeleteEvent share these properties:
conditions: the condition of theDELETEstatement. Changing it in the before event changes which rows are deleted.entity: the row that is deleted. It is set when the delete runs on a row with a primary key, and it isnullfor a delete by conditions only.data: the values of the row, extracted fromentity.options: theORMOptionsof the call.
AfterDeleteEvent adds statement, the StatementInterface of the delete.
Bulk Update Events
BeforeUpdateWhereEvent and AfterUpdateWhereEvent share these properties:
conditions: the condition of theUPDATE. A change in the before event changes which rows are updated.data: the values to set. A change in the before event changes the values.source: the input that the caller passed.options: theORMOptionsof the call.
AfterUpdateWhereEvent adds statement.
Bulk Create Events
BeforeCreateBulkEvent and AfterCreateBulkEvent share these properties:
items: the rows to insert. A change in the before event changes the rows that are inserted.entities: the entities of the call, when they were given.options: theORMOptionsof the call.
Other Events
EntitySetupEventhasmetadata, theEntityMetadataof the entity that is being set up.EnergizeEventhasentity, and the methodsstore(),storeCallback(),retrieve(),forget(), andall(). See Energize.WatchEventhasoriginEvent, the save event that triggered the watch,valuefor the new value, andoldValuefor the previous one. It also hasafterCallback, andisUpdateWhere()tells whether the change came from a bulk update.
INFO
The entity property is not limited to the after events. It is on the after save and copy events, and on the delete events, before and after. A before save event has no entity, because the row is not written yet.
Injecting Services
A static event method can take services after the event argument. In a Windwalker application, the ORM uses the attributes resolver of the container, so each parameter after the event is resolved by its type, as the container would resolve a constructor argument. The event is matched by its class, and the other parameters are filled from the container.
use Windwalker\ORM\Event\AfterSaveEvent;
#[AfterSaveEvent]
public static function afterSave(
AfterSaveEvent $event,
SearchIndexer $searchIndexer,
CachePool $cache,
): void {
$id = (int) $event->data['id'];
$searchIndexer->update('sakuras', $id, $event->data['title'] ?? '');
$cache->delete('sakura.' . $id);
}The listener above keeps two other parts of the application in step with the saved row. The search index is updated, and the cached copy of the row is removed. Neither service is passed by the caller, and the entity does not store a reference to them.
INFO
Service injection works only inside a Windwalker application, where the ORM uses the attributes resolver of the container. Outside Windwalker, the ORM passes only the event. A parameter after the event must then have a default value, or the call fails. A standalone project can extend the resolver, see Injecting Services Into Callables.
TIP
Keep the listener short. A slow service call runs inside the save, so the request waits for it. Send heavy work to a queue, and let the listener only record the job.
WARNING
The ORM does not catch an exception thrown by a listener. The exception stops the rest of the call, and the caller receives it. An AfterSaveEvent listener runs after the row is written, so the row stays saved. Make the side effects safe to repeat, because a retry of the request can run the listener again.
Setting Up Listeners Through Metadata
Listeners can also be attached in a #[EntitySetup] method. The method receives the EntityMetadata of the entity. Use it when the listener needs to be declared in code, for example to attach one listener to several events.
use Windwalker\ORM\Attributes\EntitySetup;
use Windwalker\ORM\Event\AfterSaveEvent;
use Windwalker\ORM\Metadata\EntityMetadata;
#[EntitySetup]
public static function setup(EntityMetadata $metadata): void
{
$metadata->on(
AfterSaveEvent::class,
function (AfterSaveEvent $event) {
// Runs after each save
}
);
}WARNING
A listener that saves the same entity again runs the save events again. Add a condition that stops the recursion, or the process repeats until it fails.
Watching a Column
#[Watch] runs a listener when the value of one column changes. The listener receives a WatchEvent, which gives the new value and the old value.
use Windwalker\ORM\Attributes\Watch;
use Windwalker\ORM\Event\WatchEvent;
#[Watch('state')]
public static function watchState(WatchEvent $event): void
{
if ($event->value === 0) {
// The state changed to 0
}
}#[WatchBefore] attaches the listener to the save process, before the new value is stored. Use it when the listener must change other columns in the same row.
use Windwalker\ORM\Attributes\WatchBefore;
#[WatchBefore('state')]
public static function beforeState(WatchEvent $event): void
{
if ($event->value === 1) {
$event->data['published_at'] = date('Y-m-d H:i:s');
}
}The same watch can be declared in #[EntitySetup] with $metadata->watch('state', $callable). The setup method receives the EntityMetadata, and the callable receives the WatchEvent:
use Windwalker\ORM\Attributes\EntitySetup;
use Windwalker\ORM\Event\WatchEvent;
use Windwalker\ORM\Metadata\EntityMetadata;
#[EntitySetup]
public static function setup(EntityMetadata $metadata): void
{
$metadata->watch(
'state',
function (WatchEvent $event) {
if ($event->value === 0) {
// The state changed to 0
}
}
);
}To run the callable before the new value is stored, use watchBefore() in the same way. It corresponds to #[WatchBefore]:
$metadata->watchBefore(
'state',
function (WatchEvent $event) {
if ($event->value === 1) {
$event->data['published_at'] = date('Y-m-d H:i:s');
}
}
);A watch suits one reaction to one change. For a state machine with several steps, keep the transitions in one place instead of spreading them across many watchers.
Related Topics
- Database Events: the query and fetch events of the adapter, which run below the entity events.
- Persisting Data: the operations that fire these events.