Advanced Laravel Reservation Systems: Scoped Queries and Clean API Architectures

Overview

Constructing a robust reservation system requires more than just basic CRUD operations. It demands a sophisticated approach to data privacy, query efficiency, and storage management. In this walkthrough, we explore the technical implementation of an

-style office rental platform called
Ergodnc
.

The core challenge lies in managing reservations from two distinct perspectives: the guest who books the space and the host who provides it. While a single endpoint could theoretically handle both via complex conditional logic, a cleaner architectural pattern involves separating these concerns into dedicated controllers. This guide covers how to implement scoped reservation listing, handle automated image cleanup on model deletion, and utilize

's keyed implicit model binding to secure child resources.

Prerequisites

To follow this tutorial, you should have a solid grasp of the following:

  • PHP 8.x and
    Laravel
    8+ fundamentals.
  • RESTful API Design: Understanding HTTP verbs and resource-based routing.
  • Eloquent ORM: Familiarity with relationships (HasMany, BelongsTo) and query builders.
  • PHPUnit: Basic knowledge of writing functional tests for API endpoints.
  • Laravel Sanctum: Understanding token-based authentication and guard configurations.

Key Libraries & Tools

  • Laravel
    : The primary PHP framework used for building the backend logic.
  • Laravel Sanctum
    : Handles lightweight API authentication and token scoping.
  • Filesystem Abstraction: Laravel's Storage facade for managing local and cloud-based image assets.
  • Eloquent Resources: Used for transforming models into consistent JSON structures.

Automated Asset Cleanup and Storage Abstraction

A common pitfall in application development is leaving "orphan" files in storage after a database record is deleted. If a host deletes an office listing, the associated images should not linger on the server consuming space.

Cascading Deletions in Controllers

Instead of relying solely on database constraints, we manually iterate through associated images to ensure the physical files are removed from the disk before the record is purged. This is especially critical when using soft deletes; while the office record remains for historical reservation data, the assets can be safely cleared if the listing is no longer active.

# Logic for deleting office images in the OfficeController
def destroy(office):
    office.images.each(lambda image: 
        Storage.delete(image.path)
        image.delete()
    )
    office.delete()

Decoupling Storage Disks

Hardcoding a specific disk like public or s3 within your controllers limits portability. A better practice is to use the default disk configured in your environment files. By removing the explicit disk('public') calls, the application becomes environment-aware, automatically using local storage for development and S3 for production without code changes.

Scoping Resources with Keyed Implicit Binding

Security often fails when developers forget to verify that a child resource (like an image) actually belongs to the parent resource (the office) specified in the URL. Standard route model binding only ensures the IDs exist, but it doesn't check their relationship.

By using Keyed Implicit Binding, we tell Laravel to scope the second model to the first. In the route definition /offices/{office}/images/{image:id}, Laravel automatically modifies the query to Office->images()->where('id', image_id)->firstOrFail(). This removes the need for manual ownership checks inside your controller methods, returning a 404 automatically if the image belongs to a different office.

Architectural Strategy: Separating Host and Guest Concerns

When building the index method for reservations, the logic often becomes a "spaghetti" of if/else statements. You have to check if the user is the host, if they are the guest, or if they are searching for a specific date range on an office they don't own.

The Case for Dual Controllers

Instead of a single ReservationController, we implement a UserReservationController and a HostReservationController. This separation offers several advantages:

  1. Query Simplicity: The UserReservationController always applies a where('user_id', auth()->id()) constraint, while the Host version uses a whereHas relationship to check office ownership.
  2. Security by Design: There is no risk of a user accidentally viewing another person's bookings because the base query is strictly scoped to the authenticated user's ID.
  3. Performance: We avoid complex SQL OR conditions that can bypass indexes and slow down the database as the table grows.

Syntax Notes and Implementation Details

In the implementation of the reservation filters, we utilize the when() helper extensively. This allows for a fluent query building experience where filters for status, office_id, or date_range are only appended if they exist in the request.

// UserReservationController.php index method
$reservations = Reservation::query()
    ->where('user_id', auth()->id())
    ->when(request('office_id'), function ($query, $officeId) {
        return $query->where('office_id', $officeId);
    })
    ->when(request('status'), function ($query, $status) {
        return $query->where('status', $status);
    })
    ->with(['office.featuredImage'])
    ->paginate(20);

Date Range Logic

Filtering by date ranges requires careful handling of overlapping dates. To find reservations within a specific window, we check if the start_date is between the range OR if the end_date is between the range. Using a closure to group these OR conditions is vital to ensure they don't conflict with the global user_id constraint.

Practical Examples

  • Office Management: A host deletes a workspace listing. The system triggers the cleanup loop, removing four high-resolution photos from an S3 bucket and deleting the corresponding image rows, preventing database bloat.
  • Mobile Client Access: A remote worker opens their "My Bookings" tab. The app hits /api/reservations. Because of the scoped controller, the SQL query is highly optimized to return only their specific records, eager-loading the office name and featured image in a single round-trip.

Tips & Gotchas

  • Eager Loading Overkill: While it's tempting to load every relationship, focus on what the UI needs. In the reservation list, we load office.featuredImage but skip the full user profile since the user is already known (it's the logged-in user).
  • The Default Guard Trap: When working with
    Laravel Sanctum
    , ensure your auth:sanctum middleware is applied even on public-facing index routes if you intend to customize the output for logged-in users. Without it, Auth::user() will return null regardless of the token provided.
  • Soft Deletes and Relationships: If you use soft deletes on the Office model, ensure your Reservation relationships are configured to handle trashed parents if historical records need to remain visible in user dashboards.
6 min read