Digging Deeper

Events and Hooks

Run your code at fixed moments of each endpoint with resource events and row hooks, and listen to the Laravel events of each write.

The package gives you three ways to run your code around an endpoint:

  • Resource events, such as searching() or destroyedAll(): methods of the resource that run once per request, at a fixed moment.
  • Row hooks, such as mutating() or destroyed(): methods of the resource that run for each row.
  • Laravel events, such as ResourceMutated: dispatched once a write is committed, for every resource.

Resource events

Override the event on the resource. Each one receives a typed payload and returns nothing:

EventWhen it runs
describing(): voidonce viewAny passed, before the details are built
described(array $details): voidafter the details are built
searching(SearchContext $search): voidafter validation, before any query; also for the search of an action or a metric
searched(LengthAwarePaginator $page): voidafter the query and the includes are read, before the rows are rendered
mutatingAll(array $entries): voidinside the transaction, after the row checks, before the first write
mutatedAll(array $operations): voidinside the transaction, after the last write
operating(Action $action, ActionFields $fields): voidafter the row checks of the action, before the first chunk
operated(Action $action, ActionFields $fields, int $impacted): voidafter the last chunk, before respond()
destroyingAll(Collection $models), restoringAll(), forceDestroyingAll()inside the transaction, after the row checks, before the first row
destroyedAll(Collection $models), restoredAll(), forceDestroyedAll()inside the transaction, after the last row
  • The events run after the entry checks and the validation. The events that receive rows also run after the row checks: a row that a policy refuses runs no event.
  • An event that throws stops the endpoint. A ValidationException gives a 422, an AuthorizationException a 403. Inside a transaction, the throw rolls it back: mutatedAll() and destroyedAll() can undo the write.
  • An event cannot change what the endpoint does: its payload is read-only. Read the request with $this->request().
  • operating() and operated() run inside the transaction of an action with ActionTransaction::Whole. Otherwise each chunk is committed, or each row with ActionTransaction::Row and ActionTransaction::None, and a throw in operated() leaves the rows written and skips respond().
  • The queued jobs of an action run no event: the events run in the request.

searching() receives a Lomkit\Rest\Search\SearchContext built from the validated search. Throw a ValidationException to refuse it before any query:

use Illuminate\Validation\ValidationException;
use Lomkit\Rest\Search\SearchContext;

public function searching(SearchContext $search): void
{
    if ($search->filtersOn('from') && ! $search->filtersOn('to')) {
        throw ValidationException::withMessages(['search.filters' => 'The from filter requires a to filter.']);
    }
}

filtersOn() and sortsOn() take a field, a relation path such as author.last_name, or the key of an aggregate. scopes() and includes() list the names of the scopes and includes. The check also applies to the search of an action and of a metric.

Logging a deletion

use Illuminate\Database\Eloquent\Collection;

public function destroyedAll(Collection $tickets): void
{
    ActivityLog::record('tickets.destroyed', $tickets->modelKeys());
}

Row hooks

Row hooks run for each row, inside the transaction of their endpoint:

HookWhen it runs
mutating(MutationNode $entry, Model $model): voidbefore a row of mutate is saved
mutated(MutationNode $entry, Model $model): voidafter a row of mutate is saved
destroying(Model $model), destroyed(Model $model)around the deletion of each row
restoring(Model $model), restored(Model $model)around the restore of each row
forceDestroying(Model $model), forceDestroyed(Model $model)around the force deletion of each row

mutating() and mutated() receive the entry of the request as a Lomkit\Rest\Mutations\MutationNode: operation, key, attributes, pivot, withoutDetaching, relations, type, depth and path.

use Illuminate\Database\Eloquent\Model;
use Lomkit\Rest\Fields\MutationOperation;
use Lomkit\Rest\Mutations\MutationNode;

public function mutating(MutationNode $entry, Model $model): void
{
    if ($entry->operation === MutationOperation::Create) {
        $model->setAttribute('created_by', $this->request()->user()->id);
    }
}
  • mutating() and mutated() run for each row that an entry writes, at every level: a create, an update, an entry with attributes, and an attach, detach, sync or toggle entry whose own relations change. They do not run for a target that is only linked, without attributes or relations.
  • To keep a history row for each written row, write it in these hooks: see Recording each written row.
  • performDelete(Model $model), performRestore(Model $model) and performForceDelete(Model $model) replace the write itself.
  • A delete entry of a deletable() relation in mutate goes through performDelete() and runs no destroying() or destroyed() hook.

Laravel events

After each write, the package dispatches a Laravel event, for every resource:

Event (Lomkit\Rest\Events)EndpointProperties
ResourceMutatedmutateresource, operations (the operations of the response, as arrays)
ResourceDestroyed, ResourceForceDestroyeddestroy, forceDestroyresource, model (the class of the model), keys (the keys of the rows, in the order of resources)
ResourceRestoredrestoreresource, models
ActionOperatedoperateresource, action (the class of the action), uriKey, fields (the values of the fields, by name), impacted, batch (the id of the batch, or null)
use Illuminate\Support\Facades\Event;
use Lomkit\Rest\Events\ResourceDestroyed;

Event::listen(ResourceDestroyed::class, function (ResourceDestroyed $event) {
    if ($event->resource === TicketResource::class) {
        ActivityLog::record($event->resource, 'destroyed', $event->keys);
    }
});
  • resource is the class of the resource: filter on it.
  • The event is dispatched once the write is committed, once per request: never for a write that rolls back, nor for an attempt that a deadlock runs again. The events implement ShouldDispatchAfterCommit.
  • ActionOperated is dispatched once the run ends without error. For a queued action, it is dispatched in the request, once the jobs are queued. The queued jobs dispatch no event.
  • Searches, details and metrics dispatch no event.
  • A listener cannot refuse or change the write: refuse it in the resource events.
  • A listener may implement ShouldQueue. ResourceDestroyed and ResourceForceDestroyed carry keys, since a deleted row may no longer load.

Keys out of reach

When a request names keys that the perimeterQuery() of their resource does not return, the package dispatches Lomkit\Rest\Events\KeysOutOfReach, so that a listener can record it. The request still answers its 422, The selected resources.1 is invalid.:

use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Lomkit\Rest\Events\KeysOutOfReach;

Event::listen(KeysOutOfReach::class, function (KeysOutOfReach $event) {
    Log::warning('Keys out of reach', [
        'resource' => $event->resource,
        'endpoint' => $event->endpoint->value,
        'keys' => $event->keys,
        'paths' => $event->paths,
        'user' => $event->user,
    ]);
});
PropertyValue
resourcethe class of the resource of the keys
modelthe class of its model
endpointthe Lomkit\Rest\Http\Routing\Endpoint of the request, such as Endpoint::Destroy
keysthe keys out of reach, each once, at most 50
pathsthe payload path of each refused key, such as mutate.0.relations.shelves.0.key.0, at most 50
cappedtrue when keys or paths were left out
guardthe guard of the request
userthe identifier of the user, or null for a guest
  • It covers the key of a mutate entry and of a relation entry (attach, detach, update, sync, toggle, delete), the resources of an action, and the resources of destroy, restore and forceDestroy.
  • One event per resource and per request, dispatched during validation, before the 422. The response does not change.
  • A key refused for another reason, its type, a repeat or a list over its limit, dispatches nothing.
  • A Precognition request dispatches it when it validates the keys.