404 Not Found

404 Not Found


nginx

تمرين شامل للمرحلة 3 — واجهة API وإشعارات ShopMetrics في الوقت الفعلي

التمرين الشامل للمرحلة 3 هو "تسليم ميزات متقدمة" — دمج المصادقة وواجهات API والبرمجيات الوسيطة والأحداث والتخزين لبناء واجهة API جاهزة للإنتاج.

1. ما ستتعلمه


2. قصة قبول Alice للمرحلة 3

(1) مشكلة: مادة الدروس الست مجزأة ولا يمكن تجميعها في واجهة API كاملة

بعد إكمال المرحلة 3 — Sanctum في الدرس 15، وموارد في الدرس 16، والبرمجيات الوسيطة في الدرس 17 — لم تعرف Alice كيف تجمعها جميعًا في واجهة API كاملة. طلب منها Bob كتابة واجهة RESTful API، واكتشفت أن ميزات مثل المصادقة وتحويل الموارد والبرمجيات الوسيطة وبث الأحداث وتحميل الملفات تحتاج إلى العمل معًا. كانت تفهم كل واحدة على حدة، لكن عند الدمج، أصبح كل شيء فوضويًا.

(2) حلول التمارين الشاملة

في هذا الدرس، سنبني واجهة RESTful API كاملة من الصفر — مع كل نقطة نهاية تتميز بالمصادقة وعزل المستأجرين وتحديد المعدل وتعيين الموارد وبث الأحداث — لتسليم نظام API جاهز للإنتاج في النهاية.

BASH
# مخرجات المرحلة 3: واجهة RESTful API كاملة مع ميزات في الوقت الفعلي
php artisan migrate:fresh --seed
php artisan queue:work &
php artisan storage:link
# → أكثر من 10 نقاط نهاية، المصادقة، تحديد المعدل، البث جميعها تعمل

(3) العائد

بمجرد أن أنهت Alice، كان لديها واجهة API كاملة لـ ShopMetrics — مدمجة بالكامل من المصادقة إلى البث — يمكن تسليمها مباشرة لفريق الواجهة الأمامية.


3. طبقة مصادقة API

(1) عملية مصادقة Sanctum Token

100%
flowchart TD
    A[عميل] --> B["POST /api/auth/login<br/>(بريد إلكتروني+كلمة مرور)"]
    B --> C["Sanctum ينشئ Token"]
    C --> D["يعيد plainTextToken"]
    D --> E["العميل يخزن Token"]
    E --> F["طلبات API مع<br/>Authorization: Bearer {token}"]
    F --> G["برمجية Sanctum الوسيطة<br/>تحديد المستخدم"]
    G --> H[برمجية TenantResolve الوسيطة]
    H --> I[وحدة التحكم]

(2) نقطة نهاية المصادقة

PHP
// routes/api.php
Route::prefix('auth')->group(function () {
    Route::post('/register', [Auth\RegisterController::class, 'register']);
    Route::post('/login', [Auth\ApiTokenController::class, 'login']);
    Route::post('/forgot-password', [Auth\PasswordResetController::class, 'sendResetLink']);
    Route::post('/reset-password', [Auth\PasswordResetController::class, 'reset']);

    Route::middleware('auth:sanctum')->group(function () {
        Route::get('/user', fn (Request $r) => new UserResource($r->user()));
        Route::post('/logout', [Auth\ApiTokenController::class, 'logout']);
        Route::apiResource('tokens', Auth\TokenController::class)->only(['store', 'index', 'destroy']);
    });
});
نقطة النهاية الطريقة المصادقة الوصف
/auth/register POST تسجيل
/auth/login POST الحصول على Token
/auth/user GET المستخدم الحالي
/auth/logout POST إلغاء Token
/auth/tokens POST إنشاء token جديد
/auth/tokens GET قائمة الرموز
/auth/tokens/{id} DELETE حذف Token

(1) ▶ مثال:تنفيذ كامل لمصادقة Token لـ ShopMetrics

PHP
// app/Http/Controllers/Auth/ApiTokenController.php
class ApiTokenController extends Controller
{
    public function login(Request $request): JsonResponse
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required|string',
            'device_name' => 'sometimes|string|max:255',
        ]);

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

        if (!$user || !Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['Invalid credentials.'],
            ]);
        }

        if (!$user->tenant_id || $user->tenant->status !== 'active') {
            throw ValidationException::withMessages([
                'email' => ['Account is not active.'],
            ]);
        }

        $abilities = match ($user->role) {
            'super_admin' => ['*'],
            'tenant_owner' => ['read', 'write', 'manage-users'],
            'analyst' => ['read'],
            default => [],
        };

        $token = $user->createToken(
            $request->device_name ?? 'api-token',
            $abilities,
        );

        return response()->json([
            'user' => new UserResource($user),
            'token' => $token->plainTextToken,
            'abilities' => $abilities,
        ]);
    }

    public function logout(Request $request): JsonResponse
    {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => 'Token revoked.']);
    }
}

