Domain events
Domain events tell your code that something happened: a record changed, a workflow moved, a task was completed. They are raised for every way of changing data, in panels, actions, editable columns, the API and commands.
public array $domainEventListeners = [ 'record.updated' => [DealWon::class],];use Anvil\Data\DomainEvent;
final class DealWon{ public function __invoke(DomainEvent $event): void { if ($event->subject === 'deals' && $event->newValue('stage') === 'won') { $deal = (new DealModel())->findByUuid((string) $event->subjectUuid);
// ... } }}Closures can listen as well, for example in a service or a test:
service('domainEvents')->on('record.*', $listener, subject: 'deals');Events of Anvil
Section titled “Events of Anvil”| Event | When | data |
|---|---|---|
record.created |
an auditable model inserted a row | |
record.updated |
an auditable model changed a row; nothing is raised if no value changed | |
record.deleted |
an auditable model deleted a row | soft |
workflow.transitioned |
a workflow changed its state | workflow, transition, from, to, override |
task.completed |
a task was completed | workflow, task, outcome |
ownership.changed |
owner or deputies changed | owner, added_deputy, removed_deputy or valid_until |
ownership.handover |
the owner has left and nobody can take over | owner |
Your own events use the same way:
service('domainEvents')->dispatch('invoice.sent', 'invoices', $uuid, ['to' => $email]);A pattern can be an exact name, record.* or *.
The event
Section titled “The event”| Property | Meaning |
|---|---|
name, uuid |
name of the event, and its own id |
subject, subjectUuid |
kind of record (the table or $auditSubject of the model) and its UUID |
tenantId |
the tenant; listeners run in it |
actor |
who did it: user, API client or system |
data |
what the event adds |
changes, changed('stage'), oldValue('stage'), newValue('stage') |
changed fields, masked like in the audit log |
occurredAt, toArray() |
time in UTC, everything as array |
Events hold no internal ids. Load the record by its UUID. Masked fields contain the mask, not the value.
When listeners run
Section titled “When listeners run”After the commit, with on() and $domainEventListeners. The listener only
sees changes that were saved. With a transaction around several changes, it runs
when the outermost one has ended. An exception is logged and does not reach the
user, because the change is already saved. The other listeners still run.
Inside the transaction, with during(). An exception rolls the change back,
so the user sees an error. Use it to write something that has to be consistent
with the change. Keep it short: rows are locked meanwhile.
service('domainEvents')->during('record.deleted', static function (DomainEvent $event): void { // ...}, subject: 'contacts');Good to know
Section titled “Good to know”- Listeners that change records raise new events. After five levels the chain is
stopped with an entry in the log. Check
changed()to avoid such loops. - Writes that bypass the model raise no events, and models that are not auditable neither.
- In tests, listen with
service('domainEvents')->on('*', ...). Listeners stay on the shared service untilServices::resetSingle('domainEvents').