Back to Blog
laravelPublished August 18, 2026 · 150 min read

Building Production-Ready APIs with Laravel 13: A Professional Guide

A complete Laravel 13 reference for designing, versioning, securing, filtering, authorizing, testing, and documenting a production-ready API.

Building Production-Ready APIs with Laravel 13: A Professional Guide

Building Production-Ready APIs with Laravel 13

This professional guide develops a Laravel API from its first JSON response into a versioned, authenticated, filterable, authorized, and documented service. The focus is not on isolated framework features, but on the decisions and complete implementations needed to create a stable API contract.

The guide is organized as a progressive technical reference. Each section explains one production concern and provides the complete contents of every application file introduced or changed at that stage. The implementation uses Laravel 13 and PHP 8.3 so all examples remain internally consistent.

Laravel 13 note: Laravel 13 requires PHP 8.3 or newer. Its slim application skeleton registers API routes and exception rendering in bootstrap/app.php; the legacy RouteServiceProvider, AuthServiceProvider, and app/Exceptions/Handler.php patterns have been replaced throughout this guide.

Requirements and initial setup

  • PHP 8.3 or newer, Composer, MySQL, and Git
  • Basic familiarity with Laravel routes, controllers, Eloquent, migrations, and validation
  • Postman or another HTTP client
composer create-project laravel/laravel:^13.0 orders-hub
cd orders-hub
php artisan install:api
php artisan migrate

Configure the orders_hub MySQL database in .env, then apply the displayed files section by section.

Laravel 13 ships with a minimal base controller. Because later sections use controller authorization helpers, enable the required framework traits from the start:

app/Http/Controllers/Controller.php

<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;

abstract class Controller
{
    use AuthorizesRequests, ValidatesRequests;
}

Guide roadmap

  1. Consistent JSON Responses and HTTP Status Codes
  2. Testing APIs with Postman
  3. Designing Resource-Oriented URLs
  4. Structuring a Versioned API
  5. Token Authentication with Laravel Sanctum
  6. Token Revocation and Secure Logout
  7. Designing Stable Response Payloads
  8. Conditional Fields and Relationships
  9. Optional Relationship Loading
  10. Reusable Query Filters
  11. Nested Resources and Relationship Filters
  12. Safe Client-Controlled Sorting
  13. Creating Resources with POST
  14. Deleting Resources with DELETE
  15. Full Resource Replacement with PUT
  16. Partial Resource Updates with PATCH
  17. Resource Authorization with Policies
  18. Access Control with Token Abilities
  19. Fine-Grained Field Permissions
  20. Customer-Owned Order Operations
  21. Secure User Management
  22. Applying the Principle of Least Privilege
  23. Consistent API Error Handling
  24. Generating API Documentation with Scribe

How to verify each checkpoint

php artisan migrate:fresh --seed
php artisan route:list --path=api
php artisan serve

Always send Accept: application/json. For protected routes, log in first and send the returned Sanctum token as Authorization: Bearer YOUR_TOKEN.

1. Consistent JSON Responses and HTTP Status Codes

We begin with a fresh Laravel application, add a simple API endpoint, and return a predictable JSON response with HTTP status 200 OK.

The implementation targets Laravel 13, PHP ^8.3, and Sanctum ^4.3.

What we will build

A GET /api/login request will return:

{
  "message": "Hello, Login!",
  "status": 200
}

The response body is JSON and the actual HTTP response status is also 200.

1. Create the project

To follow the guide from a clean application, create a Laravel 13 project and install API routing with Sanctum:

composer create-project laravel/laravel:^13.0 orders-hub
cd orders-hub
php artisan install:api

The reference project configures MySQL with a database named orders_hub. Update your .env file with credentials appropriate for your machine:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=orders_hub
DB_USERNAME=root
DB_PASSWORD=

This initial implementation does not query the database yet, so the endpoint works before migrations are needed.

2. Create the response trait

Create the folder and trait:

mkdir app/Traits

Here is the complete file.

app/Traits/ApiResponses.php

<?php

namespace App\Traits;

trait ApiResponses {
    protected function ok($message) {
        return $this->success($message, 200);
    }

    protected function success($message, $statusCode = 200) {
        return response()->json([
            'message' => $message,
            'status' => $statusCode
        ], $statusCode);
    }
}

ok() is a convenient shortcut for a successful 200 response. It delegates to success(), which builds the JSON payload and passes the same status code to Laravel's response factory.

The second argument to response()->json() matters: without it, the body might say 200 while the HTTP response uses a different status. Keeping both values together makes the response consistent.

3. Create the authentication controller

Generate the controller:

php artisan make:controller AuthController

Replace its contents with the complete implementation.

app/Http/Controllers/AuthController.php

<?php

namespace App\Http\Controllers;

use App\Traits\ApiResponses;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    use ApiResponses;
    public function login() {
        return $this->ok('Hello, Login!');
    }
}

The controller imports and uses ApiResponses, so all of the trait's protected helper methods become available inside the controller. The imported Request class is present in the reference implementation but is not used yet.

4. Register the API route

Here is the complete API routes file for this stage.

routes/api.php

<?php

use App\Http\Controllers\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

Route::get('/login', [AuthController::class, 'login']);

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Routes declared here automatically receive Laravel's /api prefix. Therefore, Route::get('/login', ...) is available at /api/login.

The default protected /api/user route remains in the file, but it is not used at this stage.

5. Run and test the API

Start Laravel's development server:

php artisan serve

Test the endpoint in another terminal:

curl -i http://127.0.0.1:8000/api/login

The important parts of the response are:

HTTP/1.1 200 OK
Content-Type: application/json

{"message":"Hello, Login!","status":200}

You can also verify the route registration:

php artisan route:list --path=api/login

Complete implementation file tree

Only these application-specific files are added or changed at this stage; the remaining files are the standard Laravel 13 application skeleton.

app/
├── Http/
│   └── Controllers/
│       └── AuthController.php
└── Traits/
    └── ApiResponses.php
routes/
└── api.php

Why use a response trait?

As an API grows, many controllers need to return the same response shapes. Centralizing those shapes prevents small inconsistencies and gives us one place to add other helpers later, such as created(), error(), or noContent().

For now, This foundation establishes the foundation: an API endpoint, a JSON payload, and an honest HTTP 200 status.


2. Testing APIs with Postman

Use Postman to send API requests, validate input, and inspect JSON errors instead of relying on a browser.

Implementation files

  • app/Http/Controllers/AuthController.php
  • app/Http/Requests/ApiLoginRequest.php
  • routes/api.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/AuthController.php

<?php

namespace App\Http\Controllers;

use App\Http\Requests\ApiLoginRequest;
use App\Traits\ApiResponses;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    use ApiResponses;
    public function login(ApiLoginRequest $request) {

        return $this->ok($request->get('email'));
    }

    public function register() {
        return $this->ok('register');
    }
}

app/Http/Requests/ApiLoginRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ApiLoginRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'email' => 'required',
            'password' => 'required'
        ];
    }
}

routes/api.php

<?php

use App\Http\Controllers\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

Route::prefix('v1')->group(base_path('routes/api_v1.php'));

Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);


Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

3. Designing Resource-Oriented URLs

Design resource-oriented order endpoints and prepare realistic models, factories, migrations, and seed data.

Orders carry a single-letter status code: P (pending), A (paid), S (shipped), and C (cancelled). Every validation rule, filter, and factory in this guide uses that same set.

Implementation files

  • app/Http/Controllers/AuthController.php
  • app/Http/Requests/ApiLoginRequest.php
  • app/Models/Order.php
  • database/factories/OrderFactory.php
  • database/migrations/2024_01_27_055742_create_orders_table.php
  • database/seeders/DatabaseSeeder.php
  • routes/api.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/AuthController.php

<?php

namespace App\Http\Controllers;

use App\Traits\ApiResponses;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    use ApiResponses;
    public function login() {
        return $this->ok('Hello, Login!');
    }
}

app/Models/Order.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    use HasFactory;
}

database/factories/OrderFactory.php

<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Order>
 */
class OrderFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'user_id' => User::factory(),
            'reference' => strtoupper(fake()->bothify('ord-#####')),
            'notes' => fake()->paragraph(),
            'status' => fake()->randomElement(['P', 'A', 'S', 'C']),
        ];
    }
}

database/migrations/2024_01_27_055742_create_orders_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained();
            $table->string('reference');
            $table->text('notes');
            $table->string('status');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('orders');
    }
};

database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $users = \App\Models\User::factory(10)->create();

        \App\Models\Order::factory(100)
            ->recycle($users)
            ->create();

        // \App\Models\User::factory()->create([
        //     'name' => 'Test User',
        //     'email' => 'test@example.com',
        // ]);
    }
}

routes/api.php

<?php

use App\Http\Controllers\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::prefix('v1')->group(base_path('routes/api_v1.php'));

Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);

Route::get('/orders', function() {
    return Order::all();
});


Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

4. Structuring a Versioned API

Introduce a versioned API boundary with dedicated routes, controllers, and request classes for version one.

Implementation files

  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Requests/Api/V1/StoreOrderRequest.php
  • app/Http/Requests/Api/V1/UpdateOrderRequest.php
  • bootstrap/app.php
  • routes/api.php
  • routes/api_v1.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;

class OrderController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return Order::all();
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(Order $order)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, Order $order)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Order $order)
    {
        //
    }
}

app/Http/Requests/Api/V1/StoreOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}

app/Http/Requests/Api/V1/UpdateOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class UpdateOrderRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}

routes/api.php

<?php

use Illuminate\Support\Facades\Route;

// Laravel adds the /api prefix when this file is registered in bootstrap/app.php.
Route::prefix('v1')->group(base_path('routes/api_v1.php'));

bootstrap/app.php

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        $exceptions->shouldRenderJsonWhen(
            fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
        );
    })->create();

routes/api.php

<?php

use App\Http\Controllers\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::prefix('v1')->group(base_path('routes/api_v1.php'));

Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

routes/api_v1.php

<?php

use App\Http\Controllers\Api\V1\OrderController;
use App\Http\Controllers\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::apiResource('orders', OrderController::class);

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

5. Token Authentication with Laravel Sanctum

Authenticate API clients with Laravel Sanctum, issue tokens, and protect versioned routes.

Implementation files

  • app/Http/Controllers/Api/AuthController.php
  • app/Http/Controllers/AuthController.php
  • app/Http/Requests/Api/LoginUserRequest.php
  • app/Traits/ApiResponses.php
  • routes/api.php
  • routes/api_v1.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/AuthController.php

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\LoginUserRequest;
use App\Models\User;
use App\Traits\ApiResponses;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class AuthController extends Controller
{
    use ApiResponses;
    public function login(LoginUserRequest $request) {
        $request->validated($request->all());

        if (!Auth::attempt($request->only('email', 'password'))) {
            return $this->error('Invalid credentials', 401);
        }

        $user = User::firstWhere('email', $request->email);

        return $this->ok(
            'Authenticated',
            [
                'token' => $user->createToken('API token for ' . $user->email)->plainTextToken
            ]
            );
    }
}

app/Http/Requests/Api/LoginUserRequest.php

<?php

namespace App\Http\Requests\Api;

use Illuminate\Foundation\Http\FormRequest;

class LoginUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'email' => ['required', 'string', 'email'],
            'password' => ['required', 'string', 'min:8'],
        ];
    }
}

app/Traits/ApiResponses.php

<?php

namespace App\Traits;

trait ApiResponses {
    protected function ok($message, $data) {
        return $this->success($message, $data, 200);
    }

    protected function success($message, $data, $statusCode = 200) {
        return response()->json([
            'data' => $data,
            'message' => $message,
            'status' => $statusCode
        ], $statusCode);
    }

    protected function error($message, $statusCode) {
        return response()->json([
            'message' => $message,
            'status' => $statusCode
        ], $statusCode);
    }
}

routes/api.php

<?php

use App\Http\Controllers\Api\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::prefix('v1')->group(base_path('routes/api_v1.php'));

Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

routes/api_v1.php

<?php

use App\Http\Controllers\Api\V1\OrderController;
use App\Http\Controllers\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::middleware('auth:sanctum')->apiResource('orders', OrderController::class);

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

6. Token Revocation and Secure Logout

Log clients out safely by revoking Sanctum tokens and return consistent no-content responses.

Implementation files

  • app/Http/Controllers/Api/AuthController.php
  • app/Traits/ApiResponses.php
  • routes/api.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/AuthController.php

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\LoginUserRequest;
use App\Models\User;
use App\Traits\ApiResponses;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class AuthController extends Controller
{
    use ApiResponses;
    public function login(LoginUserRequest $request) {
        $request->validated($request->all());

        if (!Auth::attempt($request->only('email', 'password'))) {
            return $this->error('Invalid credentials', 401);
        }

        $user = User::firstWhere('email', $request->email);

        return $this->ok(
            'Authenticated',
            [
                'token' => $user->createToken(
                    'API token for ' . $user->email,
                    ['*'],
                    now()->addMonth())->plainTextToken
            ]
            );
    }

    public function logout(Request $request) {
        $request->user()->currentAccessToken()->delete();

        return $this->ok('');
    }
}

app/Traits/ApiResponses.php

<?php

namespace App\Traits;

trait ApiResponses {
    protected function ok($message, $data = []) {
        return $this->success($message, $data, 200);
    }

    protected function success($message, $data = [], $statusCode = 200) {
        return response()->json([
            'data' => $data,
            'message' => $message,
            'status' => $statusCode
        ], $statusCode);
    }

    protected function error($message, $statusCode) {
        return response()->json([
            'message' => $message,
            'status' => $statusCode
        ], $statusCode);
    }
}

routes/api.php

<?php

use App\Http\Controllers\Api\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::prefix('v1')->group(base_path('routes/api_v1.php'));

Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);
Route::middleware('auth:sanctum')->post('/logout', [AuthController::class, 'logout']);

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

7. Designing Stable Response Payloads

Transform Eloquent orders into a stable public JSON contract with Laravel API Resources.

Implementation files

  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Resources/V1/OrderResource.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;

class OrderController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return OrderResource::collection(Order::paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(Order $order)
    {
        return new OrderResource($order);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, Order $order)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Order $order)
    {
        //
    }
}

app/Http/Resources/V1/OrderResource.php

<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    // public static $wrap = 'order';
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'type' => 'order',
            'id' => $this->id,
            'attributes' => [
                'reference' => $this->reference,
                'notes' => $this->notes,
                'status' => $this->status,
                'createdAt' => $this->created_at,
                'updatedAt' => $this->updated_at
            ],
            'relationships' => [
                'customer' => [
                    'data' => [
                        'type' => 'user',
                        'id' => $this->user_id
                    ],
                    'links' => [
                        ['self' => 'todo']
                    ]
                ]
            ],
            'links' => [
                ['self' => route('orders.show', ['order' => $this->id])]
            ]
        ];
    }
}

8. Conditional Fields and Relationships

Shape different payloads from the same resources by conditionally exposing fields and relationships.

