العودة للمدونة
laravelنُشر في August 18, 2026 · 150 دقيقة قراءة

بناء API جاهز للإنتاج باستخدام Laravel 13: دليل احترافي

مرجع عملي متكامل باستخدام Laravel 13 لتصميم API متعدد الإصدارات وتأمينه وفلترته وتفويضه واختباره وتوثيقه.

بناء API جاهز للإنتاج باستخدام Laravel 13: دليل احترافي

بناء API جاهز للإنتاج باستخدام Laravel 13

يطور هذا الدليل الاحترافي Laravel API بدءاً من أول استجابة JSON، وصولاً إلى خدمة متعددة الإصدارات تدعم المصادقة والفلترة والتفويض والتوثيق. ينصب التركيز على القرارات والتنفيذ الكامل اللازمين لبناء عقد API مستقر، وليس على خصائص متفرقة في إطار العمل.

نُظم الدليل كمرجع تقني متدرج. يعالج كل قسم جانباً مهماً للإنتاج، ويعرض المحتوى الكامل لكل ملف تطبيق أُنشئ أو تغير في تلك المرحلة. يستخدم التنفيذ Laravel 13 وPHP 8.3 حتى تبقى جميع الأمثلة متوافقة مع بعضها.

ملاحظة Laravel 13: يتطلب Laravel 13 إصدار PHP 8.3 أو أحدث. يسجل هيكل التطبيق الحديث مسارات API ومعالجة الاستثناءات في bootstrap/app.php؛ لذلك استُبدلت أنماط RouteServiceProvider وAuthServiceProvider وapp/Exceptions/Handler.php القديمة في هذا الدليل.

المتطلبات والإعداد الأولي

  • PHP 8.3 أو أحدث، وComposer، وMySQL، وGit
  • معرفة أساسية بمسارات Laravel والمتحكمات وEloquent والترحيلات والتحقق
  • Postman أو أي عميل HTTP آخر
composer create-project laravel/laravel:^13.0 orders-hub
cd orders-hub
php artisan install:api
php artisan migrate

اضبط قاعدة بيانات MySQL باسم orders_hub في .env، ثم طبّق الملفات المعروضة قسماً بعد قسم.

يأتي Laravel 13 بمتحكم أساسي مصغر. ولأن الأقسام اللاحقة تستخدم دوال التفويض داخل المتحكمات، فعّل Traits المطلوبة من البداية:

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;
}

خارطة الدليل

  1. استجابات JSON موحدة وحالات HTTP صحيحة
  2. اختبار API باستخدام Postman
  3. تصميم عناوين قائمة على الموارد
  4. هيكلة API متعدد الإصدارات
  5. المصادقة بالرموز باستخدام Laravel Sanctum
  6. إلغاء الرموز وتسجيل الخروج الآمن
  7. تصميم استجابات مستقرة
  8. الحقول والعلاقات الشرطية
  9. تحميل العلاقات عند الطلب
  10. فلاتر استعلام قابلة لإعادة الاستخدام
  11. الموارد المتداخلة وفلترة العلاقات
  12. ترتيب آمن يتحكم به العميل
  13. إنشاء الموارد باستخدام POST
  14. حذف الموارد باستخدام DELETE
  15. الاستبدال الكامل باستخدام PUT
  16. التحديث الجزئي باستخدام PATCH
  17. تفويض الموارد باستخدام Policies
  18. التحكم في الوصول بصلاحيات Token
  19. صلاحيات دقيقة على مستوى الحقول
  20. إدارة طلبيات العملاء
  21. إدارة المستخدمين بأمان
  22. تطبيق مبدأ الحد الأدنى من الصلاحيات
  23. معالجة موحدة لأخطاء API
  24. إنشاء توثيق API باستخدام Scribe

التحقق من كل نقطة

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

أرسل دائماً Accept: application/json. للمسارات المحمية، سجّل الدخول أولاً ثم أرسل رمز Sanctum بالشكل Authorization: Bearer YOUR_TOKEN.

1. استجابات JSON موحدة وحالات HTTP صحيحة

في الأساس الأول لبناء API سنبدأ بتطبيق Laravel جديد، ثم نضيف مسار API بسيطاً يعيد استجابة JSON منظمة مع حالة HTTP الصحيحة 200 OK.

