Lifecycle hooks¶
The hook trait is per-resource sugar over the lifecycle events: implement
ResourceLifecycleHooksInterface, use ResourceLifecycleHooksTrait, and override just the
methods you need. The trait routes the dispatched events to your resource, so the logic lives
next to the fields it concerns — no listener wiring (PLAN decision 10).
Declaring hooks¶
use haddowg\JsonApiLaravel\Hook\HookContext;
use haddowg\JsonApiLaravel\Hook\ResourceLifecycleHooksInterface;
use haddowg\JsonApiLaravel\Hook\ResourceLifecycleHooksTrait;
final class PlaylistResource extends AbstractResource implements ResourceLifecycleHooksInterface
{
use ResourceLifecycleHooksTrait;
public function beforeCreate(object $entity, HookContext $context): void
{
// derive a read-only slug + stamp an externalId before the persister flushes
$title = Accessor::get($entity, 'title');
Accessor::set($entity, 'slug', \Str::slug(\is_string($title) ? $title : ''));
}
public function beforeDelete(object $entity, HookContext $context): void
{
$tracks = Accessor::get($entity, 'tracks');
if (\is_countable($tracks) && \count($tracks) > 0) {
throw new ConflictHttpException('Cannot delete a playlist that still has tracks.');
}
}
}
Both are from the example's playlists — a mutating before-create hook and a before-delete
guard.
The full hook set¶
| Hook | Signature | Return |
|---|---|---|
beforeSave |
(object $entity, bool $creating, HookContext $context) |
void |
afterSave |
(object $entity, bool $creating, HookContext $context) |
?DataResponse |
beforeCreate |
(object $entity, HookContext $context) |
void |
afterCreate |
(object $entity, HookContext $context) |
?DataResponse |
beforeUpdate |
(object $entity, object $original, HookContext $context) |
void |
afterUpdate |
(object $entity, HookContext $context) |
?DataResponse |
beforeDelete |
(object $entity, HookContext $context) |
void |
afterDelete |
(object $entity, HookContext $context) |
DataResponse\|NoContentResponse\|null |
beforeRelationshipMutate |
(object $parent, HookContext $context) |
void |
afterRelationshipMutate |
(object $parent, HookContext $context) |
?IdentifierResponse |
afterFetchOne |
(object $entity, HookContext $context) |
?DataResponse |
afterFetchCollection |
(array $items, HookContext $context) |
?DataResponse |
ResourceLifecycleHooksTrait gives every method a no-op default, so you override only what
you need.
Before vs after¶
- A before hook runs before persistence. Mutating the entity there is durable (the example
derives
slug/externalIdinbeforeCreate). Throwing aborts the operation and renders through the error pipeline — the clean way to veto a write (beforeDeletethrowing a409). - An after hook runs after the operation and, by returning a response value object, may
replace the response — reshape the rendered document, or return a
204fromafterDelete. Returningnullkeeps the default response.
HookContext¶
Every hook receives a HookContext carrying the operation's request, server name, and type —
the same context the events expose, so a hook can branch on the request (e.g. an authed-only
side effect).
Hooks vs events vs model events¶
- Hooks — logic that belongs to one resource; the ergonomic default.
- Events — cross-cutting concerns across many types (audit, metrics),
queued work, or
Event::fake()in tests. The workbench'sAuditLogSubscriberis the worked example — an audit trail plus a read-only gate from one subscriber. - Eloquent model events — persistence concerns that must fire on any write path, not just API-driven ones.