Implementation files

  • app/Http/Controllers/Api/V1/UsersController.php
  • app/Http/Requests/Api/V1/StoreUserRequest.php
  • app/Http/Requests/Api/V1/UpdateUserRequest.php
  • app/Http/Resources/V1/OrderResource.php
  • app/Http/Resources/V1/UserResource.php
  • app/Models/Order.php
  • routes/api_v1.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/UsersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Http\Requests\Api\V1\StoreUserRequest;
use App\Http\Requests\Api\V1\UpdateUserRequest;
use App\Http\Resources\V1\UserResource;

class UsersController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return UserResource::collection(User::paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreUserRequest $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(User $user)
    {
        return new UserResource($user);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateUserRequest $request, User $user)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(User $user)
    {
        //
    }
}

app/Http/Requests/Api/V1/StoreUserRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}

app/Http/Requests/Api/V1/UpdateUserRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class UpdateUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}

app/Http/Resources/V1/OrderResource.php

<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    // public static $wrap = 'order';
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'type' => 'order',
            'id' => $this->id,
            'attributes' => [
                'reference' => $this->reference,
                'notes' => $this->when(
                    $request->routeIs('orders.show'),
                    $this->notes
                ),
                'status' => $this->status,
                'createdAt' => $this->created_at,
                'updatedAt' => $this->updated_at
            ],
            'relationships' => [
                'customer' => [
                    'data' => [
                        'type' => 'user',
                        'id' => $this->user_id
                    ],
                    'links' => [
                        ['self' => 'todo']
                    ]
                ]
            ],
            'includes' => [
                new UserResource($this->user)
            ],
            'links' => [
                ['self' => route('orders.show', ['order' => $this->id])]
            ]
        ];
    }
}

app/Http/Resources/V1/UserResource.php

<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'type' => 'user',
            'id' => $this->id,
            'attributes' => [
                'name' => $this->name,
                'email' => $this->email,
                $this->mergeWhen($request->routeIs('users.*'), [
                    'emailVerifiedAt' => $this->email_verified_at,
                    'createdAt' => $this->created_at,
                    'updatedAt' => $this->updated_at,
                ])
            ]
        ];
    }
}

app/Models/Order.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Order extends Model
{
    use HasFactory;

    public function user(): BelongsTo {
        return $this->belongsTo(User::class);
    }
}

routes/api_v1.php

<?php

use App\Http\Controllers\Api\V1\OrderController;
use App\Http\Controllers\Api\V1\UsersController;

use App\Http\Controllers\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::middleware('auth:sanctum')->apiResource('orders', OrderController::class);
Route::middleware('auth:sanctum')->apiResource('users', UsersController::class);


Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

9. Optional Relationship Loading

Let API consumers opt into relationship data through an include query parameter and avoid unnecessary queries.

Implementation files

  • app/Http/Controllers/Api/V1/ApiController.php
  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Controllers/Api/V1/UsersController.php
  • app/Http/Resources/V1/OrderResource.php
  • app/Http/Resources/V1/UserResource.php
  • app/Models/User.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/ApiController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class ApiController extends Controller
{
    public function include(string $relationship) : bool {
        $param = request()->get('include');

        if (!isset($param)) {
            return false;
        }

        $includeValues = explode(',', strtolower($param));

        return in_array(strtolower($relationship), $includeValues);
    }
}

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;

class OrderController extends ApiController
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        if ($this->include('customer')) {
            return OrderResource::collection(Order::with('user')->paginate());
        }

        return OrderResource::collection(Order::paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(Order $order)
    {
        if ($this->include('customer')) {
            return new OrderResource($order->load('user'));
        }

        return new OrderResource($order);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, Order $order)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Order $order)
    {
        //
    }
}

app/Http/Controllers/Api/V1/UsersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Models\User;
use App\Http\Requests\Api\V1\StoreUserRequest;
use App\Http\Requests\Api\V1\UpdateUserRequest;
use App\Http\Resources\V1\UserResource;

class UsersController extends ApiController
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        if ($this->include('orders')) {
            return UserResource::collection(User::with('orders')->paginate());
        }

        return UserResource::collection(User::paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreUserRequest $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(User $user)
    {
        if ($this->include('orders')) {
            return new UserResource($user->load('orders'));
        }

        return new UserResource($user);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateUserRequest $request, User $user)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(User $user)
    {
        //
    }
}

app/Http/Resources/V1/OrderResource.php

<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    // public static $wrap = 'order';
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'type' => 'order',
            'id' => $this->id,
            'attributes' => [
                'reference' => $this->reference,
                'notes' => $this->when(
                    $request->routeIs('orders.show'),
                    $this->notes
                ),
                'status' => $this->status,
                'createdAt' => $this->created_at,
                'updatedAt' => $this->updated_at
            ],
            'relationships' => [
                'customer' => [
                    'data' => [
                        'type' => 'user',
                        'id' => $this->user_id
                    ],
                    'links' => [
                        'self' => route('users.show', ['user' => $this->user_id])
                    ]
                ]
            ],
            'includes' => new UserResource($this->whenLoaded('user')),
            'links' => [
                'self' => route('orders.show', ['order' => $this->id])
            ]
        ];
    }
}

app/Http/Resources/V1/UserResource.php

<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'type' => 'user',
            'id' => $this->id,
            'attributes' => [
                'name' => $this->name,
                'email' => $this->email,
                $this->mergeWhen($request->routeIs('users.*'), [
                    'emailVerifiedAt' => $this->email_verified_at,
                    'createdAt' => $this->created_at,
                    'updatedAt' => $this->updated_at,
                ])
            ],
            'includes' => OrderResource::collection($this->whenLoaded('orders')),
            'links' => [
                'self' => route('users.show', ['user' => $this->id])
            ]
        ];
    }
}

app/Models/User.php

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    public function orders() : HasMany {
        return $this->hasMany(Order::class);
    }
}

10. Reusable Query Filters

Map query-string fields and operators to reusable query filter classes for order searches.

Implementation files

  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Filters/V1/QueryFilter.php
  • app/Http/Filters/V1/OrderFilter.php
  • app/Http/Resources/V1/OrderResource.php
  • app/Models/Order.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;

class OrderController extends ApiController
{
    /**
     * Display a listing of the resource.
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(Order $order)
    {
        if ($this->include('customer')) {
            return new OrderResource($order->load('customer'));
        }

        return new OrderResource($order);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, Order $order)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Order $order)
    {
        //
    }
}

app/Http/Filters/V1/QueryFilter.php

<?php

namespace App\Http\Filters\V1;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

abstract class QueryFilter {
    protected $builder;
    protected $request;

    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    protected function filter($arr) {
        foreach($arr as $key => $value) {
            if (method_exists($this, $key)) {
                $this->$key($value);
            }
        }

        return $this->builder;
    }

    public function apply(Builder $builder) {
        $this->builder = $builder;

        foreach($this->request->all() as $key => $value) {
            if (method_exists($this, $key)) {
                $this->$key($value);
            }
        }

        return $builder;
    }
}

app/Http/Filters/V1/OrderFilter.php

<?php

namespace App\Http\Filters\V1;

class OrderFilter extends QueryFilter {
    public function createdAt($value) {
        $dates = explode(',', $value);

        if (count($dates) > 1) {
            return $this->builder->whereBetween('created_at', $dates);
        }

        return $this->builder->whereDate('created_at', $value);
    }

    public function include($value) {
        return $this->builder->with($value);
    }

    public function status($value) {
        return $this->builder->whereIn('status', explode(',', $value));
    }

    public function reference($value) {
        $likeStr = str_replace('*', '%', $value);
        return $this->builder->where('reference', 'like', $likeStr);
    }

    public function updatedAt($value) {
        $dates = explode(',', $value);

        if (count($dates) > 1) {
            return $this->builder->whereBetween('updated_at', $dates);
        }

        return $this->builder->whereDate('updated_at', $value);
    }

    
}

app/Http/Resources/V1/OrderResource.php

<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    // public static $wrap = 'order';
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'type' => 'order',
            'id' => $this->id,
            'attributes' => [
                'reference' => $this->reference,
                'notes' => $this->when(
                    $request->routeIs('orders.show'),
                    $this->notes
                ),
                'status' => $this->status,
                'createdAt' => $this->created_at,
                'updatedAt' => $this->updated_at
            ],
            'relationships' => [
                'customer' => [
                    'data' => [
                        'type' => 'user',
                        'id' => $this->user_id
                    ],
                    'links' => [
                        'self' => route('users.show', ['user' => $this->user_id])
                    ]
                ]
            ],
            'includes' => new UserResource($this->whenLoaded('customer')),
            'links' => [
                'self' => route('orders.show', ['order' => $this->id])
            ]
        ];
    }
}

app/Models/Order.php

<?php

namespace App\Models;

use App\Http\Filters\V1\QueryFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Order extends Model
{
    use HasFactory;

    public function customer(): BelongsTo {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function scopeFilter(Builder $builder, QueryFilter $filters) {
        return $filters->apply($builder);
    }
}

11. Nested Resources and Relationship Filters

Model customers and their orders as nested resources while keeping payload and URL conventions consistent.

Implementation files

  • app/Http/Controllers/Api/V1/CustomerOrdersController.php
  • app/Http/Controllers/Api/V1/CustomersController.php
  • app/Http/Resources/V1/OrderResource.php
  • app/Http/Resources/V1/UserResource.php
  • routes/api_v1.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/CustomerOrdersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Filters\V1\OrderFilter;
use App\Http\Resources\V1\OrderResource;
use App\Models\Order;
use Illuminate\Http\Request;

class CustomerOrdersController extends Controller
{
    public function index($customer_id, OrderFilter $filters) {
        return OrderResource::collection(
            Order::where('user_id', $customer_id)->filter($filters)->paginate()
        );
    }
}

app/Http/Controllers/Api/V1/CustomersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Models\User;
use App\Http\Requests\Api\V1\StoreUserRequest;
use App\Http\Requests\Api\V1\UpdateUserRequest;
use App\Http\Resources\V1\UserResource;

class CustomersController extends ApiController
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        if ($this->include('orders')) {
            return UserResource::collection(User::with('orders')->paginate());
        }

        return UserResource::collection(User::paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreUserRequest $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(User $customer)
    {
        if ($this->include('orders')) {
            return new UserResource($customer->load('orders'));
        }

        return new UserResource($customer);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateUserRequest $request, User $user)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(User $user)
    {
        //
    }
}

app/Http/Resources/V1/OrderResource.php

<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    // public static $wrap = 'order';
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'type' => 'order',
            'id' => $this->id,
            'attributes' => [
                'reference' => $this->reference,
                'notes' => $this->when(
                    $request->routeIs('orders.show'),
                    $this->notes
                ),
                'status' => $this->status,
                'createdAt' => $this->created_at,
                'updatedAt' => $this->updated_at
            ],
            'relationships' => [
                'customer' => [
                    'data' => [
                        'type' => 'user',
                        'id' => $this->user_id
                    ],
                    'links' => [
                        'self' => route('customers.show', ['customer' => $this->user_id])
                    ]
                ]
            ],
            'includes' => new UserResource($this->whenLoaded('customer')),
            'links' => [
                'self' => route('orders.show', ['order' => $this->id])
            ]
        ];
    }
}

app/Http/Resources/V1/UserResource.php

<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'type' => 'user',
            'id' => $this->id,
            'attributes' => [
                'name' => $this->name,
                'email' => $this->email,
                $this->mergeWhen($request->routeIs('customers.*'), [
                    'emailVerifiedAt' => $this->email_verified_at,
                    'createdAt' => $this->created_at,
                    'updatedAt' => $this->updated_at,
                ])
            ],
            'includes' => OrderResource::collection($this->whenLoaded('orders')),
            'links' => [
                'self' => route('customers.show', ['customer' => $this->id])
            ]
        ];
    }
}

routes/api_v1.php

<?php

use App\Http\Controllers\Api\V1\OrderController;
use App\Http\Controllers\Api\V1\CustomersController;
use App\Http\Controllers\Api\V1\CustomerOrdersController;
use App\Http\Controllers\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::middleware('auth:sanctum')->apiResource('orders', OrderController::class);
Route::middleware('auth:sanctum')->apiResource('customers', CustomersController::class);
Route::middleware('auth:sanctum')->apiResource('customers.orders', CustomerOrdersController::class);



Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

12. Safe Client-Controlled Sorting

Add safe client-controlled sorting without exposing arbitrary database columns to the query string.

Implementation files

  • app/Http/Controllers/Api/V1/CustomersController.php
  • app/Http/Filters/V1/CustomerFilter.php
  • app/Http/Filters/V1/QueryFilter.php
  • app/Http/Filters/V1/OrderFilter.php
  • app/Models/User.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/CustomersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\CustomerFilter;
use App\Models\User;
use App\Http\Requests\Api\V1\StoreUserRequest;
use App\Http\Requests\Api\V1\UpdateUserRequest;
use App\Http\Resources\V1\UserResource;

class CustomersController extends ApiController
{
    /**
     * Display a listing of the resource.
     */
    public function index(CustomerFilter $filters)
    {
        return UserResource::collection(User::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreUserRequest $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(User $customer)
    {
        if ($this->include('orders')) {
            return new UserResource($customer->load('orders'));
        }

        return new UserResource($customer);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateUserRequest $request, User $user)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(User $user)
    {
        //
    }
}

app/Http/Filters/V1/CustomerFilter.php

<?php

namespace App\Http\Filters\V1;

class CustomerFilter extends QueryFilter {
    protected $sortable = [
        'name',
        'email',
        'createdAt' => 'created_at',
        'updatedAt' => 'updated_at'
    ];

    public function createdAt($value) {
        $dates = explode(',', $value);

        if (count($dates) > 1) {
            return $this->builder->whereBetween('created_at', $dates);
        }

        return $this->builder->whereDate('created_at', $value);
    }

    public function include($value) {
        return $this->builder->with($value);
    }

    public function id($value) {
        return $this->builder->whereIn('id', explode(',', $value));
    }

    public function email($value) {
        $likeStr = str_replace('*', '%', $value);
        return $this->builder->where('email', 'like', $likeStr);
    }

    public function name($value) {
        $likeStr = str_replace('*', '%', $value);
        return $this->builder->where('name', 'like', $likeStr);
    }

    public function updatedAt($value) {
        $dates = explode(',', $value);

        if (count($dates) > 1) {
            return $this->builder->whereBetween('updated_at', $dates);
        }

        return $this->builder->whereDate('updated_at', $value);
    }

    
}

app/Http/Filters/V1/QueryFilter.php

<?php

namespace App\Http\Filters\V1;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

abstract class QueryFilter {
    protected $builder;
    protected $request;
    protected $sortable = [];

    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    public function apply(Builder $builder) {
        $this->builder = $builder;

        foreach($this->request->all() as $key => $value) {
            if (method_exists($this, $key)) {
                $this->$key($value);
            }
        }

        return $builder;
    }

    protected function filter($arr) {
        foreach($arr as $key => $value) {
            if (method_exists($this, $key)) {
                $this->$key($value);
            }
        }

        return $this->builder;
    }

    protected function sort($value) {
        $sortAttributes = explode(',', $value);

        foreach($sortAttributes as $sortAttribute) {
            $direction = 'asc';

            if (strpos($sortAttribute, '-') === 0) {
                $direction = 'desc';
                $sortAttribute = substr($sortAttribute, 1);
            }

            if (!in_array($sortAttribute, $this->sortable) && !array_key_exists($sortAttribute, $this->sortable)) {
                continue;
            }

            $columnName = $this->sortable[$sortAttribute] ?? null;

            if ($columnName === null) {
                $columnName = $sortAttribute;
            }

            $this->builder->orderBy($columnName, $direction);
        }
    }
}

app/Http/Filters/V1/OrderFilter.php

<?php

namespace App\Http\Filters\V1;

class OrderFilter extends QueryFilter {
    protected $sortable = [
        'reference',
        'status',
        'createdAt' => 'created_at',
        'updatedAt' => 'updated_at'
    ];

    public function createdAt($value) {
        $dates = explode(',', $value);

        if (count($dates) > 1) {
            return $this->builder->whereBetween('created_at', $dates);
        }

        return $this->builder->whereDate('created_at', $value);
    }

    public function include($value) {
        return $this->builder->with($value);
    }

    public function status($value) {
        return $this->builder->whereIn('status', explode(',', $value));
    }

    public function reference($value) {
        $likeStr = str_replace('*', '%', $value);
        return $this->builder->where('reference', 'like', $likeStr);
    }

    public function updatedAt($value) {
        $dates = explode(',', $value);

        if (count($dates) > 1) {
            return $this->builder->whereBetween('updated_at', $dates);
        }

        return $this->builder->whereDate('updated_at', $value);
    }

    
}

app/Models/User.php

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;

use App\Http\Filters\V1\QueryFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    public function orders() : HasMany {
        return $this->hasMany(Order::class);
    }

    public function scopeFilter(Builder $builder, QueryFilter $filters) {
        return $filters->apply($builder);
    }
}

13. Creating Resources with POST

Validate POST payloads, create orders, and return the new resource with the correct HTTP status.

Implementation files