الناتج:

TEXT
// التنفيذ ناجح

4. نقاط نهاية موارد API

(1) قائمة كاملة بنقاط النهاية

نقطة النهاية الطريقة المورد البرمجيات الوسيطة
/v1/shops GET ShopResource::collection auth,tenant,throttle
/v1/shops POST ShopResource auth,tenant,throttle,role:owner
/v1/shops/{id} GET ShopResource auth,tenant
/v1/shops/{id} PUT ShopResource auth,tenant,role:owner
/v1/shops/{id} DELETE - auth,tenant,role:owner
/v1/shops/{id}/products GET ProductResource::collection auth,tenant
/v1/products POST ProductResource auth,tenant,role:owner
/v1/products/{id} GET ProductResource auth,tenant
/v1/products/{id} PUT ProductResource auth,tenant,role:owner
/v1/orders GET OrderResource::collection auth,tenant
/v1/orders/{id} GET OrderResource auth,tenant
/v1/orders/{id}/status PATCH OrderResource auth,tenant,role:owner
/v1/analytics/overview GET AnalyticsResource auth,tenant,ability:read
/v1/reports/generate POST ReportResource auth,tenant,ability:write

(2) تعريفات المسارات

PHP
// routes/api.php
Route::middleware(['auth:sanctum', 'tenant.resolve', 'throttle:tenant-api'])
    ->prefix('v1')->name('api.v1.')->group(function () {
        // المتاجر
        Route::apiResource('shops', Api\V1\ShopController::class);
        Route::post('shops/{shop}/logo', Api\V1\ShopLogoController::class)->name('shops.logo');

        // المنتجات
        Route::apiResource('products', Api\V1\ProductController::class);

        // الطلبات
        Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
        Route::patch('orders/{order}/status', [Api\V1\OrderController::class, 'updateStatus']);

        // التحليلات والتقارير
        Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview']);
        Route::post('reports/generate', [Api\V1\ReportController::class, 'generate']);

        // الوسائط
        Route::post('media/upload', [Api\V1\MediaController::class, 'upload']);
        Route::post('media/presign', [Api\V1\MediaController::class, 'presign']);
        Route::get('media/{media}/download', [Api\V1\MediaController::class, 'download']);
    });

(1) ▶ مثال:نقطة نهاية API طلبات ShopMetrics

PHP
// app/Http/Controllers/Api/V1/OrderController.php
class OrderController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        $query = Order::where('tenant_id', tenant()->id)
            ->with(['shop', 'user'])
            ->withCount('items');

        if ($request->filled('status')) {
            $query->where('status', $request->status);
        }
        if ($request->filled('shop_id')) {
            $query->where('shop_id', $request->shop_id);
        }
        if ($request->filled('date_from')) {
            $query->where('created_at', '>=', $request->date('date_from'));
        }

        $orders = $query->latest()->paginate($request->integer('per_page', 15));

        return OrderResource::collection($orders);
    }

    public function show(Order $order): JsonResponse
    {
        $this->authorize('view', $order);
        $order->load(['items.product', 'shop', 'user']);
        return new OrderResource($order);
    }

    public function updateStatus(Request $request, Order $order): JsonResponse
    {
        $this->authorize('update', $order);

        $validated = $request->validate([
            'status' => 'required|in:processing,completed,cancelled,refunded',
        ]);

        $oldStatus = $order->status;
        $order->update(['status' => $validated['status']]);

        event(new OrderStatusChanged($order, $oldStatus, $validated['status']));

        return new OrderResource($order->fresh());
    }
}

الناتج:

TEXT
// التنفيذ ناجح

5. طبقة البرمجيات الوسيطة المخصصة

(1) ▶ مثال:بنية البرمجيات الوسيطة لـ ShopMetrics

PHP
// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();
        if (!$user?->tenant_id) abort(403, 'No tenant.');
        $tenant = $user->tenant;
        if ($tenant->status !== 'active') abort(403, 'Tenant inactive.');
        app()->instance(Tenant::class, $tenant);
        return $next($request);
    }
}

