PHPackages                             aaronfrancis/reservable - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. aaronfrancis/reservable

ActiveLibrary[Database &amp; ORM](/categories/database)

aaronfrancis/reservable
=======================

Eloquent model reservation/locking through Laravel's cache lock system

v0.1.1(4mo ago)735MITPHPPHP ^8.2CI passing

Since Dec 28Pushed 2mo agoCompare

[ Source](https://github.com/aarondfrancis/reservable)[ Packagist](https://packagist.org/packages/aaronfrancis/reservable)[ RSS](/packages/aaronfrancis-reservable/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (8)Versions (3)Used By (0)

Reservable
==========

[](#reservable)

[![Latest Version on Packagist](https://camo.githubusercontent.com/255bb49ee2fbfafa5a0c4103e8362cac3e03355006acd172cc51ef8e5d33a61c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6161726f6e6672616e6369732f72657365727661626c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/aaronfrancis/reservable)[![Tests](https://github.com/aarondfrancis/reservable/actions/workflows/tests.yaml/badge.svg)](https://github.com/aarondfrancis/reservable/actions/workflows/tests.yaml)[![Total Downloads](https://camo.githubusercontent.com/da053d2be06ffa242d57369f5e356316159bca66be542ca3c78ac96226fb28f6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6161726f6e6672616e6369732f72657365727661626c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/aaronfrancis/reservable)[![PHP Version](https://camo.githubusercontent.com/bc1d6e64c8fcce8e3565c92021422a19304a920d9449e3f5051668c6c8ceaaa0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6161726f6e6672616e6369732f72657365727661626c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/aaronfrancis/reservable)[![License](https://camo.githubusercontent.com/0260fb469f861c3f72e68fb88f3b5acb35626e0365d43f38f535f7c946ab1a7b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6161726f6e6672616e6369732f72657365727661626c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/aaronfrancis/reservable)

Eloquent model reservation/locking through Laravel's cache lock system.

This package allows you to temporarily "reserve" Eloquent models using Laravel's atomic cache locks. This is useful when you need to ensure exclusive access to a model for a period of time, such as during background processing.

Installation
------------

[](#installation)

```
composer require aaronfrancis/reservable
```

Publish the migration:

```
php artisan vendor:publish --tag=reservable-migrations
php artisan migrate
```

Optionally publish the config:

```
php artisan vendor:publish --tag=reservable-config
```

Requirements
------------

[](#requirements)

This package requires Laravel's database cache driver. Make sure your `cache_locks` table exists (created by Laravel's default cache migration).

The published migration adds generated columns to parse reservation keys into queryable columns. This allows efficient querying of reserved/unreserved models.

**Supported databases:** PostgreSQL, MySQL/MariaDB, SQLite

Usage
-----

[](#usage)

Add the `Reservable` trait to your model:

```
use AaronFrancis\Reservable\Concerns\Reservable;

class Video extends Model
{
    use Reservable;
}
```

### Reserve a model

[](#reserve-a-model)

```
// Reserve for 60 seconds (default)
$video->reserve('processing');

// Reserve for a specific duration in seconds
$video->reserve('processing', 300); // 5 minutes

// Reserve until a specific time
$video->reserve('processing', now()->addHour());

// Reserve with CarbonInterval
$video->reserve('processing', CarbonInterval::minutes(5));
$video->reserve('processing', CarbonInterval::hours(2));
```

The `reserve()` method returns `true` if the lock was acquired, or `false` if the model is already reserved.

### Check if reserved

[](#check-if-reserved)

```
if ($video->isReserved('processing')) {
    // Model is currently reserved
}
```

### Release a reservation

[](#release-a-reservation)

```
$video->releaseReservation('processing');
```

### Blocking reserve

[](#blocking-reserve)

Wait for a lock to become available instead of failing immediately:

```
// Wait up to 10 seconds (default) for the lock
$video->blockingReserve('processing', duration: 60);

// Wait up to 30 seconds
$video->blockingReserve('processing', duration: 60, wait: 30);

// With CarbonInterval
$video->blockingReserve('processing', CarbonInterval::minutes(5), wait: 30);
```

Returns `true` if the lock was acquired, `false` if the wait time expired.

### Reserve with callback

[](#reserve-with-callback)

Automatically release the lock when your work is done:

```
$result = $video->reserveWhile('processing', 300, function ($video) {
    // Do work while holding the lock...
    return $video->transcode();
}); // Lock is automatically released
```

The lock is released even if the callback throws an exception. Returns `false` if the lock couldn't be acquired.

### Extend a reservation

[](#extend-a-reservation)

Add more time to an existing reservation without releasing it:

```
$video->reserve('processing', 60);

// Later, if you need more time:
$video->extendReservation('processing', 60); // Add 60 more seconds
```

Only reservations acquired by the current process can be extended.

Returns `true` if the reservation was extended, `false` if no active reservation exists or the current process does not own the lock.

### Query scopes

[](#query-scopes)

Find unreserved models:

```
$available = Video::unreserved('processing')->get();
```

Find reserved models:

```
$reserved = Video::reserved('processing')->get();
```

> **Note:** These scopes return point-in-time results. By the time you try to reserve a model from the results, another process may have already reserved it. Use `reserveFor` for atomic find-and-reserve operations.

Find and reserve in one query:

```
// Get unreserved models and reserve them atomically
$videos = Video::reserveFor('processing', 60)->limit(5)->get();
```

The `reserveFor` scope filters to unreserved models, then atomically reserves each one that's returned. Models that can't be reserved (race condition) are filtered out.

> **Note:** Because of race conditions, `reserveFor()->limit(5)` may return fewer than 5 results. If another process reserves a model between the query and the lock attempt, that model is excluded from the results.

### Key types

[](#key-types)

Reservation keys can be strings, enums, or objects:

```
// String key
$video->reserve('processing');

// Enum key
$video->reserve(JobType::Transcoding);

// Object key (uses class name)
$video->reserve($someService);
```

### Multiple reservation types

[](#multiple-reservation-types)

A model can have multiple different reservation types simultaneously:

```
$video->reserve('transcoding');
$video->reserve('thumbnail-generation');

$video->isReserved('transcoding'); // true
$video->isReserved('thumbnail-generation'); // true
$video->isReserved('uploading'); // false
```

How it works
------------

[](#how-it-works)

Reservable builds on Laravel's atomic cache locks, which use the `cache_locks` table to provide database-level mutual exclusion.

### Lock key format

[](#lock-key-format)

When you call `$video->reserve('processing')`, Reservable creates a cache lock with a specially formatted key:

```
reservation:{morph_class}:{model_id}:{reservation_key}

```

For example: `reservation:App\Models\Video:42:processing`

### Generated columns

[](#generated-columns)

The challenge with cache locks is that they're not inherently queryable by model. You can't efficiently ask "give me all Videos that aren't locked" because the lock table doesn't know about your models.

Reservable solves this by adding **generated columns** to the `cache_locks` table that parse the key format:

ColumnExtracted FromExample Value`is_reservation`Key contains `reservation:``true``model_type`Second segment`App\Models\Video``model_id`Third segment`42``type`Fourth segment`processing`These columns are computed automatically by the database whenever a row is inserted or updated. The exact SQL varies by database engine (PostgreSQL uses `split_part()`, MySQL uses `SUBSTRING_INDEX()`, SQLite uses `substr()`).

### Efficient queries

[](#efficient-queries)

With generated columns in place, the `reserved()` and `unreserved()` scopes become simple JOIN queries:

```
-- Find unreserved videos
SELECT * FROM videos
WHERE NOT EXISTS (
    SELECT 1 FROM cache_locks
    WHERE model_type = 'App\Models\Video'
    AND model_id = videos.id
    AND type = 'processing'
    AND expiration > UNIX_TIMESTAMP()
)
```

This is much more efficient than fetching all videos and checking each lock individually in PHP.

### Atomic reservations

[](#atomic-reservations)

The `reserve()` method uses Laravel's `Lock::get()` which performs an atomic database operation—either the lock is acquired or it isn't. There's no window where two processes can both think they have the lock.

The `reserveFor` scope combines this with query filtering: it finds unreserved models, then attempts to reserve each one, filtering out any that fail due to race conditions.

### The CacheLock model

[](#the-cachelock-model)

Reservable uses an Eloquent model (`AaronFrancis\Reservable\Models\CacheLock`) to query the `cache_locks` table. This model provides the `reservations()` relationship on your reservable models, allowing you to access active locks:

```
$video->reservations; // Collection of active CacheLock models
```

You can swap this for a custom model in the config if you need to add functionality.

Configuration
-------------

[](#configuration)

```
// config/reservable.php

return [
    // The model representing cache locks
    'model' => AaronFrancis\Reservable\Models\CacheLock::class,
];
```

Testing
-------

[](#testing)

```
composer test
```

License
-------

[](#license)

MIT

###  Health Score

39

—

LowBetter than 85% of packages

Maintenance88

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

 Bus Factor1

Top contributor holds 96.4% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

131d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/033238953a59b9223a1bde703b5e4254e63c7412195da1cb9de5af44bf53fc0a?d=identicon)[aarondfrancis](/maintainers/aarondfrancis)

---

Top Contributors

[![aarondfrancis](https://avatars.githubusercontent.com/u/881931?v=4)](https://github.com/aarondfrancis "aarondfrancis (54 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (2 commits)")

---

Tags

laraveleloquentcachelockreservation

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/aaronfrancis-reservable/health.svg)

```
[![Health](https://phpackages.com/badges/aaronfrancis-reservable/health.svg)](https://phpackages.com/packages/aaronfrancis-reservable)
```

###  Alternatives

[mongodb/laravel-mongodb

A MongoDB based Eloquent model and Query builder for Laravel

7.1k7.2M70](/packages/mongodb-laravel-mongodb)[spiritix/lada-cache

A Redis based, automated and scalable database caching layer for Laravel

591444.8k2](/packages/spiritix-lada-cache)[jerome/filterable

Streamline dynamic Eloquent query filtering with seamless API request integration and advanced caching strategies.

19226.1k](/packages/jerome-filterable)[reedware/laravel-relation-joins

Adds the ability to join on a relationship by name.

2121.2M13](/packages/reedware-laravel-relation-joins)[ymigval/laravel-model-cache

Laravel package for caching Eloquent model queries

7642.2k3](/packages/ymigval-laravel-model-cache)[morilog/infinity-cache

Infinity cache for Laravel Eloquent models and queries

3115.8k](/packages/morilog-infinity-cache)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