  • app/Http/Controllers/Api/V1/ApiController.php
  • app/Http/Controllers/Api/V1/CustomerOrdersController.php
  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Requests/Api/V1/StoreOrderRequest.php
  • app/Models/Order.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/ApiController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Traits\ApiResponses;
use Illuminate\Http\Request;

class ApiController extends Controller
{
    use ApiResponses;

    public function include(string $relationship) : bool {
        $param = request()->get('include');

        if (!isset($param)) {
            return false;
        }

        $includeValues = explode(',', strtolower($param));

        return in_array(strtolower($relationship), $includeValues);
    }
}

app/Http/Controllers/Api/V1/CustomerOrdersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\Order;
use Illuminate\Http\Request;

class CustomerOrdersController extends Controller
{
    public function index($customer_id, OrderFilter $filters) {
        return OrderResource::collection(
            Order::where('user_id', $customer_id)->filter($filters)->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store($customer_id, StoreOrderRequest $request)
    {
        $model = [
            'reference' => $request->input('data.attributes.reference'),
            'notes' => $request->input('data.attributes.notes'),
            'status' => $request->input('data.attributes.status'),
            'user_id' => $customer_id
        ];

        return new OrderResource(Order::create($model));
    }
}

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class OrderController extends ApiController
{
    /**
     * Display a listing of the resource.
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        try {
            User::findOrFail($request->input('data.relationships.customer.data.id'));
        } catch (ModelNotFoundException $exception) {
            return $this->error('The provided customer id does not exist.', 404);
        }

        $model = [
            'reference' => $request->input('data.attributes.reference'),
            'notes' => $request->input('data.attributes.notes'),
            'status' => $request->input('data.attributes.status'),
            'user_id' => $request->input('data.relationships.customer.data.id')
        ];

        return new OrderResource(Order::create($model));
    }

    /**
     * Display the specified resource.
     */
    public function show(Order $order)
    {
        if ($this->include('customer')) {
            return new OrderResource($order->load('customer'));
        }

        return new OrderResource($order);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, Order $order)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Order $order)
    {
        //
    }
}

app/Http/Requests/Api/V1/StoreOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data.attributes.reference' => 'required|string',
            'data.attributes.notes' => 'required|string',
            'data.attributes.status' => 'required|string|in:P,A,S,C',
        ];

        if ($this->routeIs('orders.store')) {
            $rules['data.relationships.customer.data.id'] = 'required|integer';
        }

        return $rules;
    }

    public function messages() {
        return [
            'data.attributes.status' => 'The data.attributes.status value is invalid. Please use P, A, S, or C.'
        ];
    }
}

app/Models/Order.php

<?php

namespace App\Models;

