›
byrcsc/laravel-hold · 1.x
React after a hold is acquired, extended, released, or marked expired.
The package ships no notifications. Holders are polymorphic and may not be notifiable, so it fires events and lets your listener decide who to tell.
| Event | Fires |
|---|---|
HoldAcquired | After a hold is acquired |
HoldExtended | After extend() succeeds |
HoldReleased | After release() stamps a hold |
HoldExpired | From hold:expire, once per expired hold |
All four live in ByRcsc\LaravelHold\Events, and all four expose the hold as a
public readonly property:
final class HoldAcquired
{
public function __construct(
public readonly Hold $hold,
) {}
}They carry nothing else. From the hold you can reach everything: $hold-> holdable, $hold->holder, $hold->releasedBy, $hold->metadata,
$hold->status.
namespace App\Listeners;
use ByRcsc\LaravelHold\Events\HoldExpired;
use Illuminate\Contracts\Queue\ShouldQueue;
final class NotifyHolderOfExpiry implements ShouldQueue
{
public function handle(HoldExpired $event): void
{
$holder = $event->hold->holder;
if ($holder instanceof User) {
$holder->notify(new HoldExpiredNotification($event->hold));
}
}
}Branch on the holder type. That instanceof is not defensive clutter, it
is the pattern: a User has an address to notify, a Cart does not, and the
package cannot tell them apart on your behalf.
Laravel discovers listeners by their type hint. Register explicitly if your application does not use discovery:
use Illuminate\Support\Facades\Event;
Event::listen(HoldExpired::class, NotifyHolderOfExpiry::class);All four are dispatched with afterCommit(). A listener never sees an
event for a row another connection cannot read yet.
Two consequences follow when you acquire inside your own transaction:
DB::transaction(function () use ($seat, $user) {
$hold = $seat->acquireHoldOrFail($user);
// HoldAcquired has not fired yet. It fires when this closure commits.
throw new PaymentFailed; // and now it never fires at all
});A rollback drops the event along with the row. Standalone, outside any transaction of yours, the event fires as soon as the package's own transaction commits, which is immediately.
HoldAcquired fires once per successful acquisition, and never on a refusal.
acquireHold() returning null and acquireHoldOrFail() throwing both fire
nothing.
HoldExtended fires only when the extension succeeds. All three refusal cases
stay silent and leave the row untouched.
HoldReleased fires once per hold. The second release is a no-op, and a no-op
announces nothing.
HoldExpired fires once per hold, ever. The command claims each row with a
conditional update before announcing it, so overlapping runs cannot both fire
for the same hold. See
scheduling expiry.
Pruning fires nothing. Deleting a released or expired hold is maintenance, not lifecycle, and whatever was going to be announced about that hold was announced when it happened.
Lazy expiry fires nothing. The moment a slot comes back is a clock reading,
not a write, so there is nothing to hook. HoldExpired comes from the command,
which is why it is best effort while availability is exact.
The package dispatches synchronously inside the code doing the work. Implement
ShouldQueue on the listener when the work should not block the request or the
command.
A queued listener receives a serialized hold and re-resolves it from the database. By the time it runs, the hold may have been released or its slot re-acquired. Read the state you need rather than assuming the state at dispatch time.