zaber-dev/laravel-reservation
Application-level resource reservation for Laravel: temporarily hold seats, rooms, or inventory, then confirm, cancel, or let them expire to prevent double-bookings. Uses cache (Redis, Memcached) or database storage.
Supports: Laravel 11, 12 & 13+ • PHP 8.2+ • Redis • Memcached • Database
Application-level resource reservation for Laravel. Temporarily hold resources, manage reservation lifecycles, and prevent double-bookings with cache or database storage.
Manage reservations with a clean, expressive API using cache or persistent database storage, attach them directly to Eloquent models, protect routes with declarative middleware, and extend the package with custom storage backends.
Unlike
Cache::lock(), which provides short-lived mutual exclusion, Laravel Reservation manages complete reservation lifecycles—from temporary holds to confirmation, cancellation, and expiration.
Available
│
▼
Held (15 min)
│
┌──┴─────────────┐
▼ ▼
Confirmed Cancelled
│
▼
Completed
or after timeout
Held ─────► Expired
Reservation::for('seat_A12')
->heldBy($user)
->forMinutes(15)
->hold();
// Process payment...
Reservation::for('seat_A12')->confirm();
Laravel Reservation is ideal for:
Cache::lock()?Laravel's Cache::lock() is excellent for preventing concurrent execution within a request.
Laravel Reservation solves a different problem. It models resource availability, not just concurrency.
It manages stateful resource reservations that survive beyond a single request and follow a complete booking lifecycle.
Typical examples include:
where a resource may remain temporarily held for several minutes before eventually being confirmed or released.
| Feature | Cache::lock() | Laravel Reservation |
|---|---|---|
| Primary Purpose | Low-level atomic mutex | Stateful resource holds & booking lifecycles |
Fluent Builder API (Reservation::for()->hold()) |
❌ Manual | ✅ Expressive & Clean |
First-Class Eloquent Integration ($seat->reserve()) |
❌ | ✅ Native (HasReservations) |
Driver-Based Architecture (cache & database) |
❌ Cache Only | ✅ Both Supported |
Multi-Step Lifecycle Tracking (held/confirmed) |
❌ | ✅ Built-in (held, confirmed, cancelled) |
| Success-Only / Auto-Cancel Route Middleware | ❌ | ✅ Built-in (ReserveResource) |
Immutable DTOs (ReservationInfo) |
❌ | ✅ Strict (CarbonImmutable) |
Automatic Database Pruning (model:prune) |
N/A | ✅ Built-in (Prunable) |
| Polymorphic Target Scoping | ❌ Manual Keys | ✅ Automatic Key Mapping |
Custom Driver Extensibility (Reservation::extend()) |
❌ | ✅ Closure / Container |
Event Dispatching (ReservationHeld / Confirmed) |
❌ | ✅ Configurable Events |
held ➔ confirmed or cancelled), preventing confirmed bookings from expiring or expiring reservations from confirming.Reservation::for('room_101')->using('database')->heldBy($user)->forMinutes(30)->hold().HasReservations trait to any resource or user model for scoped hold management.reserve:resource_key,minutes with automatic HTTP 429 enforcement.cache stores (Redis, Memcached, Array) and persistent database storage with automatic state tracking.ReservationInfo Data Transfer Objects returning precision status metadata (status, holder, expiresAt, isExpired()).Reservation::extend().Prunable trait integration ensures historical cancelled and expired reservation records never clutter your database.Ready to get started? Install the package with Composer:
composer require zaber-dev/laravel-reservation
Publish the configuration and database migrations:
php artisan vendor:publish --provider="ZaberDev\Reservation\ReservationServiceProvider"
Run migrations if you intend to use the database driver:
php artisan migrate
The configuration file config/reservations.php allows you to define your default storage driver, driver parameters, and event dispatching behaviors:
return [
/*
|--------------------------------------------------------------------------
| Default Storage Backend
|--------------------------------------------------------------------------
|
| Supported drivers: "cache", "database"
|
*/
'default' => env('RESERVATION_DRIVER', 'cache'),
'drivers' => [
'cache' => [
'driver' => 'cache',
'store' => env('RESERVATION_CACHE_STORE', null),
'prefix' => 'reservations:',
],
'database' => [
'driver' => 'database',
'table' => 'reservations',
],
],
'events' => [
'dispatch' => true,
],
];
The Reservation facade provides an expressive builder interface for holding, confirming, cancelling, and inspecting reservations.
use ZaberDev\Reservation\Facades\Reservation;
// 1. Place a temporary hold
$builder = Reservation::for('seat_A12')
->heldBy($user)
->forMinutes(15);
if ($builder->hold()) {
// Hold placed successfully, present payment form...
} else {
// Resource is currently held or already confirmed by another customer
}
// 2. Confirm the reservation once payment completes
if (Reservation::for('seat_A12')->confirm()) {
// Reservation is now permanently confirmed!
}
If the customer aborts checkout or payment fails:
Reservation::for('seat_A12')->cancel();
$info = Reservation::for('seat_A12')->info(); // ReservationInfo DTO
if ($info->isHeld()) {
echo "Resource is reserved by " . $info->holder . " until " . $info->expiresAt->toDateTimeString();
} elseif ($info->isConfirmed()) {
echo "Resource is permanently booked.";
} elseif ($info->isExpired() || $info->isCancelled()) {
echo "Resource is available.";
}
HasReservations)Add the HasReservations trait to any resource Eloquent model (such as Seat, Room, or InventoryItem) to manage reservations directly off the entity:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use ZaberDev\Reservation\HasReservations;
class Seat extends Model
{
use HasReservations;
}
You can now interact directly with your model instance:
$seat = Seat::find(1);
// Reserve this seat for the user for 20 minutes
if ($seat->reserve()->heldBy($user)->forMinutes(20)->hold()) {
return response()->json(['message' => 'Seat held!']);
}
// Confirm booking
$seat->reserve()->confirm();
// Cancel hold
$seat->reserve()->cancel();
When using the database driver, HasReservations also exposes a reservations() polymorphic relationship, allowing direct querying and bulk management:
// Get all database reservation records for this seat
$history = $seat->reservations()->orderBy('created_at', 'desc')->get();
Protect booking endpoints declaratively without writing boilerplate checks in your controllers using the ReserveResource middleware:
use Illuminate\Support\Facades\Route;
// Enforce that "seat_A12" (or dynamic route parameter) is held during checkout submission
Route::post('/checkout/submit', [CheckoutController::class, 'submit'])
->middleware('reserve:seat_id,15');
How the Middleware Works:
ReserveResource checks if the resource is currently held by someone else (HTTP 429 if unavailable).held) for the duration.2xx or 3xx), the reservation remains active until it is confirmed, cancelled, or expires. If the controller fails (4xx validation error or 5xx server error), the temporary hold is automatically cancelled (cancel()) so the resource returns immediately to the available pool.using & driver)By default, the package uses the storage backend configured in config/reservations.php. You can switch storage backends on the fly per request or action:
// Store transient shopping cart holds in fast cache/Redis
Reservation::for('cart_item_99')->using('cache')->heldBy($user)->forMinutes(30)->hold();
// Store high-value hotel booking holds inside SQL database with row locks
Reservation::for('penthouse_suite')->using('database')->heldBy($user)->forMinutes(60)->hold();
You can extend the ReservationManager with your own storage backends (e.g., DynamoDB, MongoDB) in your AppServiceProvider:
use ZaberDev\Reservation\Contracts\ReservationDriverContract;
use ZaberDev\Reservation\Facades\Reservation;
public function boot(): void
{
Reservation::extend('dynamodb', function ($app) {
return new MyDynamoDbReservationDriver($app['config']['reservations.drivers.dynamodb']);
});
}
Prunable)When using the database driver, expired or cancelled records (status in ('cancelled', 'expired')) are automatically marked for pruning via Laravel's Prunable trait on the ZaberDev\Reservation\Models\ReservationModel model.
To clean up old records automatically, schedule Laravel's model:prune command in your console.php or Kernel.php:
use Illuminate\Support\Facades\Schedule;
use ZaberDev\Reservation\Models\ReservationModel;
Schedule::command('model:prune', ['--model' => ReservationModel::class])->daily();
Whenever a reservation changes state, the package dispatches strongly typed events if enabled (reservations.events.dispatch = true):
ZaberDev\Reservation\Events\ReservationHeld: Dispatched when hold() succeeds ($key, $holder, $expiresAt, $info).ZaberDev\Reservation\Events\ReservationConfirmed: Dispatched when confirm() transitions a hold to confirmed ($key, $info).ZaberDev\Reservation\Events\ReservationCancelled: Dispatched when cancel() releases a hold ($key, $info).ZaberDev\Reservation\Events\ReservationExpired: Dispatched when an expired hold is detected or reaped ($key).You can listen to these in your EventServiceProvider for inventory metrics, webhook notifications, or customer emails.
This package is part of the ZaberDev Laravel Ecosystem (Laravel Productivity Toolkit) — a cohesive suite of high-level application primitives engineered for concurrency, state management, and resource allocation.
Explore the complete directory of packages, detailed use cases, and documentation in our Ecosystem Index Hub.
Run the comprehensive PHPUnit test suite locally:
composer test
Thank you for considering contributing! Please ensure any pull requests include thorough PHPUnit tests covering unit, feature, and driver integration scenarios.
The MIT License (MIT). Please see LICENSE.md for more information.
How can I help you explore Laravel packages today?