Mastering Laravel API Development: From Polymorphic Mapping to Sanctum Authorization

Overview: The Anatomy of a Modern Office-Sharing API

Building a robust backend requires more than just making the code work; it requires a commitment to long-term maintainability and security. In this session, we focus on the evolution of

, an
Airbnb
-like platform specifically designed for ergonomic remote workspaces. The goal isn't just to store data, but to refine how that data is presented and secured.

We address critical architectural decisions: shifting from generic polymorphic types to aliased maps, optimizing database queries for geographical distance, and implementing a strict validation layer for office creation. By the end of this walkthrough, the application will handle authenticated requests with

, enforce verified email status, and provide clean, filtered
JSON
responses that hide sensitive internal metadata. This methodical approach ensures that the API is not only functional but follows industry-standard best practices for scalability and developer experience.

Prerequisites: Essential Toolkit

To follow this tutorial, you should be comfortable with the following technologies and concepts:

  • PHP 8.x: Familiarity with modern PHP syntax, including anonymous functions and array destructuring.
  • Laravel Framework: Understanding of Routing, Controllers, Eloquent Relationships, and Migrations.
  • RESTful Principles: Knowledge of HTTP methods (POST, GET) and status codes (201 Created, 403 Forbidden).
  • Testing Fundamentals: Basic experience with
    PHPUnit
    or Laravel's testing suite for asserting database states and JSON structures.

Key Libraries & Tools

  • Laravel Sanctum
    : A lightweight authentication system for SPAs and mobile APIs. We use it here to manage token-based authorization and "abilities."
  • Eloquent ORM
    : The database mapper that allows us to interact with our tables using expressive PHP syntax.
  • API Resources
    : A transformation layer that sits between your Eloquent models and the JSON responses returned to your users.
  • Artisan Test
    : A streamlined command-line tool (driven by
    Nuno Maduro
    ) for running test suites with beautiful output.

Refined Relationships with Custom Polymorphic Mapping

By default,

stores the fully qualified class name (e.g., App\Models\Office) in the morph_type column of your database. This is a mess for long-term maintenance. If you ever rename your model or move it to a different namespace, your database links break.

We fix this in the AppServiceProvider using Relation::enforceMorphMap. By aliasing App\Models\Office to simply office, we keep the database clean and decouple the data from the physical code structure.

// AppServiceProvider.php
public function boot()
{
    Relation::enforceMorphMap([
        'office' => \App\Models\Office::class,
        'user' => \App\Models\User::class,
    ]);
}

Enforcing the map is a best practice. If you forget to add a new model to this list, Laravel will throw an exception during development, preventing silent failures in production. This ensures your polymorphic relationships (like images or tags attached to an office) remain consistent and readable.

Advanced API Resource Configuration

Returning a model directly from a controller is risky. It often leaks sensitive columns like email_verified_at or internal timestamps. We utilize

to create a custom view of our data.

In the OfficeResource, we want to hide internal IDs and timestamps while ensuring that nested relationships—like the office owner or tags—use their own refined resources. This creates a recursive cleaning process for our JSON output.

// OfficeResource.php
public function toArray($request)
{
    return array_merge(array_diff_key($this->resource->toArray(), array_flip([
        'user_id', 'created_at', 'updated_at', 'deleted_at'
    ])), [
        'user' => UserResource::make($this->user),
        'images' => ImageResource::collection($this->images),
        'tags' => TagResource::collection($this->tags),
    ]);
}

Using array_diff_key allows us to exclude specific internal attributes while maintaining the rest of the dynamic model data. This ensures that the user only sees what they need to see, reducing payload size and improving security.

Securing the Create Office Endpoint

Creating an office is a high-privilege action. We protect the route using a combination of

guards and the verified middleware. This ensures the user is who they say they are and that they have confirmed their email address.

Inside the OfficeController@create, we perform strict validation. One key decision here is the use of the validator helper instead of

. While Form Requests are popular, placing the validation logic directly in the method (or a nearby dedicated validator) can improve code legibility by making the data requirements explicit to anyone reading the controller.

// OfficeController.php
public function create(Request $request)
{
    $attributes = validator($request->all(), [
        'title' => ['required', 'string'],
        'description' => ['required', 'string'],
        'lat' => ['required', 'numeric'],
        'lng' => ['required', 'numeric'],
        'address_line1' => ['required', 'string'],
        'price_per_day' => ['required', 'integer', 'min:100'],
        'tags' => ['array'],
        'tags.*' => ['integer', 'exists:tags,id'],
    ])->validate();

    // Authorization check for Sanctum tokens
    if ($request->user()->tokenCan('office:create')) {
        // proceed with creation
    }

    $office = $request->user()->offices()->create(Arr::except($attributes, ['tags']));
    $office->tags()->sync($attributes['tags'] ?? []);

    return OfficeResource::make($office);
}

We use $request->user()->offices()->create() to automatically associate the new office with the authenticated user. This pattern is safer than manually passing a user_id, as it relies on the underlying Eloquent relationship to handle the foreign key assignment.

Syntax Notes: Practical Patterns

  • Order By Raw: When sorting by geographical distance, we don't always want to expose the raw distance calculation in the JSON response. By using orderByRaw, we can sort the results in
    SQL
    without including the computed distance column in the final object array.
  • Token Abilities: Sanctum's tokenCan method is essential for granular API control. It allows us to distinguish between a user logged in through a full-access session and an API token that might only have permission to "read" but not "create."
  • Syncing Relationships: The sync() method on a Many-to-Many relationship (like tags) is a lifesaver. It automatically handles adding new tags and removing old ones in a single operation, keeping the pivot table perfectly aligned with the request data.

Tips & Gotchas: Lessons from the Field

  1. Trust the Framework: Don't waste time writing tests that verify if required validation works or if the auth middleware blocks guests.
    Laravel
    is already heavily tested. Focus your tests on your custom business logic, such as whether an office is correctly assigned to the user who created it.
  2. Sanctum Transient Tokens: A common point of confusion is why tokenCan returns true even when no token is present. This happens because
    Laravel Sanctum
    treats first-party session-authenticated requests as having all abilities. If you need to test specific scope failures, you must explicitly generate a limited token in your test.
  3. Validation Inconsistency: Be careful when asserting JSON paths in tests. Some Laravel test methods expect the path first, others expect the value first. Always check the method signature to avoid "false pass" scenarios where your test isn't actually asserting anything.
  4. Polymorphic Errors: If you encounter a "No morph map defined" error, it usually means you've strictly enforced the map but missed an entry. Always add your User and Token models to the map if you are using packages like Sanctum that rely on polymorphic relations.
7 min read