use App\Http\Filters\V1\QueryFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Order extends Model
{
    use HasFactory;

    protected $fillable = ['reference', 'status', 'notes', 'user_id'];

    public function customer(): BelongsTo {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function scopeFilter(Builder $builder, QueryFilter $filters) {
        return $filters->apply($builder);
    }
}

14. Deleting Resources with DELETE

Delete orders through resource controllers and return a semantically correct empty response.

Implementation files

  • app/Http/Controllers/Api/V1/CustomerOrdersController.php
  • app/Http/Controllers/Api/V1/OrderController.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/CustomerOrdersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\Order;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;

class CustomerOrdersController extends ApiController
{
    public function index($customer_id, OrderFilter $filters) {
        return OrderResource::collection(
            Order::where('user_id', $customer_id)->filter($filters)->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store($customer_id, StoreOrderRequest $request)
    {
        $model = [
            'reference' => $request->input('data.attributes.reference'),
            'notes' => $request->input('data.attributes.notes'),
            'status' => $request->input('data.attributes.status'),
            'user_id' => $customer_id
        ];

        return new OrderResource(Order::create($model));
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($customer_id, $order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            if ($order->user_id == $customer_id) {
                $order->delete();
                return $this->ok('Order successfully deleted');
            }

            return $this->error('Order cannot be found.', 404);
            
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class OrderController extends ApiController
{
    /**
     * Display a listing of the resource.
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        try {
            User::findOrFail($request->input('data.relationships.customer.data.id'));
        } catch (ModelNotFoundException $exception) {
            return $this->error('The provided customer id does not exist.', 404);
        }

        $model = [
            'reference' => $request->input('data.attributes.reference'),
            'notes' => $request->input('data.attributes.notes'),
            'status' => $request->input('data.attributes.status'),
            'user_id' => $request->input('data.relationships.customer.data.id')
        ];

        return new OrderResource(Order::create($model));
    }

    /**
     * Display the specified resource.
     */
    public function show($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            if ($this->include('customer')) {
                return new OrderResource($order->load('customer'));
            }

            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, Order $order)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);
            $order->delete();

            return $this->ok('Order successfully deleted');
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

15. Full Resource Replacement with PUT

Implement full resource replacement, understand PUT semantics, and validate every required field.

Implementation files

  • app/Http/Controllers/Api/V1/CustomerOrdersController.php
  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Requests/Api/V1/ReplaceOrderRequest.php
  • app/Http/Resources/V1/OrderResource.php
  • routes/api_v1.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/CustomerOrdersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\Order;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;

class CustomerOrdersController extends ApiController
{
    public function index($customer_id, OrderFilter $filters) {
        return OrderResource::collection(
            Order::where('user_id', $customer_id)->filter($filters)->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store($customer_id, StoreOrderRequest $request)
    {
        $model = [
            'reference' => $request->input('data.attributes.reference'),
            'notes' => $request->input('data.attributes.notes'),
            'status' => $request->input('data.attributes.status'),
            'user_id' => $customer_id
        ];

        return new OrderResource(Order::create($model));
    }

    public function replace(ReplaceOrderRequest $request, $customer_id,  $order_id) {
        // PUT
        try {
            $order = Order::findOrFail($order_id);

            if ($order->user_id == $customer_id) {
                
                $model = [
                    'reference' => $request->input('data.attributes.reference'),
                    'notes' => $request->input('data.attributes.notes'),
                    'status' => $request->input('data.attributes.status'),
                    'user_id' => $request->input('data.relationships.customer.data.id')
                ];

                $order->update($model);
                return new OrderResource($order);
            }
            // TODO: order doesn't belong to user
    
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($customer_id, $order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            if ($order->user_id == $customer_id) {
                $order->delete();
                return $this->ok('Order successfully deleted');
            }

            return $this->error('Order cannot be found.', 404);
            
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class OrderController extends ApiController
{
    /**
     * Display a listing of the resource.
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        try {
            User::findOrFail($request->input('data.relationships.customer.data.id'));
        } catch (ModelNotFoundException $exception) {
            return $this->error('The provided customer id does not exist.', 404);
        }

        $model = [
            'reference' => $request->input('data.attributes.reference'),
            'notes' => $request->input('data.attributes.notes'),
            'status' => $request->input('data.attributes.status'),
            'user_id' => $request->input('data.relationships.customer.data.id')
        ];

        return new OrderResource(Order::create($model));
    }

    /**
     * Display the specified resource.
     */
    public function show($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            if ($this->include('customer')) {
                return new OrderResource($order->load('customer'));
            }

            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, $order_id)
    {
        // PATCH
    }

    public function replace(ReplaceOrderRequest $request, $order_id) {
        // PUT
        try {
            $order = Order::findOrFail($order_id);

            $model = [
                'reference' => $request->input('data.attributes.reference'),
                'notes' => $request->input('data.attributes.notes'),
                'status' => $request->input('data.attributes.status'),
                'user_id' => $request->input('data.relationships.customer.data.id')
            ];

            $order->update($model);
    
            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);
            $order->delete();

            return $this->ok('Order successfully deleted');
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

app/Http/Requests/Api/V1/ReplaceOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class ReplaceOrderRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data.attributes.reference' => 'required|string',
            'data.attributes.notes' => 'required|string',
            'data.attributes.status' => 'required|string|in:P,A,S,C',
            'data.relationships.customer.data.id' => 'required|integer',
        ];

        return $rules;
    }

    public function messages() {
        return [
            'data.attributes.status' => 'The data.attributes.status value is invalid. Please use P, A, S, or C.'
        ];
    }
}

app/Http/Resources/V1/OrderResource.php

<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    // public static $wrap = 'order';
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'type' => 'order',
            'id' => $this->id,
            'attributes' => [
                'reference' => $this->reference,
                'notes' => $this->when(
                    !$request->routeIs(['orders.index', 'customers.orders.index']),
                    $this->notes
                ),
                'status' => $this->status,
                'createdAt' => $this->created_at,
                'updatedAt' => $this->updated_at
            ],
            'relationships' => [
                'customer' => [
                    'data' => [
                        'type' => 'user',
                        'id' => $this->user_id
                    ],
                    'links' => [
                        'self' => route('customers.show', ['customer' => $this->user_id])
                    ]
                ]
            ],
            'includes' => new UserResource($this->whenLoaded('customer')),
            'links' => [
                'self' => route('orders.show', ['order' => $this->id])
            ]
        ];
    }
}

routes/api_v1.php

<?php

use App\Http\Controllers\Api\V1\OrderController;
use App\Http\Controllers\Api\V1\CustomersController;
use App\Http\Controllers\Api\V1\CustomerOrdersController;
use App\Http\Controllers\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::middleware('auth:sanctum')->group(function() {
    Route::apiResource('orders', OrderController::class)->except(['update']);
    Route::put('orders/{order}', [OrderController::class, 'replace']);
    
    Route::apiResource('customers', CustomersController::class);
    Route::apiResource('customers.orders', CustomerOrdersController::class)->except(['update']);
    Route::put('customers/{customer}/orders/{order}', [CustomerOrdersController::class, 'replace']);

    
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
});

16. Partial Resource Updates with PATCH

Implement partial updates while sharing validation rules safely between store, replace, and update requests.

Implementation files

  • app/Http/Controllers/Api/V1/CustomerOrdersController.php
  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Requests/Api/V1/BaseOrderRequest.php
  • app/Http/Requests/Api/V1/ReplaceOrderRequest.php
  • app/Http/Requests/Api/V1/StoreOrderRequest.php
  • app/Http/Requests/Api/V1/UpdateOrderRequest.php
  • routes/api_v1.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/CustomerOrdersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\Order;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;

class CustomerOrdersController extends ApiController
{
    public function index($customer_id, OrderFilter $filters) {
        return OrderResource::collection(
            Order::where('user_id', $customer_id)->filter($filters)->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store($customer_id, StoreOrderRequest $request)
    {
        return new OrderResource(Order::create($request->mappedAttributes()));
    }

    public function replace(ReplaceOrderRequest $request, $customer_id,  $order_id) {
        // PUT
        try {
            $order = Order::findOrFail($order_id);

            if ($order->user_id == $customer_id) {
                
                $order->update($request->mappedAttributes());
                return new OrderResource($order);
            }
            // TODO: order doesn't belong to user
    
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    public function update(UpdateOrderRequest $request, $customer_id,  $order_id) {
        // PUT
        try {
            $order = Order::findOrFail($order_id);

            if ($order->user_id == $customer_id) {
                $order->update($request->mappedAttributes());
                return new OrderResource($order);
            }
            // TODO: order doesn't belong to user
    
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($customer_id, $order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            if ($order->user_id == $customer_id) {
                $order->delete();
                return $this->ok('Order successfully deleted');
            }

            return $this->error('Order cannot be found.', 404);
            
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class OrderController extends ApiController
{
    /**
     * Display a listing of the resource.
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        try {
            User::findOrFail($request->input('data.relationships.customer.data.id'));
        } catch (ModelNotFoundException $exception) {
            return $this->error('The provided customer id does not exist.', 404);
        }

        return new OrderResource(Order::create($request->mappedAttributes()));
    }

    /**
     * Display the specified resource.
     */
    public function show($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            if ($this->include('customer')) {
                return new OrderResource($order->load('customer'));
            }

            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, $order_id)
    {
        // PATCH
        try {
            $order = Order::findOrFail($order_id);
            
            $order->update($request->mappedAttributes());
    
            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    public function replace(ReplaceOrderRequest $request, $order_id) {
        // PUT
        try {
            $order = Order::findOrFail($order_id);

            $order->update($request->mappedAttributes());

    
            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);
            $order->delete();

            return $this->ok('Order successfully deleted');
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

app/Http/Requests/Api/V1/BaseOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class BaseOrderRequest extends FormRequest
{
    public function mappedAttributes() {
        $attributeMap = [
            'data.attributes.reference' => 'reference',
            'data.attributes.notes' => 'notes',
            'data.attributes.status' => 'status',
            'data.attributes.createdAt' => 'created_at',
            'data.attributes.updatedAt' => 'updated_at',
            'data.relationships.customer.data.id' => 'user_id',
        ];

        $attributesToUpdate = [];
        foreach ($attributeMap as $key => $attribute) {
            if ($this->has($key)) {
                $attributesToUpdate[$attribute] = $this->input($key);
            }
        }

        return $attributesToUpdate;
    }

    public function messages() {
        return [
            'data.attributes.status' => 'The data.attributes.status value is invalid. Please use P, A, S, or C.'
        ];
    }
}

app/Http/Requests/Api/V1/ReplaceOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class ReplaceOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data.attributes.reference' => 'required|string',
            'data.attributes.notes' => 'required|string',
            'data.attributes.status' => 'required|string|in:P,A,S,C',
            'data.relationships.customer.data.id' => 'required|integer',
        ];

        return $rules;
    }
}

app/Http/Requests/Api/V1/StoreOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data.attributes.reference' => 'required|string',
            'data.attributes.notes' => 'required|string',
            'data.attributes.status' => 'required|string|in:P,A,S,C',
        ];

        if ($this->routeIs('orders.store')) {
            $rules['data.relationships.customer.data.id'] = 'required|integer';
        }

        return $rules;
    }
}

app/Http/Requests/Api/V1/UpdateOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class UpdateOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data.attributes.reference' => 'sometimes|string',
            'data.attributes.notes' => 'sometimes|string',
            'data.attributes.status' => 'sometimes|string|in:P,A,S,C',
            'data.relationships.customer.data.id' => 'sometimes|integer',
        ];

        return $rules;
    }
}

routes/api_v1.php

<?php

use App\Http\Controllers\Api\V1\OrderController;
use App\Http\Controllers\Api\V1\CustomersController;
use App\Http\Controllers\Api\V1\CustomerOrdersController;
use App\Http\Controllers\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::middleware('auth:sanctum')->group(function() {
    Route::apiResource('orders', OrderController::class)->except(['update']);
    Route::put('orders/{order}', [OrderController::class, 'replace']);
    Route::patch('orders/{order}', [OrderController::class, 'update']);

    Route::apiResource('customers', CustomersController::class);
    Route::apiResource('customers.orders', CustomerOrdersController::class)->except(['update']);
    Route::put('customers/{customer}/orders/{order}', [CustomerOrdersController::class, 'replace']);
    Route::patch('customers/{customer}/orders/{order}', [CustomerOrdersController::class, 'update']);

    Route::get('/user', function (Request $request) {
        return $request->user();
    });
});

17. Resource Authorization with Policies

Authorize actions against specific order instances with a Laravel policy and controller helpers.

Implementation files

  • app/Http/Controllers/Api/V1/ApiController.php
  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Policies/V1/OrderPolicy.php
  • app/Providers/AppServiceProvider.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/ApiController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Traits\ApiResponses;
use Illuminate\Http\Request;

class ApiController extends Controller
{
    use ApiResponses;

    protected $policyClass;

    public function include(string $relationship) : bool {
        $param = request()->get('include');

        if (!isset($param)) {
            return false;
        }

        $includeValues = explode(',', strtolower($param));

        return in_array(strtolower($relationship), $includeValues);
    }

    public function isAble($ability, $targetModel) {
        return $this->authorize($ability, [$targetModel, $this->policyClass]);
    }
}

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\User;
use App\Policies\V1\OrderPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class OrderController extends ApiController
{
    protected $policyClass = OrderPolicy::class;
    /**
     * Display a listing of the resource.
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        try {
            User::findOrFail($request->input('data.relationships.customer.data.id'));
        } catch (ModelNotFoundException $exception) {
            return $this->error('The provided customer id does not exist.', 404);
        }

        return new OrderResource(Order::create($request->mappedAttributes()));
    }

    /**
     * Display the specified resource.
     */
    public function show($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            if ($this->include('customer')) {
                return new OrderResource($order->load('customer'));
            }

            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, $order_id)
    {
        // PATCH
        try {
            $order = Order::findOrFail($order_id);

            // policy
            $this->isAble('update', $order);
            
            $order->update($request->mappedAttributes());
    
            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        } catch (AuthorizationException $ex) {
            return $this->error('You are not authorized to update that resource', 403);
        }
    }

    public function replace(ReplaceOrderRequest $request, $order_id) {
        // PUT
        try {
            $order = Order::findOrFail($order_id);

            $order->update($request->mappedAttributes());

    
            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);
            $order->delete();

            return $this->ok('Order successfully deleted');
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

app/Policies/V1/OrderPolicy.php

<?php

namespace App\Policies\V1;

use App\Models\Order;
use App\Models\User;

class OrderPolicy
{
    /**
     * Create a new policy instance.
     */
    public function __construct()
    {
        //
    }

    public function update(User $user, Order $order) {
        // TODO check for token ability
        return $user->id === $order->user_id;
    }
 }

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use App\Models\Order;
use App\Policies\V1\OrderPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        //
    }

    public function boot(): void
    {
        Gate::policy(Order::class, OrderPolicy::class);
    }
}

18. Access Control with Token Abilities

Assign Sanctum token abilities and combine them with policies to express API roles and permissions.

Implementation files

  • app/Http/Controllers/Api/AuthController.php
  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Permissions/V1/Abilities.php
  • app/Policies/V1/OrderPolicy.php
  • database/migrations/2014_10_12_000000_create_users_table.php
  • database/seeders/DatabaseSeeder.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/AuthController.php

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\LoginUserRequest;
use App\Models\User;
use App\Permissions\V1\Abilities;
use App\Traits\ApiResponses;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class AuthController extends Controller
{
    use ApiResponses;
    public function login(LoginUserRequest $request) {
        $request->validated($request->all());

        if (!Auth::attempt($request->only('email', 'password'))) {
            return $this->error('Invalid credentials', 401);
        }

        $user = User::firstWhere('email', $request->email);

        return $this->ok(
            'Authenticated',
            [
                'token' => $user->createToken(
                    'API token for ' . $user->email,
                    Abilities::getAbilities($user),
                    now()->addMonth())->plainTextToken
            ]
            );
    }

    public function logout(Request $request) {
        $request->user()->currentAccessToken()->delete();

        return $this->ok('');
    }
}

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\User;
use App\Policies\V1\OrderPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class OrderController extends ApiController
{
    protected $policyClass = OrderPolicy::class;
    /**
     * Display a listing of the resource.
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        try {
            User::findOrFail($request->input('data.relationships.customer.data.id'));

            // policy
            $this->isAble('store', Order::class);

            return new OrderResource(Order::create($request->mappedAttributes()));
        } catch (ModelNotFoundException $exception) {
            return $this->error('The provided customer id does not exist.', 404);
        } catch (AuthorizationException $ex) {
            return $this->error('You are not authorized to create that resource', 403);
        }
    }

    /**
     * Display the specified resource.
     */
    public function show($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            if ($this->include('customer')) {
                return new OrderResource($order->load('customer'));
            }

            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, $order_id)
    {
        // PATCH
        try {
            $order = Order::findOrFail($order_id);

            // policy
            $this->isAble('update', $order);
            
            $order->update($request->mappedAttributes());
    
            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        } catch (AuthorizationException $ex) {
            return $this->error('You are not authorized to update that resource', 403);
        }
    }

    public function replace(ReplaceOrderRequest $request, $order_id) {
        // PUT
        try {
            $order = Order::findOrFail($order_id);

            // policy
            $this->isAble('replace', $order);

            $order->update($request->mappedAttributes());

    
            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            // policy
            $this->isAble('delete', $order);

            $order->delete();

            return $this->ok('Order successfully deleted');
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

app/Permissions/V1/Abilities.php

<?php

namespace App\Permissions\V1;

use App\Models\User;

final class Abilities {
    public const CreateOrder = 'order:create';
    public const UpdateOrder = 'order:update';
    public const ReplaceOrder = 'order:replace';
    public const DeleteOrder = 'order:delete';

    public const UpdateOwnOrder = 'order:own:update';
    public const DeleteOwnOrder = 'order:own:delete';

    public const CreateUser = 'user:create';
    public const UpdateUser = 'user:update';
    public const ReplaceUser = 'user:replace';
    public const DeleteUser = 'user:delete';

    public static function getAbilities(User $user) {
        if ($user->is_manager) {
            return [
                self::CreateOrder,
                self::UpdateOrder,
                self::ReplaceOrder,
                self::DeleteOrder,
                self::CreateUser,
                self::UpdateUser,
                self::ReplaceUser,
                self::DeleteUser,
            ];
        } else {
            return [
                self::CreateOrder,
                self::UpdateOwnOrder,
                self::DeleteOwnOrder
            ];
        }
    }
    

}

app/Policies/V1/OrderPolicy.php

<?php

namespace App\Policies\V1;

use App\Models\Order;
use App\Models\User;
use App\Permissions\V1\Abilities;

class OrderPolicy
{
    /**
     * Create a new policy instance.
     */
    public function __construct()
    {
        //
    }

    public function delete(User $user, Order $order) {
        if ($user->tokenCan(Abilities::DeleteOrder)) {
            return true;
        } else if ($user->tokenCan(Abilities::DeleteOwnOrder)) {
            return $user->id === $order->user_id;
        }

        return false;
    }

    public function replace(User $user, Order $order) {
        if ($user->tokenCan(Abilities::ReplaceOrder)) {
            return true;
        } 

        return false;
    }

    public function store(User $user) {
        if ($user->tokenCan(Abilities::CreateOrder)) {
            return true;
        } 

        return false;
    }

    public function update(User $user, Order $order) {
        if ($user->tokenCan(Abilities::UpdateOrder)) {
            return true;
        } else if ($user->tokenCan(Abilities::UpdateOwnOrder)) {
            return $user->id === $order->user_id;
        }

        return false;
    }
 }

database/migrations/2014_10_12_000000_create_users_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->boolean('is_manager')->default(false);
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};

database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $users = \App\Models\User::factory(10)->create();

        \App\Models\Order::factory(100)
            ->recycle($users)
            ->create();

        \App\Models\User::create([
            'email' => 'manager@manager.com',
            'password' => bcrypt('password'),
            'name' => 'The Manager',
            'is_manager' => true
        ]);

        // \App\Models\User::factory()->create([
        //     'name' => 'Test User',
        //     'email' => 'test@example.com',
        // ]);
    }
}

19. Fine-Grained Field Permissions

Restrict individual operations and attributes with fine-grained abilities instead of broad all-or-nothing access.

Implementation files

  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Requests/Api/V1/StoreOrderRequest.php
  • app/Http/Requests/Api/V1/UpdateOrderRequest.php
  • app/Permissions/V1/Abilities.php
  • app/Policies/V1/OrderPolicy.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\User;
use App\Policies\V1\OrderPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class OrderController extends ApiController
{
    protected $policyClass = OrderPolicy::class;
    /**
     * Display a listing of the resource.
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        try {
           // policy
            $this->isAble('store', Order::class);

            return new OrderResource(Order::create($request->mappedAttributes()));
        } catch (AuthorizationException $ex) {
            return $this->error('You are not authorized to update that resource', 403);
        }
    }

    /**
     * Display the specified resource.
     */
    public function show($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            if ($this->include('customer')) {
                return new OrderResource($order->load('customer'));
            }

            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, $order_id)
    {
        // PATCH
        try {
            $order = Order::findOrFail($order_id);

            // policy
            $this->isAble('update', $order);
            
            $order->update($request->mappedAttributes());
    
            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        } catch (AuthorizationException $ex) {
            return $this->error('You are not authorized to update that resource', 403);
        }
    }

    public function replace(ReplaceOrderRequest $request, $order_id) {
        // PUT
        try {
            $order = Order::findOrFail($order_id);

            // policy
            $this->isAble('replace', $order);

            $order->update($request->mappedAttributes());

    
            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            // policy
            $this->isAble('delete', $order);

            $order->delete();

            return $this->ok('Order successfully deleted');
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

app/Http/Requests/Api/V1/StoreOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use App\Permissions\V1\Abilities;
use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data.attributes.reference' => 'required|string',
            'data.attributes.notes' => 'required|string',
            'data.attributes.status' => 'required|string|in:P,A,S,C',
            'data.relationships.customer.data.id' => 'required|integer|exists:users,id'
        ];

        $user = $this->user();

        if ($this->routeIs('orders.store')) {
            if ($user->tokenCan(Abilities::CreateOwnOrder)) {
                $rules['data.relationships.customer.data.id'] .= '|size:' . $user->id;
            }
        }

        return $rules;
    }
}

app/Http/Requests/Api/V1/UpdateOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use App\Permissions\V1\Abilities;
use Illuminate\Foundation\Http\FormRequest;

class UpdateOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data.attributes.reference' => 'sometimes|string',
            'data.attributes.notes' => 'sometimes|string',
            'data.attributes.status' => 'sometimes|string|in:P,A,S,C',
            'data.relationships.customer.data.id' => 'sometimes|integer',
        ];

        if ($this->user()->tokenCan(Abilities::UpdateOwnOrder)) {
            $rules['data.relationships.customer.data.id'] = 'prohibited';
        }

        return $rules;
    }
}

app/Permissions/V1/Abilities.php

<?php

namespace App\Permissions\V1;

use App\Models\User;

final class Abilities {
    public const CreateOrder = 'order:create';
    public const UpdateOrder = 'order:update';
    public const ReplaceOrder = 'order:replace';
    public const DeleteOrder = 'order:delete';

    public const CreateOwnOrder = 'order:own:create';
    public const UpdateOwnOrder = 'order:own:update';
    public const DeleteOwnOrder = 'order:own:delete';

    public const CreateUser = 'user:create';
    public const UpdateUser = 'user:update';
    public const ReplaceUser = 'user:replace';
    public const DeleteUser = 'user:delete';

    public static function getAbilities(User $user) {
        // don't assign '*'
        if ($user->is_manager) {
            return [
                self::CreateOrder,
                self::UpdateOrder,
                self::ReplaceOrder,
                self::DeleteOrder,
                self::CreateUser,
                self::UpdateUser,
                self::ReplaceUser,
                self::DeleteUser,
            ];
        } else {
            return [
                self::CreateOwnOrder,
                self::UpdateOwnOrder,
                self::DeleteOwnOrder
            ];
        }
    }
    

}

app/Policies/V1/OrderPolicy.php

<?php

namespace App\Policies\V1;

use App\Models\Order;
use App\Models\User;
use App\Permissions\V1\Abilities;

class OrderPolicy
{
    /**
     * Create a new policy instance.
     */
    public function __construct()
    {
        //
    }

    public function delete(User $user, Order $order) {
        if ($user->tokenCan(Abilities::DeleteOrder)) {
            return true;
        } else if ($user->tokenCan(Abilities::DeleteOwnOrder)) {
            return $user->id === $order->user_id;
        }

        return false;
    }

    public function replace(User $user, Order $order) {
        if ($user->tokenCan(Abilities::ReplaceOrder)) {
            return true;
        } 

        return false;
    }

    public function store(User $user) {
        return $user->tokenCan(Abilities::CreateOrder) ||
               $user->tokenCan(Abilities::CreateOwnOrder);
    }

    public function update(User $user, Order $order) {
        if ($user->tokenCan(Abilities::UpdateOrder)) {
            return true;
        } else if ($user->tokenCan(Abilities::UpdateOwnOrder)) {
            return $user->id === $order->user_id;
        }

        return false;
    }
 }

20. Customer-Owned Order Operations

Apply filtering, validation, ownership, and authorization consistently to nested customer order endpoints.

Implementation files

  • app/Http/Controllers/Api/V1/CustomerOrdersController.php
  • app/Http/Requests/Api/V1/BaseOrderRequest.php
  • app/Http/Requests/Api/V1/StoreOrderRequest.php
  • app/Policies/V1/OrderPolicy.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/CustomerOrdersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\Order;
use App\Policies\V1\OrderPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;

class CustomerOrdersController extends ApiController
{
    protected $policyClass = OrderPolicy::class;

    public function index($customer_id, OrderFilter $filters) {
        return OrderResource::collection(
            Order::where('user_id', $customer_id)->filter($filters)->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request, $customer_id)
    {
        try {
            // policy
             $this->isAble('store', Order::class);
 
             return new OrderResource(Order::create($request->mappedAttributes([
                'customer' => 'user_id'
             ])));
         } catch (AuthorizationException $ex) {
             return $this->error('You are not authorized to create that resource', 403);
         }
    }

    public function replace(ReplaceOrderRequest $request, $customer_id,  $order_id) {
        // PUT
        try {
            $order = Order::where('id', $order_id)
                            ->where('user_id', $customer_id)
                            ->firstOrFail();

            $this->isAble('replace', $order);
                
            $order->update($request->mappedAttributes());
            return new OrderResource($order);
    
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        } catch (AuthorizationException $ex) {
            return $this->error('You are not authorized to update that resource', 403);
        }
    }

    public function update(UpdateOrderRequest $request, $customer_id,  $order_id) {
        // PUT
        try {
            $order = Order::where('id', $order_id)
                            ->where('user_id', $customer_id)
                            ->firstOrFail();

            $this->isAble('update', $order);

            $order->update($request->mappedAttributes());
            return new OrderResource($order);    
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        } catch (AuthorizationException $ex) {
            return $this->error('You are not authorized to update that resource', 403);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($customer_id, $order_id)
    {
        try {
            $order = Order::where('id', $order_id)
                            ->where('user_id', $customer_id)
                            ->firstOrFail();

            $this->isAble('delete', $order);

            $order->delete();
            return $this->ok('Order successfully deleted');            
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        } catch (AuthorizationException $ex) {
            return $this->error('You are not authorized to delete that resource', 403);
        }
    }
}

app/Http/Requests/Api/V1/BaseOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class BaseOrderRequest extends FormRequest
{
    public function mappedAttributes(array $otherAttributes = []) {
        $attributeMap = array_merge([
            'data.attributes.reference' => 'reference',
            'data.attributes.notes' => 'notes',
            'data.attributes.status' => 'status',
            'data.attributes.createdAt' => 'created_at',
            'data.attributes.updatedAt' => 'updated_at',
            'data.relationships.customer.data.id' => 'user_id',
        ], $otherAttributes);

        $attributesToUpdate = [];
        foreach ($attributeMap as $key => $attribute) {
            if ($this->has($key)) {
                $attributesToUpdate[$attribute] = $this->input($key);
            }
        }

        return $attributesToUpdate;
    }

    public function messages() {
        return [
            'data.attributes.status' => 'The data.attributes.status value is invalid. Please use P, A, S, or C.'
        ];
    }
}

app/Http/Requests/Api/V1/StoreOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use App\Permissions\V1\Abilities;
use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $customerIdAttr = $this->routeIs('orders.store') ? 'data.relationships.customer.data.id' : 'customer';

        $rules = [
            'data.attributes.reference' => 'required|string',
            'data.attributes.notes' => 'required|string',
            'data.attributes.status' => 'required|string|in:P,A,S,C',
            $customerIdAttr => 'required|integer|exists:users,id'
        ];

        $user = $this->user();

        if ($user->tokenCan(Abilities::CreateOwnOrder)) {
            $rules[$customerIdAttr] .= '|size:' . $user->id;
        }
        
        return $rules;
    }

    protected function prepareForValidation() {
        if ($this->routeIs('customers.orders.store')) {
            $this->merge([
                'customer' => $this->route('customer')
            ]);
        }
    }
}

app/Policies/V1/OrderPolicy.php

<?php

namespace App\Policies\V1;

use App\Models\Order;
use App\Models\User;
use App\Permissions\V1\Abilities;

class OrderPolicy
{
    /**
     * Create a new policy instance.
     */
    public function __construct()
    {
        //
    }

    public function delete(User $user, Order $order) {
        if ($user->tokenCan(Abilities::DeleteOrder)) {
            return true;
        } else if ($user->tokenCan(Abilities::DeleteOwnOrder)) {
            return $user->id === $order->user_id;
        }

        return false;
    }

    public function replace(User $user, Order $order) {
        return $user->tokenCan(Abilities::ReplaceOrder);
    }

    public function store(User $user) {
        return $user->tokenCan(Abilities::CreateOrder) ||
               $user->tokenCan(Abilities::CreateOwnOrder);
    }

    public function update(User $user, Order $order) {
        if ($user->tokenCan(Abilities::UpdateOrder)) {
            return true;
        } else if ($user->tokenCan(Abilities::UpdateOwnOrder)) {
            return $user->id === $order->user_id;
        }

        return false;
    }
 }

21. Secure User Management

Build complete user-management endpoints with resources, form requests, policies, and route registration.

Implementation files

  • app/Http/Controllers/Api/V1/CustomersController.php
  • app/Http/Controllers/Api/V1/UserController.php
  • app/Http/Requests/Api/V1/BaseUserRequest.php
  • app/Http/Requests/Api/V1/ReplaceUserRequest.php
  • app/Http/Requests/Api/V1/StoreUserRequest.php
  • app/Http/Requests/Api/V1/UpdateUserRequest.php
  • app/Http/Resources/V1/UserResource.php
  • app/Models/User.php
  • app/Policies/V1/UserPolicy.php
  • app/Providers/AppServiceProvider.php
  • routes/api_v1.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/CustomersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\CustomerFilter;
use App\Models\User;
use App\Http\Requests\Api\V1\StoreUserRequest;
use App\Http\Requests\Api\V1\UpdateUserRequest;
use App\Http\Resources\V1\UserResource;

class CustomersController extends ApiController
{
    /**
     * Display a listing of the resource.
     */
    public function index(CustomerFilter $filters)
    {
        return UserResource::collection(
            User::select('users.*')
                ->join('orders', 'users.id', '=', 'orders.user_id')
                ->filter($filters)
                ->distinct()
                ->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreUserRequest $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(User $customer)
    {
        if ($this->include('orders')) {
            return new UserResource($customer->load('orders'));
        }

        return new UserResource($customer);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateUserRequest $request, User $user)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(User $user)
    {
        //
    }
}

app/Http/Controllers/Api/V1/UserController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\CustomerFilter;
use App\Http\Requests\Api\V1\ReplaceUserRequest;
use App\Models\User;
use App\Http\Requests\Api\V1\StoreUserRequest;
use App\Http\Requests\Api\V1\UpdateUserRequest;
use App\Http\Resources\V1\UserResource;
use App\Policies\V1\UserPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class UserController extends ApiController
{
    protected $policyClass = UserPolicy::class;
    /**
     * Display a listing of the resource.
     */
    public function index(CustomerFilter $filters)
    {
        return UserResource::collection(
            User::filter($filters)->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreUserRequest $request)
    {
        try {
            // policy
             $this->isAble('store', User::class);
 
             return new UserResource(User::create($request->mappedAttributes()));
         } catch (AuthorizationException $ex) {
             return $this->error('You are not authorized to create that resource', 403);
         }
    }

    /**
     * Display the specified resource.
     */
    public function show(User $user)
    {
        if ($this->include('orders')) {
            return new UserResource($user->load('orders'));
        }

        return new UserResource($user);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateUserRequest $request, $user_id)
    {
        try {
            $user = User::findOrFail($user_id);

            // policy
            $this->isAble('update', $user);
            
            $user->update($request->mappedAttributes());
    
            return new UserResource($user);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        } catch (AuthorizationException $ex) {
            return $this->error('You are not authorized to update that resource', 403);
        }
    }

    public function replace(ReplaceUserRequest $request, $user_id) {
        // PUT
        try {
            $user = User::findOrFail($user_id);

            // policy
            $this->isAble('replace', $user);

            $user->update($request->mappedAttributes());

    
            return new UserResource($user);
        } catch (ModelNotFoundException $exception) {
            return $this->error('User cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($user_id)
    {
        try {
            $user = User::findOrFail($user_id);

            // policy
            $this->isAble('delete', $user);

            $user->delete();

            return $this->ok('User successfully deleted');
        } catch (ModelNotFoundException $exception) {
            return $this->error('User cannot be found.', 404);
        }
    }
}

app/Http/Requests/Api/V1/BaseUserRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class BaseUserRequest extends FormRequest
{
    public function mappedAttributes(array $otherAttributes = []) {
        $attributeMap = array_merge([
            'data.attributes.name' => 'name',
            'data.attributes.email' => 'email',
            'data.attributes.isManager' => 'is_manager',
            'data.attributes.password' => 'password',
        ], $otherAttributes);

        $attributesToUpdate = [];
        foreach ($attributeMap as $key => $attribute) {
            if ($this->has($key)) {
                $value = $this->input($key);

                if ($attribute === 'password') {
                    $value = bcrypt($value);
                }

                $attributesToUpdate[$attribute] = $value;
            }
        }

        return $attributesToUpdate;
    }
}

app/Http/Requests/Api/V1/ReplaceUserRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class ReplaceUserRequest extends BaseUserRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data.attributes.name' => 'required|string',
            'data.attributes.email' => 'required|email',
            'data.attributes.isManager' => 'required|boolean',
            'data.attributes.password' => 'required|string',
        ];

        return $rules;
    }
}

app/Http/Requests/Api/V1/StoreUserRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends BaseUserRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'data.attributes.name' => 'required|string',
            'data.attributes.email' => 'required|email',
            'data.attributes.isManager' => 'required|boolean',
            'data.attributes.password' => 'required|string',
        ];
    }
}

app/Http/Requests/Api/V1/UpdateUserRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class UpdateUserRequest extends BaseUserRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'data.attributes.name' => 'sometimes|string',
            'data.attributes.email' => 'sometimes|email',
            'data.attributes.isManager' => 'sometimes|boolean',
            'data.attributes.password' => 'sometimes|string',
        ];
    }
}

app/Http/Resources/V1/UserResource.php

<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'type' => 'user',
            'id' => $this->id,
            'attributes' => [
                'name' => $this->name,
                'email' => $this->email,
                'isManager' => $this->is_manager,
                $this->mergeWhen($request->routeIs('customers.*'), [
                    'emailVerifiedAt' => $this->email_verified_at,
                    'createdAt' => $this->created_at,
                    'updatedAt' => $this->updated_at,
                ])
            ],
            'includes' => OrderResource::collection($this->whenLoaded('orders')),
            'links' => [
                'self' => route('customers.show', ['customer' => $this->id])
            ]
        ];
    }
}

app/Models/User.php

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;

use App\Http\Filters\V1\QueryFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'is_manager'
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
        'is_manager' => 'boolean'
    ];

    public function orders() : HasMany {
        return $this->hasMany(Order::class);
    }

    public function scopeFilter(Builder $builder, QueryFilter $filters) {
        return $filters->apply($builder);
    }
}

app/Policies/V1/UserPolicy.php

<?php

namespace App\Policies\V1;

use App\Models\Order;
use App\Models\User;
use App\Permissions\V1\Abilities;

class UserPolicy
{
    /**
     * Create a new policy instance.
     */
    public function __construct()
    {
        //
    }

    public function delete(User $user, User $model) {
        return $user->tokenCan(Abilities::DeleteUser);
    }

    public function replace(User $user, User $model) {
        return $user->tokenCan(Abilities::ReplaceUser);
    }

    public function store(User $user) {
        return $user->tokenCan(Abilities::CreateUser);
    }

    public function update(User $user, User $model) {
        return $user->tokenCan(Abilities::UpdateUser);
    }
 }

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use App\Models\Order;
use App\Models\User;
use App\Policies\V1\OrderPolicy;
use App\Policies\V1\UserPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        //
    }

    public function boot(): void
    {
        Gate::policy(Order::class, OrderPolicy::class);
        Gate::policy(User::class, UserPolicy::class);
    }
}

