تمرين شامل للمرحلة 3 — واجهة API وإشعارات ShopMetrics في الوقت الفعلي
التمرين الشامل للمرحلة 3 هو "تسليم ميزات متقدمة" — دمج المصادقة وواجهات API والبرمجيات الوسيطة والأحداث والتخزين لبناء واجهة API جاهزة للإنتاج.
1. ما ستتعلمه
- العملية الكاملة لمصادقة Sanctum Token API
- أكثر من 10 نقاط نهاية موارد API: CRUD للمستأجر/المنتج/الطلب/الاشتراك
- برمجيات وسيطة مخصصة: TenantResolver/RateLimiter/CorsHandler
- بث أحداث الطلبات: إشعارات WebSocket فورية لـ Alice وBob وCharlie
- تحميل صور المنتجات إلى S3 وتنزيل عناوين URL الموقعة مسبقًا
2. قصة قبول Alice للمرحلة 3
(1) مشكلة: مادة الدروس الست مجزأة ولا يمكن تجميعها في واجهة API كاملة
بعد إكمال المرحلة 3 — Sanctum في الدرس 15، وموارد في الدرس 16، والبرمجيات الوسيطة في الدرس 17 — لم تعرف Alice كيف تجمعها جميعًا في واجهة API كاملة. طلب منها Bob كتابة واجهة RESTful API، واكتشفت أن ميزات مثل المصادقة وتحويل الموارد والبرمجيات الوسيطة وبث الأحداث وتحميل الملفات تحتاج إلى العمل معًا. كانت تفهم كل واحدة على حدة، لكن عند الدمج، أصبح كل شيء فوضويًا.
(2) حلول التمارين الشاملة
في هذا الدرس، سنبني واجهة RESTful API كاملة من الصفر — مع كل نقطة نهاية تتميز بالمصادقة وعزل المستأجرين وتحديد المعدل وتعيين الموارد وبث الأحداث — لتسليم نظام API جاهز للإنتاج في النهاية.
# مخرجات المرحلة 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
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) نقطة نهاية المصادقة
// 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
// 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.']);
}
}
الناتج:
// التنفيذ ناجح
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) تعريفات المسارات
// 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
// 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());
}
}
الناتج:
// التنفيذ ناجح
5. طبقة البرمجيات الوسيطة المخصصة
(1) ▶ مثال:بنية البرمجيات الوسيطة لـ ShopMetrics
// 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);
}
}
الناتج:
// التنفيذ ناجح
(2) ▶ مثال:تكوين تحديد المعدل لـ ShopMetrics
// 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);
});
}
الناتج:
// التنفيذ ناجح
6. تكامل بث الأحداث
(1) أحداث البث
// 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 في الواجهة الأمامية
// 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
// 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]);
}
}
الناتج:
// التنفيذ ناجح
8. مثال شامل: واجهة API الكاملة للمرحلة 3 من ShopMetrics
// ============================================
// شامل: واجهة 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)
❓ أسئلة شائعة
knuckleswtf/scribe) لتوليد توثيق OpenAPI تلقائيًا من الكود؛ هذا أقل عرضة لأن يصبح قديمًا من الكتابة اليدوية في Postman. أضف تعليقات PHPDoc ببساطة إلى كل طريقة في وحدة التحكم.docker run -p 9000:9000 minio/minio server /data. غيّر AWS_URL ببساطة للإشارة إلى localhost:9000.📖 ملخص
- Sanctum Token يوفر مصادقة بدون حالة لواجهات API
- تنسيق إخراج JSON موحد لموارد API؛ الجمعيات المتداخلة باستخدام شرط
whenLoaded - مكدس البرمجيات الوسيطة: CORS → TenantResolve → Auth → Throttle → Ability
- بث الأحداث يمكّن الإشعارات الفورية لتغييرات الطلبات إلى الواجهة الأمامية
- تحميلات ملفات S3 تدعم التحميل المباشر (عناوين URL الموقعة مسبقًا) والتحميل عبر الخادم
- عند إكمال المرحلة 3، ستمتلك واجهة RESTful API جاهزة للإنتاج
📝 تمارين
-
تمرين أساسي (⭐): قم بإعداد طبقة مصادقة API لـ ShopMetrics، ونفذ النقاط الثلاث (التسجيل، وتسجيل الدخول، وتسجيل الخروج)، واستخدم Postman لاختبار الحصول على token والوصول إلى نقاط النهاية المحمية.
-
تمرين متقدم (⭐⭐): نفذ واجهة CRUD كاملة لـ Shop + Product، بما في ذلك تحويل الموارد، وعزل TenantResolve، وتحديد المعدل القائم على الاشتراك. اختبر جميع نقاط النهاية باستخدام Postman Collection.
-
تحدي (⭐⭐⭐): نفذ بثًا فوريًا لتغييرات حالة الطلب — طلب
PATCH /orders/{id}/statusمن الواجهة الخلفية يفعل حدثOrderStatusChanged، وEchoفي الواجهة الأمامية يستمع للتحديثات لتحديث واجهة المستخدم، ويسجل طلبات API في قاعدة البيانات في نفس الوقت (باستخدام برمجيةterminateالوسيطة).



