›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-hold
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Holdables and holders
  • Capacity and slots
  • Acquiring holds
  • Releasing and extending
  • Expiry
  • Hold state

Operations

  • Events and listeners
  • Scheduling expiry
  • Pruning history
  • Concurrency and databases

Reference

  • Configuration
  • Console commands
  • Database schema
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Holdables and holders
  • Capacity and slots
  • Acquiring holds
  • Releasing and extending
  • Expiry
  • Hold state

Operations

  • Events and listeners
  • Scheduling expiry
  • Pruning history
  • Concurrency and databases

Reference

  • Configuration
  • Console commands
  • Database schema
  • Testing
  • Troubleshooting

byrcsc/laravel-hold · 1.x

Events and listeners.

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.

The four events

EventFires
HoldAcquiredAfter a hold is acquired
HoldExtendedAfter extend() succeeds
HoldReleasedAfter release() stamps a hold
HoldExpiredFrom 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.

Listening

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);

Events wait for the commit

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.

Each event fires once

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.

What has no event

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.

Queue your listeners, not the events

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.

What to read next

  • Scheduling expiry for the only event that needs a command to fire.
  • Acquiring holds for how the commit boundary interacts with your own transactions.
  • Testing for faking these events and asserting on them.
PreviousHold stateNextScheduling expiry
View source

On this page

  1. The four events
  2. Listening
  3. Events wait for the commit
  4. Each event fires once
  5. What has no event
  6. Queue your listeners, not the events
  7. What to read next