routes/api_v1.php

<?php

use App\Http\Controllers\Api\V1\OrderController;
use App\Http\Controllers\Api\V1\CustomersController;
use App\Http\Controllers\Api\V1\CustomerOrdersController;
use App\Http\Controllers\Api\V1\UserController;
use App\Http\Controllers\AuthController;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are registered by bootstrap/app.php and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

// http://localhost:8000/api/
// universal resource locator
// orders
// users

Route::middleware('auth:sanctum')->group(function() {
    Route::apiResource('orders', OrderController::class)->except(['update']);
    Route::put('orders/{order}', [OrderController::class, 'replace']);
    Route::patch('orders/{order}', [OrderController::class, 'update']);

    Route::apiResource('users', UserController::class)->except(['update']);
    Route::put('users/{user}', [UserController::class, 'replace']);
    Route::patch('users/{user}', [UserController::class, 'update']);

    Route::apiResource('customers', CustomersController::class)->except(['store','update','destroy']);
    Route::apiResource('customers.orders', CustomerOrdersController::class)->except(['update']);
    Route::put('customers/{customer}/orders/{order}', [CustomerOrdersController::class, 'replace']);
    Route::patch('customers/{customer}/orders/{order}', [CustomerOrdersController::class, 'update']);

    Route::get('/user', function (Request $request) {
        return $request->user();
    });
});

22. Applying the Principle of Least Privilege

Tighten validation and controller behavior so every role can change only the data it genuinely needs.

Implementation files

  • app/Http/Controllers/Api/V1/ApiController.php
  • app/Http/Controllers/Api/V1/CustomerOrdersController.php
  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Controllers/Api/V1/UserController.php
  • app/Http/Requests/Api/V1/StoreOrderRequest.php
  • app/Http/Requests/Api/V1/UpdateOrderRequest.php
  • app/Permissions/V1/Abilities.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/V1/ApiController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Traits\ApiResponses;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request;

class ApiController extends Controller
{
    use ApiResponses;

    protected $policyClass;

    public function include(string $relationship) : bool {
        $param = request()->get('include');

        if (!isset($param)) {
            return false;
        }

        $includeValues = explode(',', strtolower($param));

        return in_array(strtolower($relationship), $includeValues);
    }

    public function isAble($ability, $targetModel) {
        try {
            $this->authorize($ability, [$targetModel, $this->policyClass]);
            return true;
        } catch (AuthorizationException $ex) {
            return false;
        }
    }
}

app/Http/Controllers/Api/V1/CustomerOrdersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\Order;
use App\Policies\V1\OrderPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;

