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

Hold state.

Read active, released, and expired state from hold timestamps.

A hold is active, released, or expired. Nothing stores that. Two nullable timestamps decide it on every read, and a third is bookkeeping that decides nothing.

The truth table

active   = released_at IS NULL AND (expires_at IS NULL OR expires_at > now)
released = released_at IS NOT NULL
expired  = released_at IS NULL AND expires_at <= now

Written out per combination:

expires_atreleased_atStatus
futurenullActive
nullnullActive
futuresetReleased
nullsetReleased
pastsetReleased
pastnullExpired

expired_at appears in no rule. It records when hold:expire announced the hold and nothing else.

The status accessor

use ByRcsc\LaravelHold\Enums\HoldStatus;

$hold->status;   // HoldStatus::Active | Released | Expired

HoldStatus is a string-backed enum with the values active, released, and expired.

The accessor recomputes on every read. Object caching is switched off deliberately: a cached Active would survive past the expiry instant, and lazy expiry forbids exactly that.

The helper methods

$hold->isActive();       // bool
$hold->isReleased();     // bool
$hold->isExpired();      // bool
$hold->isIndefinite();   // bool

The first three are the status compared against each case, so exactly one of them is true at any moment.

isIndefinite() is separate. It reports expires_at === null and says nothing about release, so a released indefinite hold reports true.

The query scopes

use ByRcsc\LaravelHold\Models\Hold;

Hold::query()->active()->get();
Hold::query()->released()->get();
Hold::query()->expired()->get();

Each scope selects exactly the rows whose accessor returns that case, so a scope and the accessor can never disagree about the same row.

The scopes compose with anything else:

Hold::query()
    ->active()
    ->whereMorphedTo('holder', $user)
    ->latest('id')
    ->get();

They are also layered inside the traits: $seat->activeHolds is the holdable's holds relation with active() applied, so the truth table keeps one definition.

The columns

ColumnTypeMeaning
idbig integerPrimary key
holdable_typestringResource class
holdable_idkey typeResource key
holder_typestringHolder class
holder_idkey typeHolder key
released_by_typestring, nullReleaser class, if named
released_by_idkey type, nullReleaser key, if named
expires_attimestamp, nullNull means indefinite
released_attimestamp, nullSet by release()
expired_attimestamp, nullSet by hold:expire, decides nothing
metadatajson, nullCast to array
created_attimestampAcquisition time
updated_attimestamp

Full column shaping and indexes are in database schema.

Metadata

metadata casts to an array in both directions. The column is nullable, so read it as ?array.

$hold->metadata;              // ['reason' => 'checkout']
$hold->metadata['reason'];    // 'checkout'

Acquisition writes an empty array when you pass none. A release merges its metadata over what is there, one level deep.

The morph relations

$hold->holdable;     // the resource
$hold->holder;       // whoever holds it
$hold->releasedBy;   // whoever released it, or null

All three are MorphTo. releasedBy stays null until a release names someone, and survives refresh() and load().

Eager-load them like any other relation:

Hold::query()->active()->with(['holdable', 'holder'])->get();

Timestamps are the API

Because nothing stores a status, there is no scheduler run to be stale relative to and no cache to invalidate. Two consequences are worth planning for.

Every status read is a clock read. The same hold can report Active and then Expired inside one request, which is correct.

A status cannot be queried as a column. Filter with the scopes rather than writing where('status', 'active'), which matches nothing because there is no such column.

What to read next

  • Expiry for the boundary condition the truth table turns on.
  • Database schema for the columns, indexes, and key types.
  • Events and listeners for reacting to the transitions between these states.
PreviousExpiryNextEvents and listeners
View source

On this page

  1. The truth table
  2. The status accessor
  3. The helper methods
  4. The query scopes
  5. The columns
  6. Metadata
  7. The morph relations
  8. Timestamps are the API
  9. What to read next