// app/Http/Middleware/CheckAbility.php
class CheckAbility
{
    public function handle(Request $request, Closure $next, string $ability): Response
    {
        if ($request->user()->tokenCan('*') || $request->user()->tokenCan($ability)) {
            return $next($request);
        }
        abort(403, "Missing ability: {$ability}");
    }
}

// app/Http/Middleware/EnsureSubscriptionActive.php
class EnsureSubscriptionActive
{
    public function handle(Request $request, Closure $next): Response
    {
        $tenant = app()->make(Tenant::class);
        if (!$tenant->subscription?->isActive()) {
            abort(402, 'Active subscription required.');
        }
        return $next($request);
    }
}

الناتج:

TEXT
// التنفيذ ناجح

(2) ▶ مثال:تكوين تحديد المعدل لـ ShopMetrics

PHP
// app/Providers/AppServiceProvider.php
public function boot(): void
{
    RateLimiter::for('tenant-api', function (Request $request) {
        $tenant = app()->make(Tenant::class);
        $plan = $tenant->plan ?? null;

        return Limit::perMinute(match ($plan->slug ?? 'starter') {
            'enterprise' => 600,
            'pro' => 120,
            default => 60,
        })->by($tenant->id);
    });
}

الناتج:

TEXT
// التنفيذ ناجح

6. تكامل بث الأحداث

(1) أحداث البث

PHP
// app/Events/OrderPlaced.php
class OrderPlaced implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('tenant.' . $this->order->tenant_id)];
    }

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->order->id,
            'order_number' => $this->order->order_number,
            'total' => (float) $this->order->total,
            'shop_name' => $this->order->shop->name,
        ];
    }

    public function broadcastAs(): string { return 'order.placed'; }
}

(2) الاستماع عبر Echo في الواجهة الأمامية

JAVASCRIPT
// resources/js/app.js
window.Echo.private(`tenant.${tenantId}`)
    .listen('.order.placed', (e) => {
        showNotification(`طلب جديد: ${e.order_number} ($${e.total})`);
    })
    .listen('.order.status_changed', (e) => {
        updateOrderRow(e.order_id, e.new_status);
    });

7. تكامل تحميل ملفات S3

(1) ▶ مثال:نقطة نهاية تحميل صور ShopMetrics

PHP
// app/Http/Controllers/Api/V1/ShopLogoController.php
class ShopLogoController extends Controller
{
    public function __invoke(Request $request, Shop $shop): JsonResponse
    {
        $this->authorize('update', $shop);

        $validated = $request->validate([
            'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
        ]);

        if ($shop->logo_path) {
            Storage::disk('s3')->delete($shop->logo_path);
        }

        $path = $request->file('logo')->store(
            "shops/{$shop->id}/logos",
            's3',
        );

        $shop->update(['logo_path' => $path]);

        return response()->json([
            'message' => 'Logo uploaded.',
            'logo_url' => Storage::disk('s3')->url($path),
        ]);
    }
}

// app/Http/Controllers/Api/V1/MediaController.php
class MediaController extends Controller
{
    public function presign(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'filename' => 'required|string',
            'mime_type' => 'required|in:image/jpeg,image/png,image/webp',
        ]);

        $path = 'uploads/' . tenant()->id . '/' . Str::uuid() . '/' . $validated['filename'];
        $url = Storage::disk('s3')->temporaryUploadUrl($path, now()->addMinutes(30));

        return response()->json(['upload_url' => $url, 'path' => $path]);
    }
}

الناتج:

TEXT
// التنفيذ ناجح

8. مثال شامل: واجهة API الكاملة للمرحلة 3 من ShopMetrics

PHP
// ============================================
// شامل: واجهة API الكاملة للمرحلة 3 من ShopMetrics
// يغطي: المصادقة، الموارد، البرمجيات الوسيطة، الأحداث، التخزين
// ============================================

// routes/api.php — مسارات API كاملة
Route::prefix('auth')->group(function () {
    Route::post('/register', [Auth\RegisterController::class, 'register']);
    Route::post('/login', [Auth\ApiTokenController::class, 'login']);
    Route::middleware('auth:sanctum')->group(function () {
        Route::get('/user', fn (Request $r) => new UserResource($r->user()));
        Route::post('/logout', [Auth\ApiTokenController::class, 'logout']);
    });
});