class CustomerOrdersController extends ApiController
{
    protected $policyClass = OrderPolicy::class;

    public function index($customer_id, OrderFilter $filters) {
        return OrderResource::collection(
            Order::where('user_id', $customer_id)->filter($filters)->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request, $customer_id)
    {
        if ($this->isAble('store', Order::class)) {
            return new OrderResource(Order::create($request->mappedAttributes([
                'customer' => 'user_id'
            ])));
        }

        return $this->error('You are not authorized to create that resource', 403);
    }

    public function replace(ReplaceOrderRequest $request, $customer_id,  $order_id) {
        // PUT
        try {
            $order = Order::where('id', $order_id)
                            ->where('user_id', $customer_id)
                            ->firstOrFail();

            if ($this->isAble('replace', $order)) {
                $order->update($request->mappedAttributes());
                return new OrderResource($order);
            }
                
            return $this->error('You are not authorized to update that resource', 403);
            
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    public function update(UpdateOrderRequest $request, $customer_id,  $order_id) {
        // PUT
        try {
            $order = Order::where('id', $order_id)
                            ->where('user_id', $customer_id)
                            ->firstOrFail();

            if ($this->isAble('update', $order)) {
                $order->update($request->mappedAttributes());
                return new OrderResource($order);    
            }

            return $this->error('You are not authorized to update that resource', 403);

        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($customer_id, $order_id)
    {
        try {
            $order = Order::where('id', $order_id)
                            ->where('user_id', $customer_id)
                            ->firstOrFail();

            if ($this->isAble('delete', $order)) {
                $order->delete();
                return $this->ok('Order successfully deleted');       
            }

            return $this->error('You are not authorized to delete that resource', 403);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\User;
use App\Policies\V1\OrderPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class OrderController extends ApiController
{
    protected $policyClass = OrderPolicy::class;
    /**
     * Display a listing of the resource.
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        if ($this->isAble('store', Order::class)) {
            return new OrderResource(Order::create($request->mappedAttributes()));
        }

        return $this->error('You are not authorized to create that resource', 403);        
    }

    /**
     * Display the specified resource.
     */
    public function show($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            if ($this->include('customer')) {
                return new OrderResource($order->load('customer'));
            }

            return new OrderResource($order);
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, $order_id)
    {
        // PATCH
        try {
            $order = Order::findOrFail($order_id);

            if ($this->isAble('update', $order)) {
                $order->update($request->mappedAttributes());
    
                return new OrderResource($order);
            }

            return $this->error('You are not authorized to update that resource', 403);

        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    public function replace(ReplaceOrderRequest $request, $order_id) {
        // PUT
        try {
            $order = Order::findOrFail($order_id);

            // policy
            if ($this->isAble('replace', $order)) {
                $order->update($request->mappedAttributes());
                return new OrderResource($order);
            }

            return $this->error('You are not authorized to update that resource', 403);


            
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($order_id)
    {
        try {
            $order = Order::findOrFail($order_id);

            // policy
            if ($this->isAble('delete', $order)) {
                $order->delete();

                return $this->ok('Order successfully deleted');
            }

            return $this->error('You are not authorized to delete that resource', 403);

        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }
}

app/Http/Controllers/Api/V1/UserController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\CustomerFilter;
use App\Http\Requests\Api\V1\ReplaceUserRequest;
use App\Models\User;
use App\Http\Requests\Api\V1\StoreUserRequest;
use App\Http\Requests\Api\V1\UpdateUserRequest;
use App\Http\Resources\V1\UserResource;
use App\Policies\V1\UserPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class UserController extends ApiController
{
    protected $policyClass = UserPolicy::class;
    /**
     * Display a listing of the resource.
     */
    public function index(CustomerFilter $filters)
    {
        return UserResource::collection(
            User::filter($filters)->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreUserRequest $request)
    {
        if ($this->isAble('store', User::class)) {
            return new UserResource(User::create($request->mappedAttributes()));
        }

        return $this->error('You are not authorized to create that resource', 403);
    }

    /**
     * Display the specified resource.
     */
    public function show(User $user)
    {
        if ($this->include('orders')) {
            return new UserResource($user->load('orders'));
        }

        return new UserResource($user);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateUserRequest $request, $user_id)
    {
        try {
            $user = User::findOrFail($user_id);

            if ($this->isAble('update', $user)) {
                $user->update($request->mappedAttributes());
    
                return new UserResource($user);
            }
            
            return $this->error('You are not authorized to update that resource', 403);
            
        } catch (ModelNotFoundException $exception) {
            return $this->error('Order cannot be found.', 404);
        }
    }

    public function replace(ReplaceUserRequest $request, $user_id) {
        // PUT
        try {
            $user = User::findOrFail($user_id);

            if ($this->isAble('replace', $user)) {
                $user->update($request->mappedAttributes());

                return new UserResource($user);
            }

            return $this->error('You are not authorized to update that resource', 403);

        } catch (ModelNotFoundException $exception) {
            return $this->error('User cannot be found.', 404);
        }
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy($user_id)
    {
        try {
            $user = User::findOrFail($user_id);

            if ($this->isAble('delete', $user)) {
                $user->delete();

                return $this->ok('User successfully deleted');
            }

            return $this->error('You are not authorized to delete that resource', 403);

        } catch (ModelNotFoundException $exception) {
            return $this->error('User cannot be found.', 404);
        }
    }
}

app/Http/Requests/Api/V1/StoreOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use App\Permissions\V1\Abilities;
use Illuminate\Foundation\Http\FormRequest;

class StoreOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $customerIdAttr = $this->routeIs('orders.store') ? 'data.relationships.customer.data.id' : 'customer';
        $user = $this->user();
        $customerRule = 'required|integer|exists:users,id';

        $rules = [
            'data.attributes.reference' => 'required|string',
            'data.attributes.notes' => 'required|string',
            'data.attributes.status' => 'required|string|in:P,A,S,C',
            $customerIdAttr => $customerRule . '|size:' . $user->id
        ];

        if ($user->tokenCan(Abilities::CreateOrder)) {
            $rules[$customerIdAttr] = $customerRule;
        }
        
        return $rules;
    }

    protected function prepareForValidation() {
        if ($this->routeIs('customers.orders.store')) {
            $this->merge([
                'customer' => $this->route('customer')
            ]);
        }
    }
}

app/Http/Requests/Api/V1/UpdateOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use App\Permissions\V1\Abilities;
use Illuminate\Foundation\Http\FormRequest;

class UpdateOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data.attributes.reference' => 'sometimes|string',
            'data.attributes.notes' => 'sometimes|string',
            'data.attributes.status' => 'sometimes|string|in:P,A,S,C',
            'data.relationships.customer.data.id' => 'prohibited',
        ];

        if ($this->user()->tokenCan(Abilities::UpdateOrder)) {
            $rules['data.relationships.customer.data.id'] = 'sometimes|integer';
        }

        return $rules;
    }
}

app/Permissions/V1/Abilities.php

<?php

namespace App\Permissions\V1;

use App\Models\User;

final class Abilities {
    public const CreateOrder = 'order:create';
    public const UpdateOrder = 'order:update';
    public const ReplaceOrder = 'order:replace';
    public const DeleteOrder = 'order:delete';

    public const CreateOwnOrder = 'order:own:create';
    public const UpdateOwnOrder = 'order:own:update';
    public const DeleteOwnOrder = 'order:own:delete';

    public const CreateUser = 'user:create';
    public const UpdateUser = 'user:update';
    public const ReplaceUser = 'user:replace';
    public const DeleteUser = 'user:delete';

    public static function getAbilities(User $user) {
        if ($user->is_manager) {
            return [
                self::CreateOrder,
                self::UpdateOrder,
                self::ReplaceOrder,
                self::DeleteOrder,
                self::CreateUser,
                self::UpdateUser,
                self::ReplaceUser,
                self::DeleteUser,
            ];
        } else {
            return [
                self::CreateOwnOrder,
                self::UpdateOwnOrder,
                self::DeleteOwnOrder
            ];
        }
    }
    

}

23. Consistent API Error Handling

Normalize exceptions and validation failures into the same predictable JSON error envelope.

Implementation files

  • bootstrap/app.php
  • app/Http/Controllers/Api/V1/ApiController.php
  • app/Http/Controllers/Api/V1/CustomerOrdersController.php
  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Controllers/Api/V1/UserController.php
  • app/Traits/ApiResponses.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

bootstrap/app.php

<?php

use App\Traits\ApiResponses;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        $exceptions->shouldRenderJsonWhen(
            fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
        );

        $exceptions->render(function (ValidationException $exception, Request $request) {
            if (! $request->is('api/*')) {
                return null;
            }

            $errors = collect($exception->errors())
                ->flatMap(fn (array $messages, string $field) =>
                    collect($messages)->map(fn (string $message) => [
                        'status' => 422,
                        'message' => $message,
                        'source' => $field,
                    ])
                )->values()->all();

            return response()->json(['errors' => $errors], 422);
        });

        $exceptions->render(function (ModelNotFoundException $exception, Request $request) {
            if (! $request->is('api/*')) {
                return null;
            }

            return response()->json(['errors' => [[
                'status' => 404,
                'message' => 'The resource cannot be found.',
                'source' => $exception->getModel(),
            ]]], 404);
        });

        $exceptions->render(function (AuthenticationException $exception, Request $request) {
            if (! $request->is('api/*')) {
                return null;
            }

            return response()->json(['errors' => [[
                'status' => 401,
                'message' => 'Unauthenticated.',
                'source' => '',
            ]]], 401);
        });
    })->create();

app/Http/Controllers/Api/V1/ApiController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Traits\ApiResponses;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;

class ApiController extends Controller
{
    use ApiResponses;

    protected $policyClass;

    public function include(string $relationship) : bool {
        $param = request()->get('include');

        if (!isset($param)) {
            return false;
        }

        $includeValues = explode(',', strtolower($param));

        return in_array(strtolower($relationship), $includeValues);
    }

    public function isAble($ability, $targetModel) {
        try {
            $this->authorize($ability, [$targetModel, $this->policyClass]);
            return true;
        } catch (AuthorizationException $ex) {
            return false;
        }
    }
}

app/Http/Controllers/Api/V1/CustomerOrdersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\Order;
use App\Models\User;
use App\Policies\V1\OrderPolicy;

class CustomerOrdersController extends ApiController
{
    protected $policyClass = OrderPolicy::class;

    public function index(User $customer, OrderFilter $filters) {
        return OrderResource::collection(
            Order::where('user_id', $customer->id)->filter($filters)->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request, User $customer)
    {
        if ($this->isAble('store', Order::class)) {
            return new OrderResource(Order::create($request->mappedAttributes([
                'customer' => 'user_id'
            ])));
        }

        return $this->notAuthorized('You are not authorized to create that resource');
    }

    public function replace(ReplaceOrderRequest $request, User $customer, Order $order) {
        // PUT
        if ($this->isAble('replace', $order)) {
            $order->update($request->mappedAttributes());
            return new OrderResource($order);
        }
            
        return $this->notAuthorized('You are not authorized to update that resource');
            
    }

    public function update(UpdateOrderRequest $request, User $customer, Order $order) {
        // PUT
        if ($this->isAble('update', $order)) {
            $order->update($request->mappedAttributes());
            return new OrderResource($order);    
        }

        return $this->notAuthorized('You are not authorized to update that resource');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(User $customer, Order $order)
    {
        if ($this->isAble('delete', $order)) {
            $order->delete();
            return $this->ok('Order successfully deleted');       
        }

        return $this->notAuthorized('You are not authorized to delete that resource');
    }
}

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\User;
use App\Policies\V1\OrderPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class OrderController extends ApiController
{
    protected $policyClass = OrderPolicy::class;
    /**
     * Display a listing of the resource.
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreOrderRequest $request)
    {
        if ($this->isAble('store', Order::class)) {
            return new OrderResource(Order::create($request->mappedAttributes()));
        }

        return $this->notAuthorized('You are not authorized to create that resource');        
    }

    /**
     * Display the specified resource.
     */
    public function show(Order $order)
    {
        if ($this->include('customer')) {
            return new OrderResource($order->load('customer'));
        }

        return new OrderResource($order);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateOrderRequest $request, Order $order)
    {
        // PATCH
        

        if ($this->isAble('update', $order)) {
            $order->update($request->mappedAttributes());

            return new OrderResource($order);
        }

        return $this->notAuthorized('You are not authorized to update that resource');


    }

    public function replace(ReplaceOrderRequest $request, Order $order) {
        // PUT

        if ($this->isAble('replace', $order)) {
            $order->update($request->mappedAttributes());
            return new OrderResource($order);
        }

        return $this->notAuthorized('You are not authorized to update that resource');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Order $order)
    {
        // policy
        if ($this->isAble('delete', $order)) {
            $order->delete();

            return $this->ok('Order successfully deleted');
        }

        return $this->notAuthorized('You are not authorized to delete that resource');
    }
}

app/Http/Controllers/Api/V1/UserController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\CustomerFilter;
use App\Http\Requests\Api\V1\ReplaceUserRequest;
use App\Models\User;
use App\Http\Requests\Api\V1\StoreUserRequest;
use App\Http\Requests\Api\V1\UpdateUserRequest;
use App\Http\Resources\V1\UserResource;
use App\Policies\V1\UserPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class UserController extends ApiController
{
    protected $policyClass = UserPolicy::class;
    /**
     * Display a listing of the resource.
     */
    public function index(CustomerFilter $filters)
    {
        return UserResource::collection(
            User::filter($filters)->paginate()
        );
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreUserRequest $request)
    {
        if ($this->isAble('store', User::class)) {
            return new UserResource(User::create($request->mappedAttributes()));
        }

        return $this->notAuthorized('You are not authorized to create that resource');
    }

    /**
     * Display the specified resource.
     */
    public function show(User $user)
    {
        if ($this->include('orders')) {
            return new UserResource($user->load('orders'));
        }

        return new UserResource($user);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateUserRequest $request, User $user)
    {
        if ($this->isAble('update', $user)) {
            $user->update($request->mappedAttributes());

            return new UserResource($user);
        }
        
        return $this->notAuthorized('You are not authorized to update that resource');
    }

    public function replace(ReplaceUserRequest $request, User $user) {
        // PUT
        if ($this->isAble('replace', $user)) {
            $user->update($request->mappedAttributes());

            return new UserResource($user);
        }

        return $this->notAuthorized('You are not authorized to update that resource');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(User $user)
    {
        if ($this->isAble('delete', $user)) {
            $user->delete();

            return $this->ok('User successfully deleted');
        }

        return $this->notAuthorized('You are not authorized to delete that resource');
    }
}

app/Traits/ApiResponses.php

<?php

namespace App\Traits;

trait ApiResponses {
    protected function ok($message, $data = []) {
        return $this->success($message, $data, 200);
    }

    protected function success($message, $data = [], $statusCode = 200) {
        return response()->json([
            'data' => $data,
            'message' => $message,
            'status' => $statusCode
        ], $statusCode);
    }

    protected function error($errors = [], $statusCode = null) {
        if (is_string($errors)) {
            return response()->json([
                'message' => $errors,
                'status' => $statusCode
            ], $statusCode);
        }

        return response()->json([
            'errors' => $errors
        ], $statusCode);
    }

    protected function notAuthorized($message) {
        return $this->error([[
            'status' => 403,
            'message' => $message,
            'source' => ''
        ]], 403);
    }
}

24. Generating API Documentation with Scribe

Install Scribe, annotate endpoints, and generate interactive API documentation plus OpenAPI and Postman artifacts.

Implementation files

  • app/Http/Controllers/Api/AuthController.php
  • app/Http/Controllers/Api/V1/CustomerOrdersController.php
  • app/Http/Controllers/Api/V1/CustomersController.php
  • app/Http/Controllers/Api/V1/OrderController.php
  • app/Http/Controllers/Api/V1/UserController.php
  • app/Http/Requests/Api/V1/ReplaceOrderRequest.php
  • app/Http/Requests/Api/V1/ReplaceUserRequest.php
  • app/Http/Requests/Api/V1/StoreOrderRequest.php
  • app/Http/Requests/Api/V1/StoreUserRequest.php
  • app/Http/Requests/Api/V1/UpdateOrderRequest.php
  • composer.json
  • config/scribe.php

The following sections contain complete file contents, not abbreviated diffs. Unchanged framework-generated files are intentionally not repeated.

Complete code

app/Http/Controllers/Api/AuthController.php

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\LoginUserRequest;
use App\Models\User;
use App\Permissions\V1\Abilities;
use App\Traits\ApiResponses;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    use ApiResponses;


    /**
     * Login
     * 
     * Authenticates the user and returns the user's API token.
     * 
     * @unauthenticated
     * @group Authentication
     * @response 200 {
    "data": {
        "token": "{YOUR_AUTH_KEY}"
    },
    "message": "Authenticated",
    "status": 200
}
     */
    public function login(LoginUserRequest $request) {
        $request->validated($request->all());

        if (!Auth::attempt($request->only('email', 'password'))) {
            return $this->error('Invalid credentials', 401);
        }

        $user = User::firstWhere('email', $request->email);

        return $this->ok(
            'Authenticated',
            [
                'token' => $user->createToken(
                    'API token for ' . $user->email,
                    Abilities::getAbilities($user),
                    now()->addMonth())->plainTextToken
            ]
            );
    }

    /**
     * Logout
     * 
     * Signs out the user and destroy's the API token.
     * 
     * @group Authentication
     * @response 200 {}
     */
    public function logout(Request $request) {
        $request->user()->currentAccessToken()->delete();

        return $this->ok('');
    }
}

app/Http/Controllers/Api/V1/CustomerOrdersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\Order;
use App\Models\User;
use App\Policies\V1\OrderPolicy;

class CustomerOrdersController extends ApiController
{
    protected $policyClass = OrderPolicy::class;

    /**
     * Get all orders
     * 
     * Retrieves all orders created by a specific user.
     * 
     * @group Managing Orders by Customer
     * 
     * @urlParam customer_id integer required The customer's ID. No-example
     * 
     * @response 200 {"data":[{"type":"user","id":3,"attributes":{"name":"Mr. Henri Beatty MD","email":"bmertz@example.net","isManager":false,"emailVerifiedAt":"2024-03-14T04:41:51.000000Z","createdAt":"2024-03-14T04:41:51.000000Z","updatedAt":"2024-03-14T04:41:51.000000Z"},"links":{"self":"http:\/\/localhost:8000\/api\/v1\/customers\/3"}}],"links":{"first":"http:\/\/localhost:8000\/api\/v1\/customers?page=1","last":"http:\/\/localhost:8000\/api\/v1\/customers?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[{"url":null,"label":"&laquo; Previous","active":false},{"url":"http:\/\/localhost:8000\/api\/v1\/customers?page=1","label":"1","active":true},{"url":null,"label":"Next &raquo;","active":false}],"path":"http:\/\/localhost:8000\/api\/v1\/customers","per_page":15,"to":1,"total":10}}
     *
     * @queryParam sort string Data field(s) to sort by. Separate multiple fields with commas. Denote descending sort with a minus sign. Example: sort=name
     * @queryParam filter[name] Filter by name. Wildcards are supported. 
     * @queryParam filter[email] Filter by email. Wildcards are supported.
     */
    public function index(User $customer, OrderFilter $filters) {
        return OrderResource::collection(
            Order::where('user_id', $customer->id)->filter($filters)->paginate()
        );
    }

    /**
     * Create a order
     * 
     * Creates a order for the specific user.
     * 
     * @group Managing Orders by Customer
     * 
     * @urlParam customer_id integer required The customer's ID. No-example
     * 
     */
    public function store(StoreOrderRequest $request, User $customer)
    {
        if ($this->isAble('store', Order::class)) {
            return new OrderResource(Order::create($request->mappedAttributes([
                'customer' => 'user_id'
            ])));
        }

        return $this->notAuthorized('You are not authorized to create that resource');
    }

    /**
     * Replace an customer's order
     * 
     * Replaces an customer's order.
     * 
     * @group Managing Orders by Customer
     * @urlParam customer_id integer required The customer's ID. No-example
     * @urlParam order_id integer required The order ID. No-example
     * @response {"data":{"type":"order","id":107,"attributes":{"reference":"ORD-10432","notes":"Priority delivery","status":"A","createdAt":"2024-03-26T04:40:48.000000Z","updatedAt":"2024-03-26T04:40:48.000000Z"},"relationships":{"customer":{"data":{"type":"user","id":1},"links":{"self":"http:\/\/localhost:8000\/api\/v1\/customers\/1"}}},"links":{"self":"http:\/\/localhost:8000\/api\/v1\/orders\/107"}}}
     */
    public function replace(ReplaceOrderRequest $request, User $customer, Order $order) {
        // PUT
        if ($this->isAble('replace', $order)) {
            $order->update($request->mappedAttributes());
            return new OrderResource($order);
        }
            
        return $this->notAuthorized('You are not authorized to update that resource');
            
    }

    /**
     * Update an customer's order
     * 
     * Updates an customer's order.
     * 
     * @group Managing Orders by Customer
     * @urlParam customer_id integer required The customer's ID. No-example
     * @urlParam order_id integer required The order ID. No-example
     */
    public function update(UpdateOrderRequest $request, User $customer, Order $order) {
        // PUT
        if ($this->isAble('update', $order)) {
            $order->update($request->mappedAttributes());
            return new OrderResource($order);    
        }

        return $this->notAuthorized('You are not authorized to update that resource');
    }

    /**
     * Delete an customer's order
     * 
     * Deletes an customer's order.
     * 
     * @group Managing Orders by Customer
     * @urlParam customer_id integer required The customer's ID. No-example
     * @urlParam id integer required The order ID. No-example
     * @response {}
     */    public function destroy(User $customer, Order $order)
    {
        if ($this->isAble('delete', $order)) {
            $order->delete();
            return $this->ok('Order successfully deleted');       
        }

        return $this->notAuthorized('You are not authorized to delete that resource');
    }
}

app/Http/Controllers/Api/V1/CustomersController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\CustomerFilter;
use App\Models\User;
use App\Http\Requests\Api\V1\StoreUserRequest;
use App\Http\Requests\Api\V1\UpdateUserRequest;
use App\Http\Resources\V1\UserResource;

class CustomersController extends ApiController
{
    /**
     * Get customers.
     * 
     * Retrieves all users that created a order.
     * 
     * @group Showing Customers
     */
    public function index(CustomerFilter $filters)
    {
        return UserResource::collection(
            User::select('users.*')
                ->join('orders', 'users.id', '=', 'orders.user_id')
                ->filter($filters)
                ->distinct()
                ->paginate()
        );
    }

    /**
     * Get an customer.
     * 
     * Retrieves all users that created a order.
     * 
     * @group Showing Customers
     */
    public function show(User $customer)
    {
        if ($this->include('orders')) {
            return new UserResource($customer->load('orders'));
        }

        return new UserResource($customer);
    }
}

app/Http/Controllers/Api/V1/OrderController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\OrderFilter;
use App\Http\Requests\Api\V1\ReplaceOrderRequest;
use App\Models\Order;
use App\Http\Requests\Api\V1\StoreOrderRequest;
use App\Http\Requests\Api\V1\UpdateOrderRequest;
use App\Http\Resources\V1\OrderResource;
use App\Models\User;
use App\Policies\V1\OrderPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class OrderController extends ApiController
{
    protected $policyClass = OrderPolicy::class;
    /**
     * Get all orders
     * 
     * @group Managing Orders
     * @queryParam sort string Data field(s) to sort by. Separate multiple fields with commas. Denote descending sort with a minus sign. Example: sort=reference,-createdAt
     * @queryParam filter[status] Filter by status code: P, A, S, C. No-example
     * @queryParam filter[reference] Filter by reference. Wildcards are supported. Example: *ORD-1*
     */
    public function index(OrderFilter $filters)
    {
        return OrderResource::collection(Order::filter($filters)->paginate());
    }

    /**
     * Create a order
     * 
     * Creates a new order record. Users can only create orders for themselves. Managers can create orders for any user.
     * 
     * @group Managing Orders
     * 
     * @response {"data":{"type":"order","id":107,"attributes":{"reference":"ORD-10432","notes":"Priority delivery","status":"A","createdAt":"2024-03-26T04:40:48.000000Z","updatedAt":"2024-03-26T04:40:48.000000Z"},"relationships":{"customer":{"data":{"type":"user","id":1},"links":{"self":"http:\/\/localhost:8000\/api\/v1\/customers\/1"}}},"links":{"self":"http:\/\/localhost:8000\/api\/v1\/orders\/107"}}}
     */
    public function store(StoreOrderRequest $request)
    {
        if ($this->isAble('store', Order::class)) {
            return new OrderResource(Order::create($request->mappedAttributes()));
        }

        return $this->notAuthorized('You are not authorized to create that resource');        
    }

    /**
     * Show a specific order.
     * 
     * Display an individual order.
     * 
     * @group Managing Orders
     * 
     */
    public function show(Order $order)
    {
        if ($this->include('customer')) {
            return new OrderResource($order->load('customer'));
        }

        return new OrderResource($order);
    }

    /**
     * Update Order
     * 
     * Update the specified order in storage.
     * 
     * @group Managing Orders
     * 
     */
    public function update(UpdateOrderRequest $request, Order $order)
    {
        // PATCH
        

        if ($this->isAble('update', $order)) {
            $order->update($request->mappedAttributes());

            return new OrderResource($order);
        }

        return $this->notAuthorized('You are not authorized to update that resource');


    }

    /**
     * Replace Order
     * 
     * Replace the specified order in storage.
     * 
     * @group Managing Orders
     * 
     */
    public function replace(ReplaceOrderRequest $request, Order $order) {
        // PUT

        if ($this->isAble('replace', $order)) {
            $order->update($request->mappedAttributes());
            return new OrderResource($order);
        }

        return $this->notAuthorized('You are not authorized to update that resource');
    }

    /**
     * Delete order.
     * 
     * Remove the specified resource from storage.
     * 
     * @group Managing Orders
     * 
     */
    public function destroy(Order $order)
    {
        // policy
        if ($this->isAble('delete', $order)) {
            $order->delete();

            return $this->ok('Order successfully deleted');
        }

        return $this->notAuthorized('You are not authorized to delete that resource');
    }
}

app/Http/Controllers/Api/V1/UserController.php

<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Filters\V1\CustomerFilter;
use App\Http\Requests\Api\V1\ReplaceUserRequest;
use App\Models\User;
use App\Http\Requests\Api\V1\StoreUserRequest;
use App\Http\Requests\Api\V1\UpdateUserRequest;
use App\Http\Resources\V1\UserResource;
use App\Policies\V1\UserPolicy;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class UserController extends ApiController
{
    protected $policyClass = UserPolicy::class;
    /**
     * Get all users
     * 
     * @group Managing Users
     * 
     * @queryParam sort string Data field(s) to sort by. Separate multiple fields with commas. Denote descending sort with a minus sign. Example: sort=name
     * @queryParam filter[name] Filter by status name. Wildcards are supported. No-example
     * @queryParam filter[email] Filter by email. Wildcards are supported. No-example
     * 
     */
    public function index(CustomerFilter $filters)
    {
        return UserResource::collection(
            User::filter($filters)->paginate()
        );
    }

    /**
     * Create a user
     * 
     * @group Managing Users
     * 
     * @response 200 {"data":{"type":"user","id":16,"attributes":{"name":"My User","email":"user@user.com","isManager":false},"links":{"self":"http:\/\/localhost:8000\/api\/v1\/customers\/16"}}}
     */
    public function store(StoreUserRequest $request)
    {
        if ($this->isAble('store', User::class)) {
            return new UserResource(User::create($request->mappedAttributes()));
        }

        return $this->notAuthorized('You are not authorized to create that resource');
    }

     /**
     * Display a user
     * 
     * @group Managing Users
     * 
     * 
     */
    public function show(User $user)
    {
        if ($this->include('orders')) {
            return new UserResource($user->load('orders'));
        }

        return new UserResource($user);
    }

     /**
     * Update a user
     * 
     * @group Managing Users
     * 
     * @response 200 {"data":{"type":"user","id":16,"attributes":{"name":"My User","email":"user@user.com","isManager":false},"links":{"self":"http:\/\/localhost:8000\/api\/v1\/customers\/16"}}}
     */
    public function update(UpdateUserRequest $request, User $user)
    {
        if ($this->isAble('update', $user)) {
            $user->update($request->mappedAttributes());

            return new UserResource($user);
        }
        
        return $this->notAuthorized('You are not authorized to update that resource');
    }

     /**
     * Replace a user
     * 
     * @group Managing Users
     * 
     * @response 200 {"data":{"type":"user","id":16,"attributes":{"name":"My User","email":"user@user.com","isManager":false},"links":{"self":"http:\/\/localhost:8000\/api\/v1\/customers\/16"}}}
     */
    public function replace(ReplaceUserRequest $request, User $user) {
        // PUT
        if ($this->isAble('replace', $user)) {
            $user->update($request->mappedAttributes());

            return new UserResource($user);
        }

        return $this->notAuthorized('You are not authorized to update that resource');
    }

     /**
     * Delete a user
     * 
     * @group Managing Users
     * 
     * @response 200 {}
     */
    public function destroy(User $user)
    {
        if ($this->isAble('delete', $user)) {
            $user->delete();

            return $this->ok('User successfully deleted');
        }

        return $this->notAuthorized('You are not authorized to delete that resource');
    }
}

app/Http/Requests/Api/V1/ReplaceOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class ReplaceOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data' => 'required|array',
            'data.attributes' => 'required|array',
            'data.attributes.reference' => 'required|string',
            'data.attributes.notes' => 'required|string',
            'data.attributes.status' => 'required|string|in:P,A,S,C',
            'data.relationships' => 'required|array',
            'data.relationships.customer' => 'required|array',
            'data.relationships.customer.data' => 'required|array',
            'data.relationships.customer.data.id' => 'required|integer',
        ];

        return $rules;
    }
}

app/Http/Requests/Api/V1/ReplaceUserRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class ReplaceUserRequest extends BaseUserRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data' => 'required|array',
            'data.attributes' => 'required|array',
            'data.attributes.name' => 'required|string',
            'data.attributes.email' => 'required|email',
            'data.attributes.isManager' => 'required|boolean',
            'data.attributes.password' => 'required|string',
        ];

        return $rules;
    }
}

app/Http/Requests/Api/V1/StoreOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use App\Permissions\V1\Abilities;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;

class StoreOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $isOrdersController = $this->routeIs('orders.store');
        $customerIdAttr = $isOrdersController ? 'data.relationships.customer.data.id' : 'customer';
        $user = Auth::user();
        $customerRule = 'required|integer|exists:users,id';

        $rules = [
            'data' => 'required|array',
            'data.attributes' => 'required|array',
            'data.attributes.reference' => 'required|string',
            'data.attributes.notes' => 'required|string',
            'data.attributes.status' => 'required|string|in:P,A,S,C',
        ];

        if ($isOrdersController) {
            $rules['data.relationships'] = 'required|array';
            $rules['data.relationships.customer'] = 'required|array';
            $rules['data.relationships.customer.data'] = 'required|array';
        }

        $rules[$customerIdAttr] = $customerRule . '|size:' . $user->id;

        if ($user->tokenCan(Abilities::CreateOrder)) {
            $rules[$customerIdAttr] = $customerRule;
        }
        
        return $rules;
    }

    protected function prepareForValidation() {
        if ($this->routeIs('customers.orders.store')) {
            $this->merge([
                'customer' => $this->route('customer')
            ]);
        }
    }

    public function bodyParameters() {
        $documentation = [
            'data.attributes.reference' => [
                'description' => "The order's reference",
                'example' => 'No-example'
            ],
            'data.attributes.notes' => [
                'description' => "The order's notes",
                'example' => 'No-example',
            ],
            'data.attributes.status' => [
                'description' => "The order's status",
                'example' => 'No-example',
            ],
        ];

        if ($this->routeIs('orders.store')) {
            $documentation['data.relationships.customer.data.id'] = [
                'description' => 'The customer the order belongs to.',
                'example' => 'No-example'
            ];
        } else {
            $documentation['customer'] = [
                'description' => 'The customer the order belongs to.',
                'example' => 'No-example'
            ];
        }

        return $documentation;

    }
}

app/Http/Requests/Api/V1/StoreUserRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends BaseUserRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'data' => 'required|array',
            'data.attributes' => 'required|array',
            'data.attributes.name' => 'required|string',
            'data.attributes.email' => 'required|email',
            'data.attributes.isManager' => 'required|boolean',
            'data.attributes.password' => 'required|string',
        ];
    }
}