يستهدف التنفيذ Laravel 13 وPHP بإصدار ^8.3 وSanctum بإصدار ^4.3.

ما الذي سنبنيه؟

عند إرسال طلب GET /api/login سنحصل على:

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

محتوى الاستجابة بصيغة JSON، وحالة استجابة HTTP الفعلية هي أيضاً 200.

1. إنشاء المشروع

إذا أردت تنفيذ الدليل من مشروع نظيف، أنشئ تطبيق Laravel 13:

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

يستخدم المشروع المرجعي قاعدة بيانات MySQL باسم orders_hub. حدّث ملف .env ببيانات جهازك:

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

لا يتعامل هذا التنفيذ الأولي مع قاعدة البيانات بعد، ولذلك سيعمل المسار قبل الحاجة إلى تشغيل migrations.

2. إنشاء Trait للاستجابات

أنشئ المجلد والملف:

mkdir app/Traits

هذا هو محتوى الملف كاملاً.

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() اختصار لاستجابة ناجحة بحالة 200، وهي تستدعي success() التي تنشئ محتوى JSON وترسل رمز الحالة نفسه إلى Laravel.

المعامل الثاني في response()->json() مهم؛ فمن دونه قد يحتوي جسم الاستجابة على الرقم 200 بينما تكون حالة HTTP مختلفة. جمع القيمتين في الدالة نفسها يحافظ على اتساق الاستجابة.

3. إنشاء متحكم المصادقة

أنشئ المتحكم:

php artisan make:controller AuthController

استبدل محتواه بكود التنفيذ الكامل.

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!');
    }
}

يستورد المتحكم ApiResponses ويستخدمه، فتكون دوال الـTrait المحمية متاحة داخل المتحكم. استيراد Request موجود في التنفيذ المرجعي، لكنه لم يُستخدم بعد.

4. تسجيل مسار API

هذا هو ملف مسارات API الكامل في هذه المرحلة.

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();
});

يضيف Laravel البادئة /api تلقائياً للمسارات الموجودة هنا. لذلك يصبح Route::get('/login', ...) متاحاً على العنوان /api/login.

يبقى مسار Laravel الافتراضي والمحمي /api/user في الملف، لكننا لن نستخدمه في هذه المرحلة.

5. تشغيل الـAPI واختباره

شغّل خادم Laravel المحلي:

php artisan serve

اختبر المسار من نافذة terminal أخرى:

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

الأجزاء المهمة في النتيجة هي:

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

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

ويمكنك التأكد من تسجيل المسار باستخدام:

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

هيكل ملفات التنفيذ الكامل

هذه فقط هي ملفات التطبيق الخاصة التي أُضيفت أو تغيرت في هذه المرحلة، أما بقية الملفات فهي هيكل Laravel 13 الافتراضي.

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

لماذا نستخدم Trait للاستجابات؟

مع نمو الـAPI ستحتاج متحكمات كثيرة إلى إعادة استجابات بالشكل نفسه. وضع هذا الشكل في مكان مركزي يمنع الاختلافات الصغيرة، ويوفر مكاناً واحداً لإضافة دوال مساعدة لاحقاً مثل created() وerror() وnoContent().

هكذا تؤسس هذه المرحلة القاعدة التي سنبني عليها: مسار API، ومحتوى JSON منظم، وحالة HTTP صحيحة بقيمة 200.


2. اختبار API باستخدام Postman

استخدام Postman لإرسال طلبات API والتحقق من المدخلات وفحص أخطاء JSON بدلاً من الاعتماد على المتصفح.

ملفات التنفيذ

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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. تصميم عناوين قائمة على الموارد

تصميم مسارات قائمة على الموارد للطلبيات وتجهيز الموديلات والمصانع والترحيلات والبيانات التجريبية.

تحمل كل طلبية رمز حالة من حرف واحد: P (قيد الانتظار) وA (مدفوع) وS (تم الشحن) وC (ملغى). وتستخدم جميع قواعد التحقق والفلاتر والمصانع في هذا الدليل المجموعة نفسها.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. هيكلة API متعدد الإصدارات

