›
›
›
  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

Introduction.

Laravel Hold reserves capacity on Eloquent models until release or expiry.

A checkout may reserve the last item while a buyer pays. A booking flow may hold a hotel room or event seat for several minutes. During that time, another user must not claim the same capacity.

Laravel Hold reserves capacity on any Eloquent model. Row locks prevent two holders from taking the last slot, and an expired timestamp frees the slot without waiting for a scheduled command.

Your application keeps ownership of its UI, its users, its checkout flow, and whatever being held actually means.

RequirementSupported versions
PHP8.3, 8.4
Laravel12.x, 13.x

The package follows semantic versioning: upgrading within 1.x is safe. Source and issues live at github.com/byrcsc/laravel-hold.

The three nouns

A holdable is the resource. It uses the Holdable trait, and it owns every write, because capacity is a fact about the resource rather than about whoever wants it. A seat, a domain name, a rental unit.

A holder is whoever claims it. It uses the HasHolds trait, which is read-only. A holder is asked for nothing beyond being an Eloquent model: a user, a cart, a session record. It does not need to be authenticatable, and it does not need to be notifiable.

A hold is one claim by one holder on one holdable. It is a row. Its timestamps derive its state at read time, and no stored status column exists to drift out of date.

The rules

Every holdable has a capacity, and it defaults to 1. A capacity of 1 means the resource is exclusive: at most one active hold at a time. A capacity of N models slot-style resources such as event seats or stock units.

One hold consumes exactly one slot. There is no quantity column. Holding three seats means three holds.

A hold is active when it has not been released and its expiry, if it has one, is in the future. The timestamps are authoritative. The instant expires_at passes, the slot is free for the next acquirer, whether or not any command has run.

A hold with no expires_at is indefinite. It blocks its slot until something releases it explicitly.

Acquisition is atomic. Two acquirers racing for the last slot cannot both win, on MySQL, PostgreSQL, and SQLite.

What is included

  • Two traits: Holdable for the resource side, HasHolds for the holder side, composable on one model when it is both.
  • Capacity-aware acquisition that locks the holdable's row, counts inside a transaction, and replays on a database concurrency error.
  • Time-boxed and indefinite holds, with expiry read from the clock on every availability check.
  • Release that records who decided it and merges release context into the hold's metadata, and is a no-op the second time.
  • Extension measured from the current expiry rather than from now, refused on a hold with no live window to push.
  • Four lifecycle events, dispatched after the transaction commits.
  • Two optional commands: hold:expire to announce expiries exactly once, and hold:prune to bound history.
  • Polymorphic identity columns shaped for integer, UUID, ULID, or string keys.

What it does not do

The package draws its edges deliberately. What follows describes what it sets out to do rather than what it might do later.

  • Quantity per hold. One hold is one slot, always.
  • Waitlists and queues. Who gets a freed slot next is application policy.
  • Hold approval. A hold succeeds atomically or fails. If claims need review, that is Laravel Approval.
  • Notifications. Holders are polymorphic and may not be notifiable, so the package ships events instead.
  • A transition-log audit table. The hold row records who released it and when. A full actor-attributed history is application territory.
  • UI, pricing, and payments. The package has no opinion on what a hold costs or looks like.
  • Hiding held resources. Nothing filters a held row out of your queries. You read hold state and decide.

A first hold

use ByRcsc\LaravelHold\Concerns\Holdable;

class Seat extends Model
{
    use Holdable;
}
$hold = $seat->acquireHold($user, expiresAt: now()->addMinutes(15));

if ($hold === null) {
    // No slot was free.
}

$hold->extend(CarbonInterval::minutes(5));
$hold->release();

That is the whole core loop. Everything else is detail.

Design boundaries

Four decisions shape the rest of the package.

Timestamps are the state. status is computed on every read from released_at and expires_at, and the query scopes encode the same truth table. Nothing stores a status, so nothing can disagree with the clock between scheduler runs. See hold state.

Expiry is lazy, and the command is optional. Availability is correct with hold:expire never running. The command exists to fire an event, not to free a slot. See expiry.

The holdable owns every write. HasHolds offers no acquire-from-here and no release-all, because a holder cannot serialize a slot it does not yet occupy. Acquisition takes a row lock on the holdable. See concurrency.

Acquisition always attempts to create. acquireHold() never returns an existing hold, so the same holder can hold the same resource as many times as capacity allows. Guard double submits yourself with activeHoldFor(). See acquiring holds.

What to read next

  • Installation and setup to add the package and shape its identity columns before the first migration.
  • Quick start to take a seat from free to held to released in one sitting.
  • Capacity and slots if you are modelling anything other than an exclusive resource.
NextInstallation and setup
View source

On this page

  1. The three nouns
  2. The rules
  3. What is included
  4. What it does not do
  5. A first hold
  6. Design boundaries
  7. What to read next