Extending
Events
Listen to Lathe's Laravel events for records, relations, actions and requests, and run code once per boot or on every panel request.
Lathe fires ordinary Laravel events when a record is created, changed or deleted, when a relation changes, when an action runs and when a panel request starts. You listen to them the way you listen to any event. Next to the events, the Lathe class has a few callbacks that run code once when the app boots or on every panel request.
Note: The events are plain classes with public properties. You listen with
Event::listen()or a listener class. The Resource events fire for changes made through the panel, not for changes your own code makes to a model.
Resource events
Lathe fires an event after each write to a record. Each carries the Eloquent model and the Resource class that owns it, as $model and $resource:
| Event | Fires when | Properties |
|---|---|---|
Lathe\Events\ResourceCreated |
A record is created from the create form | Model $model, string $resource |
Lathe\Events\ResourceUpdated |
A record is updated, from the edit form or an inline edit | Model $model, string $resource, array $original, array $changes |
Lathe\Events\ResourceDeleted |
A record is deleted | Model $model, string $resource |
Lathe\Events\ResourceForceDeleted |
A record is deleted for good | Model $model, string $resource |
Lathe\Events\ResourceRestored |
A soft-deleted record is restored | Model $model, string $resource |
$resource is the Resource class, such as App\Lathe\Resources\Post, not the model class. On ResourceUpdated, $original holds the changed attributes as they were and $changes as they are now. A delete, force delete or restore fires its event for each record, also when many records are chosen at once.
These events fire inside the database transaction of the write. A listener that throws rolls the write back. A listener that calls out to another system should wait for the commit. Make it a queued listener with public bool $afterCommit = true;.
Events are separate from a Resource's own hooks such as afterCreate(). Use an event for a side effect that spans Resources, such as clearing a cache, sending a webhook or updating a search index. Use a Resource hook when the logic belongs to that one Resource.
Relation events
Attaching and detaching records on a many-to-many relation fire these events, also inside the transaction:
| Event | Fires when | Properties |
|---|---|---|
Lathe\Events\ResourceAttached |
Records are attached to a parent | Model $parent, string $resource, string $relationship, array $ids, array $pivot |
Lathe\Events\ResourceDetached |
Records are detached | Model $parent, string $resource, string $relationship, array $ids, array $rows |
Lathe\Events\ResourceAttachmentUpdated |
The pivot values of an attached record change | Model $parent, string $resource, string $relationship, array $ids, array $original, array $changes |
Here $resource is the parent's Resource class and $ids are the related keys.
Action events
| Event | Fires when | Properties |
|---|---|---|
Lathe\Events\ActionExecuted |
An action has run on its models | object|string $action, Collection $models |
An action that runs at once fires the event once. A queued action fires it once for each chunk. $models is the Illuminate\Support\Collection of models the action ran on. For a history you can query, use the audit log and not this event.
The serving event
| Event | Fires when | Properties |
|---|---|---|
Lathe\Events\ServingLathe |
A panel request starts | Request $request |
Lathe fires it once at the start of every panel request, before the response is built. It never fires for your own routes. The Lathe::serving() callback below listens for it.
Impersonation events
Lathe\Events\ImpersonationStarted and Lathe\Events\ImpersonationStopped fire when an administrator starts or stops acting as another user. See Impersonation.
Listen to events
A listener class with a handle() method receives the event:
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Http;
use Lathe\Events\ResourceCreated;
class SyncSearchIndex implements ShouldQueue
{
public bool $afterCommit = true;
public function handle(ResourceCreated $event): void
{
Http::post('https://search.example.com/index', [
'type' => class_basename($event->model),
'id' => $event->model->getKey(),
'resource' => $event->resource,
]);
}
}
Laravel finds listeners in app/Listeners by the type of the handle() argument. Or register one by hand in a service provider:
use App\Listeners\SyncSearchIndex;
use Illuminate\Support\Facades\Event;
use Lathe\Events\ResourceCreated;
public function boot(): void
{
Event::listen(ResourceCreated::class, SyncSearchIndex::class);
}
For a short piece of logic, use a closure:
Event::listen(ResourceDeleted::class, function (ResourceDeleted $event) {
logger("Deleted {$event->resource} #{$event->model->getKey()}");
});
Lifecycle callbacks
The Lathe class has callbacks for code that must run at a fixed point. Register them in boot() of app/Providers/LatheServiceProvider.php, after parent::boot().
Lathe::serving
Lathe::serving(Closure $callback) runs the callback at the start of every panel request. It receives the ServingLathe event. Use it for state that depends on the request, such as values for the browser or a locale:
use Lathe\Events\ServingLathe;
use Lathe\Lathe;
public function boot(): void
{
parent::boot();
Lathe::serving(function (ServingLathe $event) {
Lathe::provideToScript([
'timezone' => $event->request->user()?->timezone ?? config('app.timezone'),
]);
});
}
A plugin has its own serving(Request $request) hook for this. See Plugins.
Lathe::booted
Lathe::booted(Closure $callback) runs the callback once, when the app has booted, or at once if it already has. The callback takes no arguments. Use it for one-time setup that does not depend on a request:
use Illuminate\Support\Facades\Gate;
use Lathe\Lathe;
public function boot(): void
{
parent::boot();
Lathe::booted(function () {
Gate::define('view-metrics', fn ($user) => $user->isAdmin());
});
}
Note: Put anything that runs once in
booted(). A callback inserving()runs on every request, which costs time. Under Octane,booted()runs once for each worker.
The setters that define the panel, such as Lathe::auth(), Lathe::userLocale() and Lathe::footer(), must run in a service provider's boot(). Called while a request is served, they throw a LogicException, because the change would leak into the next request of a long-lived worker.
Lathe::isServing and Lathe::whenServing
These let code outside the panel, such as a model observer, react to whether a panel request is running.
Lathe::isServing(): bool tells whether a panel request is being served:
if (Lathe::isServing()) {
// A panel request is running.
}
Lathe::whenServing(Closure $callback, ?Closure $default = null): mixed runs $callback during a panel request and $default otherwise. The first receives the LatheRequest, the second the current Request. If you give no $default outside a panel request, the result is null.
use Lathe\Http\Requests\LatheRequest;
use Lathe\Lathe;
class PostObserver
{
public function saving(Post $post): void
{
$post->last_touched_by = Lathe::whenServing(
fn (LatheRequest $request) => $request->user()?->id,
);
}
}
This records the admin only when a post is saved through the panel.
Lathe::ensureBooted
Lathe::ensureBooted() runs the serving callbacks and every plugin's serving() once, outside a web request. Use it in an Artisan command or a script that needs the panel set up, for example to read the assets a plugin registers while serving. It binds no request, so the first real request is still served as usual.
What you see
Nothing changes on screen. After you listen to ResourceCreated, creating a record in the panel runs your listener, and the new record is saved only if the listener does not throw. A Lathe::serving() callback runs on every panel page and API request, and a Lathe::booted() callback runs once when the app starts.