Route::middleware(['auth:sanctum', 'tenant.resolve', 'throttle:tenant-api', 'subscription.active'])
    ->prefix('v1')->group(function () {
        Route::apiResource('shops', Api\V1\ShopController::class);
        Route::post('shops/{shop}/logo', Api\V1\ShopLogoController::class);

        Route::apiResource('products', Api\V1\ProductController::class);
        Route::post('products/{product}/images', Api\V1\ProductImageController::class);

        Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
        Route::patch('orders/{order}/status', [Api\V1\OrderController::class, 'updateStatus']);

        Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview'])
            ->middleware('ability:read');
        Route::post('reports/generate', [Api\V1\ReportController::class, 'generate'])
            ->middleware('ability:write');

        Route::post('media/upload', [Api\V1\MediaController::class, 'upload']);
        Route::post('media/presign', [Api\V1\MediaController::class, 'presign']);
        Route::get('media/{media}/download', [Api\V1\MediaController::class, 'download']);
    });

// routes/channels.php
Broadcast::channel('tenant.{tenantId}', fn ($user, $tenantId) => $user->tenant_id === (int) $tenantId);

// تكوين البث
// BROADCAST_CONNECTION=redis
// QUEUE_CONNECTION=redis
// تشغيل: php artisan queue:work
// تشغيل: php artisan reverb:start (خادم WebSocket المدمج في Laravel 11)

❓ أسئلة شائعة

س كم يستغرق تمرين المرحلة 3؟
ج حوالي 6-8 ساعات. الشهادات: 1.5 ساعة؛ موارد API + نقاط النهاية: ساعتان؛ البرمجيات الوسيطة: ساعة؛ بث الأحداث: 1.5 ساعة؛ تحميل الملفات: ساعة؛ اختبار التكامل: ساعة. نوصي بالاختبار باستخدام Postman بعد إكمال كل وحدة.
س كيف أدمج واجهات API الأمامية والخلفية؟
ج استخدم Postman أو Newman لاختبار جميع نقاط النهاية أولاً واحفظها كـ Collection. عند تطوير الواجهة الأمامية، استخدم Mock Server من Postman أو اتصل مباشرة بالواجهة الخلفية. تأكد من تكوين CORS بشكل صحيح.
س هل يجب استخدام Reverb أم Pusher لخدمة WebSocket؟
ج يتضمن Laravel 11 Reverb (خادم WebSocket مستضاف ذاتيًا) افتراضيًا. Reverb مجاني وسريع الاستخدام للتطوير؛ لبيئات الإنتاج، اختر بين Reverb (مستضاف ذاتيًا) أو Pusher (مستضاف) حسب حجمك.
س كيف أنشئ توثيق API؟
ج نوصي باستخدام Scribe (knuckleswtf/scribe) لتوليد توثيق OpenAPI تلقائيًا من الكود؛ هذا أقل عرضة لأن يصبح قديمًا من الكتابة اليدوية في Postman. أضف تعليقات PHPDoc ببساطة إلى كل طريقة في وحدة التحكم.
س كيف تختبر تحديد المعدل؟
ج استخدم Postman Runner أو ab (Apache Bench) لإرسال طلبات تتجاوز حد المعدل، وتحقق من إرجاع رمز حالة 429 ورأس Retry-After. يمكنك أيضًا كتابة اختبار PHPUnit لمحاكاة تحديد المعدل.
س كيف أختبر تحميلات S3 أثناء التطوير المحلي؟
ج استخدم MinIO (خدمة S3 المتوافقة محليًا) كبديل لـ S3 الفعلي: docker run -p 9000:9000 minio/minio server /data. غيّر AWS_URL ببساطة للإشارة إلى localhost:9000.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): قم بإعداد طبقة مصادقة API لـ ShopMetrics، ونفذ النقاط الثلاث (التسجيل، وتسجيل الدخول، وتسجيل الخروج)، واستخدم Postman لاختبار الحصول على token والوصول إلى نقاط النهاية المحمية.

  2. تمرين متقدم (⭐⭐): نفذ واجهة CRUD كاملة لـ Shop + Product، بما في ذلك تحويل الموارد، وعزل TenantResolve، وتحديد المعدل القائم على الاشتراك. اختبر جميع نقاط النهاية باستخدام Postman Collection.

  3. تحدي (⭐⭐⭐): نفذ بثًا فوريًا لتغييرات حالة الطلب — طلب PATCH /orders/{id}/status من الواجهة الخلفية يفعل حدث OrderStatusChanged، وEcho في الواجهة الأمامية يستمع للتحديثات لتحديث واجهة المستخدم، ويسجل طلبات API في قاعدة البيانات في نفس الوقت (باستخدام برمجية terminate الوسيطة).

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%