app/Http/Requests/Api/V1/UpdateOrderRequest.php

<?php

namespace App\Http\Requests\Api\V1;

use App\Permissions\V1\Abilities;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;

class UpdateOrderRequest extends BaseOrderRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $rules = [
            'data.attributes.reference' => 'sometimes|string',
            'data.attributes.notes' => 'sometimes|string',
            'data.attributes.status' => 'sometimes|string|in:P,A,S,C',
            'data.relationships.customer.data.id' => 'prohibited',
        ];

        if (Auth::user()->tokenCan(Abilities::UpdateOrder)) {
            $rules['data.relationships.customer.data.id'] = 'sometimes|integer';
        }

        return $rules;
    }
}

composer.json

{
    "$schema": "https://getcomposer.org/schema.json",
    "name": "laravel/laravel",
    "type": "project",
    "description": "A production-ready Laravel API.",
    "keywords": ["laravel", "api"],
    "license": "MIT",
    "require": {
        "php": "^8.3",
        "laravel/framework": "^13.0",
        "laravel/sanctum": "^4.3",
        "laravel/tinker": "^3.0"
    },
    "require-dev": {
        "fakerphp/faker": "^1.23",
        "knuckleswtf/scribe": "^5.0",
        "laravel/pail": "^1.2.5",
        "laravel/pint": "^1.27",
        "mockery/mockery": "^1.6",
        "nunomaduro/collision": "^8.6",
        "phpunit/phpunit": "^12.5"
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": [
            "@php artisan config:clear --ansi @no_additional_args",
            "@php artisan test"
        ],
        "post-autoload-dump": [
            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
            "@php artisan package:discover --ansi"
        ],
        "post-update-cmd": [
            "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
        ],
        "post-root-package-install": [
            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
        ],
        "post-create-project-cmd": [
            "@php artisan key:generate --ansi",
            "@php artisan migrate --graceful --ansi"
        ],
        "pre-package-uninstall": [
            "Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
        ]
    },
    "extra": {
        "laravel": {
            "dont-discover": []
        }
    },
    "config": {
        "optimize-autoloader": true,
        "preferred-install": "dist",
        "sort-packages": true,
        "allow-plugins": {
            "pestphp/pest-plugin": true,
            "php-http/discovery": true
        }
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}

