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:
- Ticket & seat reservations
- E-commerce inventory reservation while payment processes
- Hotel room and appointment slot bookings
- Limited edition product release drops
- Cloud resource or sandbox instance provisioning
- Equipment rentals
- Time-bounded coupon or discount reservation
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:
- Seat reservations
- Hotel bookings
- Inventory holds
- Appointment scheduling
- Checkout reservations
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 |
- Stateful reservation lifecycle: Strictly enforce valid reservation lifecycles (
held➔confirmedorcancelled), preventing confirmed bookings from expiring or expiring reservations from confirming. - Atomic concurrency protection: Prevent double-booking race conditions during high-traffic ticket or inventory drops.
- Expressive API: Chain expressive calls like
Reservation::for('room_101')->using('database')->heldBy($user)->forMinutes(30)->hold(). - Automatic expiration: Schedule holds to expire at precise times or minute durations.
- Eloquent integration: Attach the
HasReservationstrait to any resource or user model for scoped hold management. - Middleware: Protect booking and checkout endpoints automatically using
reserve:resource_key,minuteswith automatic HTTP429enforcement. - Storage backends: Switch seamlessly between high-performance
cachestores (Redis, Memcached, Array) and persistentdatabasestorage with automatic state tracking. - DTOs: Work safely with strict
ReservationInfoData Transfer Objects returning precision status metadata (status,holder,expiresAt,isExpired()). - Extensibility: Register custom storage drivers on the fly with closure-based creators via
Reservation::extend(). - Prunable storage: Built-in
Prunabletrait integration ensures historicalcancelledandexpiredreservation records never clutter your database.
Ready to get started? Install the package with Composer:
composer require zaber-dev/laravel-reservationPublish 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 migrateThe 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.";
}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:
- Before executing your controller,
ReserveResourcechecks if the resource is currently held by someone else (HTTP 429if unavailable). - If available, it places a temporary hold (
held) for the duration. - When your controller completes successfully (
2xxor3xx), the reservation remains active until it is confirmed, cancelled, or expires. If the controller fails (4xxvalidation error or5xxserver error), the temporary hold is automatically cancelled (cancel()) so the resource returns immediately to the available pool.
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']);
});
}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 whenhold()succeeds ($key,$holder,$expiresAt,$info).ZaberDev\Reservation\Events\ReservationConfirmed: Dispatched whenconfirm()transitions a hold to confirmed ($key,$info).ZaberDev\Reservation\Events\ReservationCancelled: Dispatched whencancel()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.
Laravel Reservation includes built-in AI support and architectural skills engineered for Laravel Boost.
When using AI coding assistants (such as Cursor, Claude Code, or GitHub Copilot connected via the Boost MCP server), your AI agent can automatically load specialized design patterns and exact API rules for implementing stateful resource reservations, hold/confirm lifecycles, and inventory holds with our package.
When Laravel Boost (laravel/boost) is installed in your application, our package AI skill is automatically discovered and published during package installation and updates (php artisan boost:install or php artisan boost:update).
If you install laravel-reservation into an existing Boost-enabled project, our service provider also automatically synchronizes the skill directly into your .ai/skills/laravel-reservation directory on boot with zero configuration needed.
If you prefer to install or update the AI skill manually, you can use any of the following commands:
# Using Laravel Boost
php artisan boost:add-skill zaber-dev/laravel-reservation
# Using Vendor Publish
php artisan vendor:publish --tag=reservations-skillBy enabling our skill, your AI assistant will strictly follow package conventions, including:
- Modeling full booking lifecycles (
hold()->confirm()/cancel()) rather than treating reservations as short-lived mutexes. - Applying
use ZaberDev\Reservation\HasReservations;directly to Eloquent models ($seat->reserve()->heldBy($user)->forMinutes(15)->hold()). - Selecting the proper storage driver (
cachefor fast volatile holds vsdatabasefor auditability, persistence, and reporting). - Enforcing route middleware (
middleware('reservation:seat_{id},15')) to guard booking actions against double-holds. - Handling
ReservationConflictException(HTTP 409) idiomatically when a requested resource is already held or confirmed.
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 testThank 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.
Built with ❤️ as part of the ZaberDev Laravel Ecosystem.
