Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Reservation Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require zaber-dev/laravel-reservation
php artisan vendor:publish --tag="reservation-config"
php artisan vendor:publish --tag="reservation-migrations"   # only if using the database store
php artisan migrate

Where to look first

  1. config/reservation.php — this is the decision point for the whole package. It selects the storage driver (cache vs database), the cache store/key prefix, and the default TTL. Read this file before writing any code; most surprises later trace back to it.
  2. The published migration — confirms the table name and columns you'll query directly when building admin/reporting screens.
  3. The package's own test suite on GitHub — for a package at this size, the tests are the most reliable documentation of edge-case behaviour (what happens on expiry, on double-reserve, on cancel).

First use case: a 10-minute hold

The canonical day-one scenario is "hold this slot while the user finishes checkout." Roughly:

use ZaberDev\Reservation\Facades\Reservation; // confirm exact namespace in README

$hold = Reservation::make('seat:'.$seat->id)
    ->for($user)              // optional owner
    ->for($seat)              // optional Eloquent subject
    ->expiresIn(10)           // minutes
    ->create();

if (! $hold->exists()) {      // or ->failed() — check the fluent builder's terminal methods
    return back()->withErrors('That seat was just taken.');
}

Method names are the one thing you should verify against the README/tests before committing — the fluent builder is the package's main surface and it has evolved across releases. Everything else (driver choice, expiry, Eloquent attachment, middleware) is stable in concept.

Implementation Patterns

Pick the store per use case, not per app

The two drivers serve genuinely different needs:

  • Cache (Redis/Memcached) — high-traffic, short-lived holds where a lost reservation is an annoyance, not a data-loss event. Seats, flash sales, API rate-style holds.
  • Database — anything with money, audit, or customer support attached. A flushed Redis or an eviction under memory pressure silently drops holds; the database store doesn't.

A common production shape is a cache hold for the "in-flight" window and a database row written on confirmation, so the durable record and the hot path are decoupled.

Treat the reservation as a state machine, not a boolean

Model your own domain states explicitly (held → confirmed → cancelled/expired) and drive transitions from your application, using the package for the "is this still exclusively mine?" question. On a payment webhook, confirm the reservation inside a transaction and only then write your Booking record. On cancellation, release explicitly rather than waiting for TTL — it makes capacity available immediately and keeps cache and DB consistent.

Guard the double-booking race properly

Expiry is not mutual exclusion. Two concurrent requests can both observe "free" before either writes. For anything with real contention:

Cache::lock("reservation:seat:{$seat->id}", 5)->block(3, function () use ($seat, $user) {
    // check-then-create here
});

…or a unique index on (resource_key, active) in your own table. The package reduces the problem; it doesn't remove the need for an atomic primitive at the boundary.

Eloquent integration is for display, not enforcement

Attaching reservations to models (->for($model)) is excellent for "show the current hold on this listing" and for relationship-scoped queries. Use it for reads and UI. Keep the authoritative availability check in the reservation call itself, not in a ->reservations()->count() comparison, which is racy and drifts once expired rows exist.

Middleware for route-level protection

Route middleware is the right tool for coarse guards — "only the holder of this reservation may hit the checkout step." Order it after auth/session so the owner is resolvable, and return a redirect rather than a 403 for expired holds; users hitting a stale tab should land somewhere useful.

Housekeeping is your job

Schedule a prune for expired database rows (daily is usually enough) and make sure your availability queries exclude expired records. Without pruning, the table grows monotonically and where('expires_at', '>', now()) scans degrade.

Testing

Carbon::setTestNow() plus the array cache driver gives you fast, deterministic expiry tests. For the database store, use RefreshDatabase and assert on the table directly — it's the fastest way to confirm the package does what you assume about release-on-expiry.

Gotchas and Tips

Cache eviction is silent data loss. Under Redis maxmemory-policy allkeys-lru, holds evaporate without any exception. If a lost hold means a lost sale or a support ticket, use the database store — or at minimum monitor evictions and treat holds as advisory.

Config caching bites during driver switches. After changing the store in config/reservation.php, run php artisan config:clear locally and config:cache in deploy. A stale cached config pointing at the old driver is a classic "why is this writing to the wrong place" bug.

Key composition needs a namespace. If you build keys by hand ("seat:{$id}"), collisions across resource types are easy. Prefix consistently (hold:flight:{id}) and keep the package's own key prefix in mind — don't assume the raw string you pass is the literal cache key.

Clock and timezone consistency. Expiry comparisons depend on now(). Make sure APP_TIMEZONE, your DB session timezone, and queue workers agree; a worker in UTC expiring rows written by a web node in a local timezone produces off-by-hours behaviour that looks like a package bug and isn't.

Never create holds on GET. Reservation creation is a write. Put it behind POST with an idempotency key so a double-submit or retry doesn't consume two holds or fail the second one confusingly.

Idempotency on webhooks. Payment callbacks retry. Confirming an already-confirmed reservation, or confirming one that expired between the provider's call and yours, needs an explicit branch — decide whether late payment confirms or refunds, and encode it.

Watch middleware ordering. Reservation middleware that resolves an owner before auth has run will see null and behave inconsistently between requests; the failure is intermittent and confusing.

Small ecosystem, so pin the version. With a handful of stars and no dependents, there's no crowd to catch regressions. Pin to a specific minor in composer.json, and after any upgrade re-run your own expiry and double-booking tests — those are the paths most likely to shift.

Debugging tactic. When a hold "isn't working," bypass the package once: read the raw cache key (or the database row) directly. That immediately tells you whether the problem is creation, lookup, or expiry, and it's faster than reasoning about the fluent builder.

Extension point. If you outgrow the bundled stores, the storage layer is contract-based — a custom driver (e.g., DynamoDB, or a sharded table) is the intended escape hatch. Implement the contract and point the config at your class rather than forking the package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo