Installation
composer require yanselmask/reservable
php artisan migrate
reservations and reservable tables exist in your database.Basic Model Setup
Book, Room):
use Yanselmask\Reservable\Interfaces\ReservableInterface;
use Yanselmask\Reservable\Traits\Reservable;
class Book extends Model implements ReservableInterface
{
use Reservable;
}
User):
use Yanselmask\Reservable\Interfaces\CustomerInterface;
use Yanselmask\Reservable\Traits\Customer;
class User extends Authenticatable implements CustomerInterface
{
use Customer;
}
First Use Case: Reserve a Model
$book = Book::find(1);
$user = auth()->user();
$reservation = $book->reserve($user, now()->addDays(3));
// Returns a `Reserve` model instance.
Reserving a Model
// Basic reservation (auto-assigns to authenticated user if `Customer` trait is used)
$reservation = $model->reserve($user, $endAt);
// Force reservation (bypasses availability checks)
$reservation = $model->forceReserve($user, $endAt);
Checking Availability
// Check if a model is available for a given period
$isAvailable = $model->isAvailable($startAt, $endAt);
// Get overlapping reservations
$overlaps = $model->getOverlappingReservations($startAt, $endAt);
Managing Reservations
// Cancel a reservation
$reservation->cancel();
// Extend a reservation
$reservation->extend($newEndAt);
// List all reservations for a model
$model->reservations; // MorphMany relationship
Customer-Specific Reservations
// Get all reservations for a customer
$user->reservations; // MorphMany relationship
// Check if a customer has an active reservation
$hasActiveReservation = $user->hasActiveReservation($model);
Customizing Reservation Logic
Override the isAvailable() method in your model to enforce business rules:
public function isAvailable($startAt, $endAt)
{
// Custom logic (e.g., blackout dates, capacity limits)
return parent::isAvailable($startAt, $endAt) && $this->customRule();
}
Scopes for Querying Add scopes to your models for common queries:
public function scopeAvailable($query, $startAt, $endAt)
{
return $query->whereDoesntHave('reservations', function ($q) use ($startAt, $endAt) {
$q->where(function ($query) use ($startAt, $endAt) {
$query->where('start_at', '<', $endAt)
->where('end_at', '>', $startAt);
});
});
}
Events and Observers
Listen for reservation events (e.g., Reserved, Cancelled) to trigger notifications or updates:
// In EventServiceProvider
protected $listen = [
\Yanselmask\Reservable\Events\Reserved::class => [
\App\Listeners\SendReservationConfirmation::class,
],
];
API Endpoints Example controller methods:
public function reserve(Request $request, $modelId)
{
$model = Model::findOrFail($modelId);
$reservation = $model->reserve(auth()->user(), $request->end_at);
return response()->json($reservation);
}
public function checkAvailability(Request $request, $modelId)
{
$model = Model::findOrFail($modelId);
$isAvailable = $model->isAvailable($request->start_at, $request->end_at);
return response()->json(['available' => $isAvailable]);
}
Missing Traits/Interfaces
ReservableInterface or CustomerInterface will cause runtime errors.Time Zone Mismatches
$reservation = $model->reserve($user, now()->setTimezone('UTC')->addDays(3));
Overlapping Reservations
isAvailable() method or use the getOverlappingReservations() method for granular control.Database Constraints
reservations table may lack foreign key constraints by default. Add them if needed:
Schema::table('reservations', function (Blueprint $table) {
$table->foreign('reservable_id')->references('id')->on('reservables')->onDelete('cascade');
$table->foreign('customer_id')->references('id')->on('users')->onDelete('cascade');
});
Soft Deletes
Reserve model also does:
use Illuminate\Database\Eloquent\SoftDeletes;
class Reserve extends Model
{
use SoftDeletes;
}
Reservation Not Saving
reservable_id and customer_id are correctly set in the reservations table.reservable_type column matches your model's fully qualified class name (e.g., App\Models\Book).Availability Checks Failing
$overlaps = $model->getOverlappingReservations($startAt, $endAt);
\Log::info('Overlaps:', $overlaps->toArray());
Performance Issues
$model->load('reservations:start_at,end_at'); // Load only specific columns
Custom Reservation Attributes
Add fields to the reservations table via migration:
Schema::table('reservations', function (Blueprint $table) {
$table->string('status')->default('active');
$table->json('metadata');
});
Update the Reserve model to cast these fields.
Validation Rules
Extend the package's validation by overriding the reserve() method:
public function reserve($customer, $endAt)
{
if (!$this->validateReservation($endAt)) {
throw new \Exception('Reservation validation failed.');
}
return parent::reserve($customer, $endAt);
}
Custom Reserve Model
Replace the default Reserve model by binding it in the service provider:
public function register()
{
$this->app->bind(
\Yanselmask\Reservable\Models\Reserve::class,
\App\Models\CustomReserve::class
);
}
Localization
Override language strings (e.g., for validation errors) in your app/config/reservable.php:
'messages' => [
'overlap' => 'This item is already reserved for the selected time period.',
],
How can I help you explore Laravel packages today?