تحليل معمق لبرمجيات Laravel الوسيطة
البرمجيات الوسيطة هي "خط أنابيب نقاط التفتيش الأمنية" في Laravel — يمر كل طلب عبر طبقة تلو طبقة من الفحوصات، تمامًا مثل الأمتعة؛ تلك التي تفشل يتم اعتراضها فورًا، بينما تلك التي تجتاز يُسمح لها بالانتقال إلى المرحلة التالية.
1. ما ستتعلمه
- تدفق تنفيذ البرمجيات الوسيطة ونمط Pipeline
- ثلاث طرق تسجيل: عالمية، مسار، ومجموعة برمجيات وسيطة
- برمجيات وسيطة مخصصة: تحديد المعدل / CORS / تحديد المستأجر
- تمرير معاملات البرمجيات الوسيطة: throttle:60,1 / role:admin
- فرز البرمجيات الوسيطة والتحكم في الأولوية
2. قصة حقيقية لمهندس معماري
(1) مشكلة: جميع فحوصات الأمن مركزة في وحدة التحكم
كتب Bob 10 أسطر من كود التحقق في بداية كل طريقة في وحدة التحكم — فحوصات المصادقة، وعزل المستأجرين، وتحديد المعدل، وCOS، والتسجيل. 50 طريقة x 10 أسطر = 500 سطر من الكود المكرر. عندما أضافت Alice سياسة أمنية جديدة، كان عليها إجراء تغييرات في 50 مكانًا؛ فاتتها ثلاثة، مما أدى إلى ثغرة أمنية. سأل Charlie: "هل سمعتم عن البرمجيات الوسيطة؟"
(2) حل البرمجيات الوسيطة
تستخرج البرمجيات الوسيطة منطق التحقق المشترك إلى فئات منفصلة، ويمر كل طلب تلقائيًا عبر طبقات متعددة من البرمجيات الوسيطة — المصادقة، وتحديد المعدل، وCORS، وعزل المستأجرين — بينما تركز وحدات التحكم فقط على منطق الأعمال.
// قبل — 10 أسطر من الفحوصات في كل طريقة
public function index() {
if (!auth()->check()) abort(401);
if (!tenant()->isActive()) abort(403);
if (RateLimiter::tooManyAttempts(...)) abort(429);
// ... أخيرًا، منطق الأعمال
}
// بعد — البرمجيات الوسيطة تعالج جميع الفحوصات
Route::middleware(['auth', 'tenant.resolve', 'throttle:60,1'])
->get('/shops', [ShopController::class, 'index']);
// وحدة التحكم تحتوي فقط على منطق الأعمال
(3) العائد
بعد أن نفذ Bob البرمجيات الوسيطة، انخفض كود وحدة التحكم بنسبة 60%؛ سياسة Alice الأمنية الجديدة تطلبت تعديل برمجية وسيطة واحدة فقط، بدون أي إغفالات.
3. تدفق تنفيذ خط أنابيب البرمجيات الوسيطة
(1) العملية الكاملة لمرور طلب عبر البرمجيات الوسيطة
flowchart LR
A[طلب] --> B[برمجية وسيطة عالمية 1]
B --> C[برمجية وسيطة عالمية 2]
C --> D[برمجية وسيطة مسار 1]
D --> E[برمجية وسيطة مسار 2]
E --> F[وحدة التحكم]
F --> G[استجابة]
G --> H[بعد برمجية وسيطة 2]
H --> I[بعد برمجية وسيطة 1]
I --> J[العميل]
(2) نموذج البصل
طلب →
برمجية وسيطة A (قبل) →
برمجية وسيطة B (قبل) →
وحدة التحكم → استجابة
برمجية وسيطة B (بعد) →
برمجية وسيطة A (بعد) →
استجابة
كل برمجية وسيطة يمكنها:
- قبل: تنفيذ المنطق قبل وصول الطلب إلى وحدة التحكم
- بعد: تنفيذ المنطق بعد أن تعيد وحدة التحكم استجابة
- دائرة قصر: إعادة استجابة فورًا دون تمريرها إلى الطبقة التالية
(1) ▶ مثال:فهم نموذج البصل للبرمجيات الوسيطة
// app/Http/Middleware/LogRequests.php
class LogRequests
{
public function handle(Request $request, Closure $next): Response
{
// قبل — تسجيل الطلب الوارد
Log::info('Request:', [
'method' => $request->method(),
'url' => $request->fullUrl(),
'ip' => $request->ip(),
]);
// تمرير إلى البرمجية الوسيطة التالية
$response = $next($request);
// بعد — تسجيل حالة الاستجابة
Log::info('Response:', [
'status' => $response->getStatusCode(),
'duration' => defined('LARAVEL_START') ? round((microtime(true) - LARAVEL_START) * 1000) : null,
]);
return $response;
}
}
الناتج:
// التنفيذ ناجح
4. ثلاث طرق للتسجيل
(1) برمجيات وسيطة عالمية
يتم تنفيذها لكل طلب؛ لا حاجة لتحديدها في المسار.
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->append([
\App\Http\Middleware\LogRequests::class,
\App\Http\Middleware\SetLocale::class,
]);
$middleware->remove([
\Illuminate\Foundation\Http\Middleware\TrimStrings::class,
]);
})
(2) برمجيات وسيطة للمسار
برمجيات وسيطة محددة في تعريف المسار.
// تسجيل اسم مستعار
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'tenant' => \App\Http\Middleware\TenantResolve::class,
'role' => \App\Http\Middleware\CheckRole::class,
'ability' => \App\Http\Middleware\CheckAbility::class,
]);
})
// استخدام في المسارات
Route::middleware(['auth', 'tenant', 'role:admin'])
->get('/admin/users', [AdminController::class, 'users']);
(3) مجموعة البرمجيات الوسيطة
تجميع مجموعة من البرمجيات الوسيطة وتطبيقها كمجموعة.
| اسم المجموعة | الافتراضي | مناسب لـ |
|---|---|---|
web |
StartSession, EncryptCookies, VerifyCsrfToken | مسارات الويب |
api |
Throttle:api, SubstituteBindings | مسارات API |
// تخصيص مجموعات البرمجيات الوسيطة
->withMiddleware(function (Middleware $middleware) {
$middleware->appendToGroup('api', [
\App\Http\Middleware\TenantResolve::class,
]);
$middleware->prependToGroup('web', [
\App\Http\Middleware\SetLocale::class,
]);
})
| طريقة التسجيل | التوقيت | السيناريوهات المطبقة |
|---|---|---|
| عالمية | جميع الطلبات | السجلات، CORS |
| اسم مستعار مسار | مسار محدد | المصادقة، المستأجر |
| مجموعة برمجيات وسيطة | مسارات داخل المجموعة | مجموعة Web/API |
(1) ▶ مثال:تسجيل البرمجيات الوسيطة لـ ShopMetrics
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
// برمجيات وسيطة عالمية
$middleware->append([
\App\Http\Middleware\SetLocale::class,
]);
// أسماء مستعارة
$middleware->alias([
'tenant' => \App\Http\Middleware\TenantResolve::class,
'role' => \App\Http\Middleware\CheckRole::class,
'ability' => \App\Http\Middleware\CheckAbility::class,
]);
// إضافة إلى مجموعة API
$middleware->appendToGroup('api', [
\App\Http\Middleware\EnsureJsonAccept::class,
]);
})
الناتج:
// التنفيذ ناجح
5. برمجيات وسيطة مخصصة
(1) إنشاء برمجيات وسيطة
php artisan make:middleware TenantResolve
php artisan make:middleware CheckRole
php artisan make:middleware EnsureJsonAccept
(2) برمجية TenantResolve الوسيطة
// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if (!$user || !$user->tenant_id) {
abort(403, 'No tenant associated with this account.');
}
$tenant = $user->tenant;
if ($tenant->status !== 'active') {
abort(403, 'Tenant account is ' . $tenant->status . '.');
}
// تخزين المستأجر في الحاوية للنطاق العام
app()->instance(Tenant::class, $tenant);
// تعيين سياق المستأجر على الطلب
$request->attributes->set('tenant', $tenant);
return $next($request);
}
}
(3) برمجية CORS الوسيطة
// app/Http/Middleware/HandleCors.php
class HandleCors
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('Access-Control-Allow-Origin', config('cors.allowed_origins'));
$response->headers->set('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Tenant-Id');
$response->headers->set('Access-Control-Max-Age', '86400');
if ($request->isMethod('OPTIONS')) {
$response->setStatusCode(204);
}
return $response;
}
}
(1) ▶ مثال:برمجية تحديد المعدل لـ ShopMetrics
// app/Http/Middleware/PerTenantRateLimit.php
class PerTenantRateLimit
{
public function handle(Request $request, Closure $next, int $maxAttempts = 60, int $decayMinutes = 1): Response
{
$tenant = app()->make(Tenant::class);
$key = 'tenant:' . $tenant->id . ':' . $request->ip();
if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
$seconds = RateLimiter::availableIn($key);
abort(429, "Too many requests. Try again in {$seconds} seconds.");
}
RateLimiter::hit($key, $decayMinutes * 60);
$response = $next($request);
$response->headers->set('X-RateLimit-Limit', $maxAttempts);
$response->headers->set('X-RateLimit-Remaining', $maxAttempts - RateLimiter::attempts($key));
return $response;
}
}
الناتج:
// التنفيذ ناجح
6. معاملات البرمجيات الوسيطة
(1) تمرير المعاملات
// تعريف المسار مع المعاملات
Route::middleware('role:admin,super_admin')
->get('/admin/dashboard', [AdminController::class, 'dashboard']);
// البرمجية الوسيطة تستقبل المعاملات
class CheckRole
{
public function handle(Request $request, Closure $next, string ...$roles): Response
{
if (!in_array($request->user()->role, $roles)) {
abort(403, 'Insufficient role. Required: ' . implode(', ', $roles));
}
return $next($request);
}
}
(2) معامل throttle
// throttle المدمج مع المعاملات
Route::middleware('throttle:60,1')->group(function () {
// 60 طلب لكل 1 دقيقة
});
Route::middleware('throttle:10,1')->group(function () {
// 10 طلبات لكل 1 دقيقة (لنقاط النهاية المكلفة)
});
// محدد معدل مخصص
RateLimiter::for('api', function (Request $request) {
return $request->user()
? Limit::perMinute(60)->by($request->user()->id)
: Limit::perMinute(10)->by($request->ip());
});
RateLimiter::for('tenant-api', function (Request $request) {
$tenant = app()->make(Tenant::class);
$plan = $tenant->subscription?->plan;
return Limit::perMinute(match ($plan->slug ?? 'starter') {
'enterprise' => 300,
'pro' => 120,
default => 60,
})->by($tenant->id);
});
| تنسيق المعامل | الوصف | مثال |
|---|---|---|
middleware:p1 |
معامل واحد | role:admin |
middleware:p1,p2 |
معاملات متعددة | role:admin,editor |
throttle:max,decay |
معاملات التحديد | throttle:60,1 |
(1) ▶ مثال:تحديد المعدل القائم على الاشتراك لـ 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)->response(function () {
return response()->json([
'message' => 'Rate limit exceeded. Upgrade your plan for higher limits.',
'upgrade_url' => route('pricing'),
], 429);
});
});
}
// routes/api.php
Route::middleware(['auth:sanctum', 'tenant', 'throttle:tenant-api'])
->prefix('v1')
->group(function () {
Route::apiResource('shops', Api\ShopController::class);
});
الناتج:
// التنفيذ ناجح
7. ترتيب البرمجيات الوسيطة والأولوية
(1) الأولوية الافتراضية
يتم تنفيذ البرمجيات الوسيطة بالترتيب الذي تم تسجيلها به، لكن بعض البرمجيات الوسيطة يجب تنفيذها أولاً (على سبيل المثال، يجب معالجة CORS قبل المصادقة).
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->priority([
\Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TenantResolve::class, // يجب أن يعمل بعد CORS
\App\Http\Middleware\Authenticate::class,
]);
})
(2) برمجيات وسيطة قابلة للإنهاء
// app/Http/Middleware/SendAnalyticsEvent.php
class SendAnalyticsEvent
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
// يعمل بعد إرسال الاستجابة إلى العميل
public function terminate(Request $request, Response $response): void
{
// تسجيل التحليلات بدون حظر
AnalyticsEvent::create([
'tenant_id' => app()->make(Tenant::class)?->id,
'path' => $request->path(),
'method' => $request->method(),
'status' => $response->getStatusCode(),
'duration_ms' => defined('LARAVEL_START')
? round((microtime(true) - LARAVEL_START) * 1000)
: null,
]);
}
}
| نهج دورة الحياة | التوقيت | الغرض |
|---|---|---|
handle() (قبل) |
عند استلام الطلب | المصادقة، التصفية، وتعديل الطلب |
handle() (بعد) |
عند إرجاع الاستجابة | تعديل رؤوس الاستجابة |
terminate() |
بعد إرسال الاستجابة | السجلات، الإحصائيات، التنظيف |
(1) ▶ مثال:تكوين أولوية البرمجيات الوسيطة لـ ShopMetrics
// bootstrap/app.php
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
// الأولوية — CORS أولاً، المصادقة قبل المستأجر
$middleware->priority([
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\TenantResolve::class,
\App\Http\Middleware\Authenticate::class,
\App\Http\Middleware\CheckRole::class,
]);
// أسماء مستعارة
$middleware->alias([
'tenant' => \App\Http\Middleware\TenantResolve::class,
'role' => \App\Http\Middleware\CheckRole::class,
'tenant.throttle' => \App\Http\Middleware\PerTenantRateLimit::class,
]);
// إضافات مجموعة API
$middleware->appendToGroup('api', [
\App\Http\Middleware\EnsureJsonAccept::class,
]);
});
الناتج:
// التنفيذ ناجح
8. مثال شامل: بنية البرمجيات الوسيطة لـ ShopMetrics
// ============================================
// شامل: مكدس البرمجيات الوسيطة لـ 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 associated.');
}
$tenant = $user->tenant;
if ($tenant->status !== 'active') {
abort(403, 'Tenant is ' . $tenant->status);
}
app()->instance(Tenant::class, $tenant);
return $next($request);
}
}
// app/Http/Middleware/CheckRole.php
class CheckRole
{
public function handle(Request $request, Closure $next, string ...$roles): Response
{
if (!in_array($request->user()?->role, $roles)) {
abort(403, 'Required role: ' . implode('|', $roles));
}
return $next($request);
}
}
// app/Http/Middleware/EnsureSubscriptionActive.php
class EnsureSubscriptionActive
{
public function handle(Request $request, Closure $next): Response
{
$tenant = app()->make(Tenant::class);
$subscription = $tenant->subscription;
if (!$subscription || $subscription->status !== 'active') {
abort(402, 'Active subscription required.');
}
return $next($request);
}
}
// app/Http/Middleware/LogApiRequests.php
class LogApiRequests
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
ApiLog::create([
'tenant_id' => app()->make(Tenant::class)?->id,
'user_id' => $request->user()?->id,
'method' => $request->method(),
'path' => $request->path(),
'status' => $response->getStatusCode(),
'ip' => $request->ip(),
]);
}
}
// المسارات باستخدام مكدس البرمجيات الوسيطة الكامل
Route::middleware(['auth:sanctum', 'tenant', 'throttle:tenant-api', 'subscription.active'])
->prefix('v1')->group(function () {
Route::apiResource('shops', ShopController::class);
Route::middleware('role:tenant_owner,analyst')
->apiResource('analytics', AnalyticsController::class)->only(['index', 'show']);
Route::middleware('role:tenant_owner')
->apiResource('settings', SettingsController::class);
});
❓ أسئلة شائعة
terminate؟register_shutdown_function). مناسبة للعمليات مثل التسجيل والإحصائيات التي لا تحتاج إلى التأثير على الاستجابة. terminate لا يحظر المستخدم.return response()->json(['error' => 'Unauthorized'], 401);. البرمجيات الوسيطة ووحدات التحكم اللاحقة لن تُنفذ.$middleware->skipWhen(callback) أو $request->is('api/*') داخل البرمجية الوسيطة لتحديد ما إذا كنت تريد تخطي مسار. لا يُنصح بتضمين فحوصات المسار في البرمجيات الوسيطة العالمية.throttle:اسم_الاستراتيجية.📖 ملخص
- البرمجيات الوسيطة تعمل وفق نموذج البصل: قبل → next → بعد
- ثلاث طرق للتسجيل: اسم مستعار عالمي، اسم مستعار مسار، ومجموعة برمجيات وسيطة
- البرمجيات الوسيطة المخصصة تُنشأ باستخدام
make:middlewareوتُستخدم بعد تسجيل اسم مستعار - معاملات البرمجيات الوسيطة مفصولة بنقطتين:
role:admin,editor - الأولوية تحدد ترتيب التنفيذ؛ CORS يجب أن يسبق المصادقة
terminate()تُنفذ بعد إرسال الاستجابة؛ مناسبة للتسجيل والإحصائيات
📝 تمارين
-
تمرين أساسي (⭐): أنشئ برمجية TenantResolve الوسيطة التي تحدد المستأجر من المستخدم المصادق عليه وتخزنه في حاوية، ثم استخدمها في مجموعة مسارات API. اختبر أن المستخدمين بدون مستأجر مرتبط يتلقون خطأ 403 عند محاولتهم الوصول إلى API.
-
تمرين متقدم (⭐⭐): نفذ برمجية PerTenantRateLimit الوسيطة لفرض تحديد المعدل بناءً على معرف المستأجر وعنوان IP، مع تعيين حدود معدل ديناميكيًا وفقًا لخطة الاشتراك (Starter: 60/دقيقة، Pro: 120/دقيقة، Enterprise: 600/دقيقة).
-
تحدي (⭐⭐⭐): صمم نظام أولوية كامل للبرمجيات الوسيطة — CORS → TenantResolve → Auth → SubscriptionActive → CheckRole → throttle — لضمان تنفيذ كل برمجية وسيطة في الوقت الصحيح. اكتب اختبارات للتحقق من أن طلبات الفحص المسبق OPTIONS لا يتم اعتراضها بواسطة المصادقة.



