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

Releasing and extending.

Release a slot or extend the expiry of an active hold.

Both operations live on the Hold model, and both return the hold so they chain. Neither opens a transaction: a hold owns the writes to its own row.

Releasing

$hold->release();

The slot is free immediately. The row is not deleted: it stays as history until hold:prune takes it.

Record who decided it, and why:

$hold->release(by: $request->user(), metadata: [
    'reason' => 'cancelled by support',
]);
public function release(?Model $by = null, array $metadata = []): self
ArgumentEffect
byAssociated through the polymorphic releasedBy relation
metadataMerged over the hold's existing metadata, one level deep

by is any model. A releaser is an actor of the same kind as a holder, and the released_by columns take the holder key type. See configuration.

What a release writes

$hold->release(by: $support, metadata: ['reason' => 'cancelled']);

$hold->status;            // HoldStatus::Released
$hold->isReleased();      // true
$hold->released_at;       // the moment of the call
$hold->releasedBy->name;  // Support
$hold->metadata;          // acquisition metadata, with 'reason' merged over

The metadata merge is one level deep and your keys win. A nested array replaces its counterpart rather than combining with it. Passing no metadata writes none, so a null metadata column stays null rather than becoming an empty array.

Releasing twice is a no-op

The second release changes nothing. The first stamp, the first releaser, and the one HoldReleased event all stand.

$hold->release(by: $support);
$hold->release(by: $someoneElse);

$hold->fresh()->releasedBy->name;   // Support

This holds across instances, not only on one object. A double-submitted cancel button is two requests holding two copies of the same row, and the guard is a single conditional update, so the database decides the winner. The loser reads the winner's values back and stays silent.

Releasing an expired hold is allowed

A hold whose expiry has passed can still be released, and it stamps normally.

$hold->status;      // HoldStatus::Expired
$hold->release();
$hold->status;      // HoldStatus::Released

The slot was free either way. The stamp records that somebody decided so, and Released wins over Expired in the status truth table because a release records a decision while expiry only records the clock passing.

Extending

use Carbon\CarbonInterval;

$hold->extend(CarbonInterval::minutes(10));
public function extend(DateInterval $by): self

The interval is added to the current expires_at, never to now. Extending early must not shorten the hold.

// Acquired at 12:00 for 15 minutes, so expires_at is 12:15.
// Extended at 12:05 by 10 minutes.
$hold->expires_at;   // 12:25, not 12:15

Extensions accumulate. Call it three times and the window grows three times.

The interval is added as given. extend() does not check that you passed a positive interval, so a negative one moves the expiry backwards.

When extension is refused

extend() throws CannotExtendHoldException in three cases. Each has its own message, so a caller reporting to a user does not have to work out which it caught.

CaseMessage says
Already releasedit was already released
Already expiredit has already expired; acquire a new hold instead
Indefinite, so no expiryit is indefinite and has no expiry to push
use ByRcsc\LaravelHold\Exceptions\CannotExtendHoldException;

try {
    $hold->extend(CarbonInterval::minutes(10));
} catch (CannotExtendHoldException $e) {
    $e->hold;          // the hold it refused
    $e->getMessage();
}

A hold that is both released and expired reports the release, because that is the decision somebody made.

An expired hold cannot be revived by extension. Its slot is free and may already belong to somebody else. Acquire a new hold and contend for the slot like every other acquirer.

The row is untouched on every refusal, and no event fires.

Events

HoldReleased fires once per hold, after the transaction commits. HoldExtended fires only on a successful extension, after the new expiry is saved, so a listener reads the extended window rather than the one it replaced.

Both are dropped if the surrounding transaction rolls back. See events and listeners.

What to read next

  • Expiry for the boundary that decides when extension is refused.
  • Hold state for the status truth table these operations move a hold through.
  • Pruning history for what eventually removes released rows.
PreviousAcquiring holdsNextExpiry
View source

On this page

  1. Releasing
  2. What a release writes
  3. Releasing twice is a no-op
  4. Releasing an expired hold is allowed
  5. Extending
  6. When extension is refused
  7. Events
  8. What to read next