config/scribe.php

<?php

use Knuckles\Scribe\Extracting\Strategies;

return [
    // The HTML <title> for the generated documentation. If this is empty, Scribe will infer it from config('app.name').
    'title' => 'Orders Hub API Documentation',

    // A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec.
    'description' => '',

    // The base URL displayed in the docs. If this is empty, Scribe will use the value of config('app.url') at generation time.
    // If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL.
    'base_url' => null,

    'routes' => [
        [
            // Routes that match these conditions will be included in the docs
            'match' => [
                // Match only routes whose paths match this pattern (use * as a wildcard to match any characters). Example: 'users/*'.
                'prefixes' => ['api/*'],

                // Match only routes whose domains match this pattern (use * as a wildcard to match any characters). Example: 'api.*'.
                'domains' => ['*'],
            ],

            // Include these routes even if they did not match the rules above.
            'include' => [
                // 'users.index', 'POST /new', '/auth/*'
            ],

            // Exclude these routes even if they matched the rules above.
            'exclude' => [
                // 'GET /health', 'admin.*'
            ],
        ],
    ],

    // The type of documentation output to generate.
    // - "static" will generate a static HTMl page in the /public/docs folder,
    // - "laravel" will generate the documentation as a Blade view, so you can add routing and authentication.
    // - "external_static" and "external_laravel" do the same as above, but generate a basic template,
    // passing the OpenAPI spec as a URL, allowing you to easily use the docs with an external generator
    'type' => 'static',

    // See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options
    'theme' => 'default',

    'static' => [
        // HTML documentation, assets and Postman collection will be generated to this folder.
        // Source Markdown will still be in resources/docs.
        'output_path' => 'public/docs',
    ],

    'laravel' => [
        // Whether to automatically create a docs endpoint for you to view your generated docs.
        // If this is false, you can still set up routing manually.
        'add_routes' => true,

        // URL path to use for the docs endpoint (if `add_routes` is true).
        // By default, `/docs` opens the HTML page, `/docs.postman` opens the Postman collection, and `/docs.openapi` the OpenAPI spec.
        'docs_url' => '/docs',

        // Directory within `public` in which to store CSS and JS assets.
        // By default, assets are stored in `public/vendor/scribe`.
        // If set, assets will be stored in `public/{{assets_directory}}`
        'assets_directory' => null,

        // Middleware to attach to the docs endpoint (if `add_routes` is true).
        'middleware' => [],
    ],

    'external' => [
        'html_attributes' => []
    ],

    'try_it_out' => [
        // Add a Try It Out button to your endpoints so consumers can test endpoints right from their browser.
        // Don't forget to enable CORS headers for your endpoints.
        'enabled' => true,

        // The base URL for the API tester to use (for example, you can set this to your staging URL).
        // Leave as null to use the current app URL when generating (config("app.url")).
        'base_url' => 'http://localhost:8000',

        // [Laravel Sanctum] Fetch a CSRF token before each request, and add it as an X-XSRF-TOKEN header.
        'use_csrf' => false,

        // The URL to fetch the CSRF token from (if `use_csrf` is true).
        'csrf_url' => '/sanctum/csrf-cookie',
    ],

    // How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls.
    'auth' => [
        // Set this to true if ANY endpoints in your API use authentication.
        'enabled' => true,

        // Set this to true if your API should be authenticated by default. If so, you must also set `enabled` (above) to true.
        // You can then use @unauthenticated or @authenticated on individual endpoints to change their status from the default.
        'default' => true,

        // Where is the auth value meant to be sent in a request?
        // Options: query, body, basic, bearer, header (for custom header)
        'in' => 'bearer',

        // The name of the auth parameter (eg token, key, apiKey) or header (eg Authorization, Api-Key).
        'name' => 'Authorization',

        // The value of the parameter to be used by Scribe to authenticate response calls.
        // This will NOT be included in the generated documentation. If empty, Scribe will use a random value.
        'use_value' => '1|EXAMPLE_TOKEN_REPLACE_ME',

        // Placeholder your users will see for the auth parameter in the example requests.
        // Set this to null if you want Scribe to use a random value as placeholder instead.
        'placeholder' => '{YOUR_AUTH_KEY}',

        // Any extra authentication-related info for your users. Markdown and HTML are supported.
        'extra_info' => 'You can retrieve your token by visiting your dashboard and clicking <b>Generate API token</b>.',
    ],

    // Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported.
    'intro_text' => <<<INTRO
This documentation aims to provide all the information you need to work with our API.

<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>
INTRO
    ,

    // Example requests for each endpoint will be shown in each of these languages.
    // Supported options are: bash, javascript, php, python
    // To add a language of your own, see https://scribe.knuckles.wtf/laravel/advanced/example-requests
    'example_languages' => [
        'bash',
        'javascript',
    ],

    // Generate a Postman collection (v2.1.0) in addition to HTML docs.
    // For 'static' docs, the collection will be generated to public/docs/collection.json.
    // For 'laravel' docs, it will be generated to storage/app/scribe/collection.json.
    // Setting `laravel.add_routes` to true (above) will also add a route for the collection.
    'postman' => [
        'enabled' => true,

        'overrides' => [
            // 'info.version' => '2.0.0',
        ],
    ],

    // Generate an OpenAPI spec (v3.0.1) in addition to docs webpage.
    // For 'static' docs, the collection will be generated to public/docs/openapi.yaml.
    // For 'laravel' docs, it will be generated to storage/app/scribe/openapi.yaml.
    // Setting `laravel.add_routes` to true (above) will also add a route for the spec.
    'openapi' => [
        'enabled' => true,

        'overrides' => [
            // 'info.version' => '2.0.0',
        ],
    ],

    'groups' => [
        // Endpoints which don't have a @group will be placed in this default group.
        'default' => 'Endpoints',

        // By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined.
        // You can override this by listing the groups, subgroups and endpoints here in the order you want them.
        // See https://scribe.knuckles.wtf/blog/laravel-v4#easier-sorting and https://scribe.knuckles.wtf/laravel/reference/config#order for details
        'order' => [],
    ],

    // Custom logo path. This will be used as the value of the src attribute for the <img> tag,
    // so make sure it points to an accessible URL or path. Set to false to not use a logo.
    // For example, if your logo is in public/img:
    // - 'logo' => '../img/logo.png' // for `static` type (output folder is public/docs)
    // - 'logo' => 'img/logo.png' // for `laravel` type
    'logo' => false,

    // Customize the "Last updated" value displayed in the docs by specifying tokens and formats.
    // Examples:
    // - {date:F j Y} => March 28, 2022
    // - {git:short} => Short hash of the last Git commit
    // Available tokens are `{date:<format>}` and `{git:<format>}`.
    // The format you pass to `date` will be passed to PHP's `date()` function.
    // The format you pass to `git` can be either "short" or "long".
    'last_updated' => 'Last updated: {date:F j, Y}',

    'examples' => [
        // Set this to any number (eg. 1234) to generate the same example values for parameters on each run,
        'faker_seed' => null,

        // With API resources and transformers, Scribe tries to generate example models to use in your API responses.
        // By default, Scribe will try the model's factory, and if that fails, try fetching the first from the database.
        // You can reorder or remove strategies here.
        'models_source' => ['factoryCreate', 'factoryMake', 'databaseFirst'],
    ],

    // The strategies Scribe will use to extract information about your routes at each stage.
    // If you create or install a custom strategy, add it here.
    'strategies' => [
        'metadata' => [
            Strategies\Metadata\GetFromDocBlocks::class,
            Strategies\Metadata\GetFromMetadataAttributes::class,
        ],
        'urlParameters' => [
            Strategies\UrlParameters\GetFromLaravelAPI::class,
            Strategies\UrlParameters\GetFromUrlParamAttribute::class,
            Strategies\UrlParameters\GetFromUrlParamTag::class,
        ],
        'queryParameters' => [
            Strategies\QueryParameters\GetFromFormRequest::class,
            Strategies\QueryParameters\GetFromInlineValidator::class,
            Strategies\QueryParameters\GetFromQueryParamAttribute::class,
            Strategies\QueryParameters\GetFromQueryParamTag::class,
        ],
        'headers' => [
            Strategies\Headers\GetFromHeaderAttribute::class,
            Strategies\Headers\GetFromHeaderTag::class,
            [
                'override',
                [
                    'Content-Type' => 'application/json',
                    'Accept' => 'application/json',
                ]
            ]
        ],
        'bodyParameters' => [
            Strategies\BodyParameters\GetFromFormRequest::class,
            Strategies\BodyParameters\GetFromInlineValidator::class,
            Strategies\BodyParameters\GetFromBodyParamAttribute::class,
            Strategies\BodyParameters\GetFromBodyParamTag::class,
        ],
        'responses' => [
            Strategies\Responses\UseResponseAttributes::class,
            Strategies\Responses\UseTransformerTags::class,
            Strategies\Responses\UseApiResourceTags::class,
            Strategies\Responses\UseResponseTag::class,
            Strategies\Responses\UseResponseFileTag::class,
            [
                Strategies\Responses\ResponseCalls::class,
                ['only' => ['GET *']]
            ]
        ],
        'responseFields' => [
            Strategies\ResponseFields\GetFromResponseFieldAttribute::class,
            Strategies\ResponseFields\GetFromResponseFieldTag::class,
        ],
    ],

    // For response calls, API resource responses and transformer responses,
    // Scribe will try to start database transactions, so no changes are persisted to your database.
    // Tell Scribe which connections should be transacted here. If you only use one db connection, you can leave this as is.
    'database_connections_to_transact' => [config('database.default')],

    'fractal' => [
        // If you are using a custom serializer with league/fractal, you can specify it here.
        'serializer' => null,
    ],

    'routeMatcher' => \Knuckles\Scribe\Matching\RouteMatcher::class,
];

#Laravel #Laravel API #REST API #Sanctum #API Resources #Policies #Scribe #PHP