إنشاء حدود واضحة للإصدار الأول من API باستخدام مسارات ومتحكمات وطلبات مخصصة.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. المصادقة بالرموز باستخدام Laravel Sanctum

مصادقة عملاء API باستخدام Laravel Sanctum وإنشاء الرموز وحماية المسارات ذات الإصدارات.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. إلغاء الرموز وتسجيل الخروج الآمن

تسجيل خروج العملاء بأمان عبر إلغاء رموز Sanctum وإعادة استجابات موحدة بلا محتوى.

ملفات التنفيذ

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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. تصميم استجابات مستقرة

تحويل طلبيات Eloquent إلى عقد JSON عام ومستقر باستخدام Laravel API Resources.

ملفات التنفيذ

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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. الحقول والعلاقات الشرطية

تشكيل استجابات مختلفة من المورد نفسه عبر إظهار الحقول والعلاقات أو إخفائها شرطياً.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. تحميل العلاقات عند الطلب

السماح للمستهلك بطلب بيانات العلاقات عبر معامل include وتجنب الاستعلامات غير الضرورية.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. فلاتر استعلام قابلة لإعادة الاستخدام

ربط حقول ومعاملات query string بفئات فلترة قابلة لإعادة الاستخدام للبحث في الطلبيات.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. الموارد المتداخلة وفلترة العلاقات

تمثيل العملاء وطلبياتهم كموارد متداخلة مع الحفاظ على اتساق الاستجابات وعناوين URL.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. ترتيب آمن يتحكم به العميل

إضافة ترتيب آمن يتحكم به العميل من دون كشف أعمدة قاعدة البيانات عشوائياً في query string.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. إنشاء الموارد باستخدام POST

التحقق من بيانات POST وإنشاء الطلبيات وإعادة المورد الجديد مع حالة HTTP الصحيحة.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. حذف الموارد باستخدام DELETE

حذف الطلبيات عبر متحكمات الموارد وإعادة استجابة فارغة صحيحة دلالياً.

ملفات التنفيذ

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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. الاستبدال الكامل باستخدام PUT

تنفيذ الاستبدال الكامل للمورد وفهم دلالة PUT والتحقق من جميع الحقول المطلوبة.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. التحديث الجزئي باستخدام PATCH

تنفيذ التحديث الجزئي مع مشاركة قواعد التحقق بأمان بين طلبات الإنشاء والاستبدال والتحديث.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. تفويض الموارد باستخدام Policies

تفويض الإجراءات على طلبيات محددة باستخدام Laravel Policy ودوال المتحكم المساعدة.

ملفات التنفيذ

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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. التحكم في الوصول بصلاحيات Token

تعيين صلاحيات لرموز Sanctum ودمجها مع Policies للتعبير عن أدوار وصلاحيات API.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. صلاحيات دقيقة على مستوى الحقول

تقييد العمليات والحقول الفردية بصلاحيات دقيقة بدلاً من الوصول الشامل أو المنع الكامل.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. إدارة طلبيات العملاء

تطبيق الفلترة والتحقق والملكية والتفويض باتساق على مسارات طلبيات العميل المتداخلة.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. إدارة المستخدمين بأمان

بناء مسارات كاملة لإدارة المستخدمين باستخدام Resources وForm Requests وPolicies وتسجيل المسارات.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. تطبيق مبدأ الحد الأدنى من الصلاحيات

تشديد التحقق وسلوك المتحكم بحيث لا يستطيع كل دور تغيير إلا البيانات التي يحتاج إليها فعلاً.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. معالجة موحدة لأخطاء API

توحيد الاستثناءات وأخطاء التحقق داخل بنية JSON واحدة ومتوقعة للأخطاء.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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. إنشاء توثيق API باستخدام Scribe

تثبيت Scribe وتوصيف المسارات وإنشاء توثيق API تفاعلي وملفات OpenAPI وPostman.

ملفات التنفيذ

  • 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

تعرض المقاطع التالية المحتوى الكامل للملفات، وليست أجزاء مختصرة من diff. لا نكرر ملفات Laravel المولدة التي لم تتغير في هذه المرحلة.

الكود الكامل

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 #لارافيل