الـ Middleware في لارافيل هي الطبقة التي تمرّ عبرها كل طلبات HTTP قبل أن تصل إلى الكونترولر، وبعد أن تخرج منه في طريقها إلى المتصفح. في هذا الدليل سنغطي مفهوم الـ middleware، وكيفية إنشائه وتسجيله في لارافيل 11 و12 و13 (وأيضاً في الإصدارات الأقدم التي ما زالت تستخدم Kernel.php)، مروراً بتمرير الوسائط، وترتيب التنفيذ، وterminable middleware، وصولاً إلى الأخطاء الشائعة وأفضل الممارسات.

ما هو الـ middleware؟

يمكن تعريف الـ middleware بأنه بوابة أو جسر بين الطلب request والاستجابة response؛ فهو بمثابة فلتر يقرر: هل يُسمح لهذا الطلب بالمرور والحصول على الاستجابة أم لا؟ إنه طبقة إضافية تعترض الطلب قبل وصوله إلى منطق التطبيق.

تخيّل حارس عمارة: أنت طلبت الدخول إلى العمارة (request)، فيتأكد الحارس من هويتك وسبب دخولك، وبناءً على ذلك يقرر السماح لك بالدخول أو ردّك من الباب (response). الحارس هنا هو الـ middleware.

المثال الأشهر الذي نواجهه في كل تطبيق تقريباً هو auth middleware: عند محاولة الوصول إلى صفحة محمية، يتحقق الـ middleware من كون المستخدم مسجّل الدخول، فإن لم يكن كذلك أعاد توجيهه إلى صفحة تسجيل الدخول، وإن كان مسجلاً سمح للطلب بالمرور إلى الكونترولر.

أين يقع الـ middleware في دورة حياة الطلب؟

تخيّل الـ middleware كطبقات متتالية يمرّ الطلب عبرها ذهاباً، ثم تمرّ الاستجابة عبرها إياباً:

Request
   ↓
[ Global Middleware ]        ← يعمل على كل طلب
   ↓
[ Group Middleware: web / api ]
   ↓
[ Route Middleware: auth, verified, ... ]
   ↓
Controller / Closure
   ↓
[ Route Middleware ]         ← الجزء الذي يلي ()next$ في كل middleware
   ↓
[ Group Middleware ]
   ↓
[ Global Middleware ]
   ↓
Response → المتصفح
   ↓
[ terminate() ]              ← بعد إرسال الاستجابة

الفكرة المهمة هنا: كل middleware يستطيع إيقاف السلسلة بإرجاع استجابة مباشرة (redirect أو abort) دون استدعاء $next($request)، وحينها لن يصل الطلب إلى الكونترولر أصلاً.

متى تستخدم الـ middleware ومتى لا تستخدمه؟

حالات مناسبة لاستخدام الـ middleware

  • التحقق من تسجيل الدخول والصلاحيات (authentication & authorization).
  • ضبط لغة الموقع في التطبيقات متعددة اللغات (setLocale).
  • تفعيل وضع الصيانة maintenance mode.
  • تتبّع المستخدمين وتسجيل الطلبات (logging / analytics).
  • إضافة ترويسات أمان إلى الاستجابة (Security Headers، CORS).
  • تحديد معدل الطلبات rate limiting لحماية الـ API.
  • فرض HTTPS أو إعادة التوجيه بناءً على الجهاز أو الدولة.
  • التحقق من الاشتراك أو من تفعيل البريد الإلكتروني قبل الوصول إلى مجموعة صفحات.

حالات لا يُنصح فيها بالـ middleware

  • التحقق من صحة المدخلات: مكانها الطبيعي هو Form Request وليس الـ middleware.
  • صلاحيات مرتبطة بسجل معيّن (هل يملك هذا المستخدم هذا المقال تحديداً؟): استخدم Gates وPolicies، لأن الـ middleware يعمل قبل ربط النماذج بشكل كامل ولا يعرف سياق السجل.
  • منطق العمل (business logic): مكانه الكونترولر أو Service class؛ الـ middleware يجب أن يبقى قراراً بسيطاً: مرِّر أو اقطع.
  • عمليات ثقيلة أو بطيئة: إرسال بريد، استدعاء API خارجي، أو استعلامات مكلفة — استخدم Queue أو terminate().

كما يأتي لارافيل مع مجموعة من الـ middleware المبنية مسبقاً داخل الإطار نفسه، مثل التعامل مع مدخلات الطلب (TrimStrings, ConvertEmptyStringsToNull)، والتحقق من وضع الصيانة (PreventRequestsDuringMaintenance)، وحماية النماذج من التزوير.

إنشاء middleware في لارافيل

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

ننشئ الـ middleware باستخدام أمر artisan:

php artisan make:middleware MobileRedirect

بعد تنفيذ الأمر سيُنشأ كلاس باسم MobileRedirect.php في المسار:

app/Http/Middleware/MobileRedirect.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class MobileRedirect
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }
}

الدالة handle هي العمود الفقري للـ middleware؛ بداخلها نضع المنطق الذي يقرر مصير الطلب. انتبه إلى نقطتين مهمتين في التوقيع:

  • نوع الإرجاع هو Symfony\Component\HttpFoundation\Response وليس Illuminate\Http\Response، لأن استجابات لارافيل جميعها ترث من كلاس Symfony، وهذا يشمل الـ redirect والـ JSON والـ streams.
  • يتم حل الـ middleware عبر Service Container، أي يمكنك عمل type-hint لأي اعتمادية في الـ constructor وسيقوم لارافيل بحقنها تلقائياً.

أنواعه: before middleware و after middleware

يوجد نمطان أساسيان لكتابة الـ middleware، والفرق بينهما هو موضع الكود بالنسبة لاستدعاء $next($request):

Before middleware

ينفّذ المنطق قبل أن يعالج التطبيق الطلب، ويُستخدم للفحص والاعتراض وإعادة التوجيه.

public function handle(Request $request, Closure $next): Response
{
    // الكود هنا ينفّذ قبل وصول الطلب إلى الكونترولر
    if (! $request->user()?->is_active) {
        return redirect('/suspended');
    }

    return $next($request);
}

After middleware

ينفّذ المنطق بعد أن يعالج التطبيق الطلب ويُنتج الاستجابة، ويُستخدم لتعديل الاستجابة نفسها.

public function handle(Request $request, Closure $next): Response
{
    $response = $next($request);

    // الكود هنا ينفّذ بعد توليد الاستجابة وقبل إرسالها
    $response->headers->set('X-Frame-Options', 'SAMEORIGIN');

    return $response;
}

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

مثال عملي: إعادة توجيه زوّار الهاتف إلى نطاق فرعي

أبسط صيغة تعتمد على قيمة في الـ query string:

public function handle(Request $request, Closure $next): Response
{
    if ($request->query('mobile') === '1') {
        return redirect()->away('https://m.example.test');
    }

    return $next($request);
}

الآن عند زيارة https://example.test/?mobile=1 سيُنفَّذ الـ middleware ويعيد توجيهك إلى النطاق الفرعي، وفي حال غياب المعامل سيُستدعى $next($request) ويكمل الطلب طريقه الطبيعي.

لكن الاعتماد على query string ليس حلاً واقعياً في الإنتاج، لأن الزائر لن يضيفه بنفسه. الحل العملي هو فحص User-Agent، مع الانتباه إلى ثلاث نقاط: تجاهل عناكب محركات البحث حتى لا تتأثر أرشفة الموقع، وإتاحة خيار «النسخة الكاملة» للمستخدم، وتجنّب الدخول في حلقة إعادة توجيه لا نهائية.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class MobileRedirect
{
    private const MOBILE_URL = 'https://m.example.test';

    public function handle(Request $request, Closure $next): Response
    {
        // لا نعيد توجيه عناكب محركات البحث ولا طلبات الـ API
        if ($request->is('api/*') || $this->isBot($request)) {
            return $next($request);
        }

        // احترام رغبة المستخدم إذا اختار النسخة الكاملة
        if ($request->cookie('force_desktop')) {
            return $next($request);
        }

        if ($this->isMobile($request)) {
            return redirect()->away(self::MOBILE_URL . $request->getRequestUri(), 302);
        }

        return $next($request);
    }

    private function isMobile(Request $request): bool
    {
        return (bool) preg_match(
            '/Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini|Windows Phone/i',
            (string) $request->userAgent()
        );
    }

    private function isBot(Request $request): bool
    {
        return (bool) preg_match(
            '/bot|crawl|slurp|spider|facebookexternalhit/i',
            (string) $request->userAgent()
        );
    }
}

ملاحظة مهمة لتحسين محركات البحث: عند وجود نطاق فرعي للهواتف يُفضّل استخدام إعادة توجيه مؤقتة 302 وليس 301، مع إضافة وسم canonical في نسخة الهاتف يشير إلى نسخة سطح المكتب. والأفضل من ذلك كله في المشاريع الجديدة هو التصميم المتجاوب بدلاً من نطاق منفصل.

تسجيل الـ middleware (لارافيل 11 وما بعده)

حتى الآن أنشأنا الـ middleware وكتبنا منطق العمل، لكن لارافيل لا يعلم بوجوده بعد. ابتداءً من لارافيل 11 حُذف ملف app/Http/Kernel.php نهائياً، وانتقل كل إعداد الـ middleware إلى ملف bootstrap/app.php عبر الدالة withMiddleware(). هذا هو الشكل العام للملف:

<?php

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

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

1. التسجيل العام (Global Middleware)

إذا أردت تنفيذ الـ middleware على كل طلب في التطبيق:

use App\Http\Middleware\MobileRedirect;

->withMiddleware(function (Middleware $middleware): void {
    // إضافته في نهاية السلسلة
    $middleware->append(MobileRedirect::class);

    // أو في بدايتها ليعمل أولاً
    $middleware->prepend(MobileRedirect::class);
})

الكائن $middleware هو نسخة من Illuminate\Foundation\Configuration\Middleware، وهو المسؤول عن إدارة كل الـ middleware في التطبيق.

وإذا احتجت التحكم الكامل بسلسلة الـ middleware العامة الافتراضية (لحذف أحدها أو إعادة ترتيبها)، استخدم الدالة use ومرّر لها السلسلة كاملة:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->use([
        \Illuminate\Foundation\Http\Middleware\InvokeDeferredCallbacks::class,
        // \Illuminate\Http\Middleware\TrustHosts::class,
        \Illuminate\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Http\Middleware\ValidatePostSize::class,
        \Illuminate\Foundation\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ]);
})

2. تسجيل اختصار (Alias)

غالباً لا نريد تنفيذ الـ middleware على كل طلب، بل على مسارات محددة. في هذه الحالة نسجّل له اسماً مختصراً بدلاً من كتابة اسم الكلاس الكامل في كل مرة:

use App\Http\Middleware\MobileRedirect;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'mobile.redirect' => MobileRedirect::class,
    ]);
})

ثم نستخدم الاختصار في ملف المسارات:

Route::get('/contact', [ContactController::class, 'index'])
    ->middleware('mobile.redirect');

وتسجيل الاختصار اختياري تماماً؛ يمكنك دائماً تمرير اسم الكلاس مباشرة: ->middleware(MobileRedirect::class).

تسجيل الـ middleware في لارافيل 10 وما قبله

إذا كنت تعمل على مشروع قديم، فالتسجيل يتم في ملف app/Http/Kernel.php عبر خصائص الكلاس. للتسجيل العام:

// app/Http/Kernel.php

protected $middleware = [
    \App\Http\Middleware\TrustProxies::class,
    \Illuminate\Http\Middleware\HandleCors::class,
    \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,

    \App\Http\Middleware\MobileRedirect::class, // الـ middleware الخاص بنا
];

وللتسجيل باسم مختصر يُستخدم على مسارات محددة:

// لارافيل 10: $middlewareAliases
// لارافيل 9 وما قبله: $routeMiddleware

protected $middlewareAliases = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,

    'mobile.redirect' => \App\Http\Middleware\MobileRedirect::class,
];

جدول مقارنة سريع

أين تسجّل الـ middleware حسب إصدار لارافيل
الغرض لارافيل 10 وما قبله لارافيل 11 / 12 / 13
ملف الإعداد app/Http/Kernel.php bootstrap/app.php
middleware عام $middleware $middleware->append() / prepend()
اسم مختصر $routeMiddleware ثم $middlewareAliases $middleware->alias([...])
المجموعات $middlewareGroups $middleware->appendToGroup() / web() / api()
الأولوية $middlewarePriority $middleware->priority([...])
داخل الكونترولر $this->middleware() في الـ constructor HasMiddleware أو #[Middleware]

تعيين الـ middleware على المسارات Routes

على مسار واحد

use App\Http\Middleware\MobileRedirect;

Route::get('/contact', [ContactController::class, 'index'])
    ->middleware(MobileRedirect::class);

أكثر من middleware على المسار نفسه

Route::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware(['auth', 'verified', 'mobile.redirect']);

على مجموعة مسارات

ماذا لو كان لديك خمسة مسارات تريد تطبيق الـ middleware نفسه عليها؟ تكرار ->middleware('mobile.redirect') على كل مسار يعمل بلا مشاكل، لكنه تكرار يمكن اختصاره باستخدام مجموعة:

// بدلاً من التكرار
Route::get('/contact', [ContactController::class, 'index'])->middleware('mobile.redirect');
Route::get('/about', [AboutController::class, 'index'])->middleware('mobile.redirect');
Route::get('/cats', [CatController::class, 'index'])->middleware('mobile.redirect');

// استخدم مجموعة
Route::middleware('mobile.redirect')->group(function () {
    Route::get('/contact', [ContactController::class, 'index']);
    Route::get('/about', [AboutController::class, 'index']);
    Route::get('/cats', [CatController::class, 'index']);
});

ويمكن دمج الـ middleware مع بادئة للمسار واسم للمجموعة:

Route::middleware(['auth', 'role:admin'])
    ->prefix('admin')
    ->name('admin.')
    ->group(function () {
        Route::get('/users', [UserController::class, 'index'])->name('users.index');
    });

استثناء مسار من الـ middleware

أحياناً تريد استثناء مسار واحد من middleware مطبّق على المجموعة كلها، وهنا تأتي الدالة withoutMiddleware:

Route::middleware([EnsureTokenIsValid::class])->group(function () {
    Route::get('/', HomeController::class);

    Route::get('/profile', [ProfileController::class, 'show'])
        ->withoutMiddleware([EnsureTokenIsValid::class]);
});

انتبه: withoutMiddleware يعمل على middleware المسارات فقط، ولا يستطيع إلغاء الـ global middleware. لهذا السبب إن كان الـ middleware يحتاج استثناءات، فلا تسجّله عالمياً منذ البداية.

مجموعات الـ middleware والمجموعتان web و api

يمكنك تجميع عدة middleware تحت مفتاح واحد ليسهل تطبيقها معاً:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->appendToGroup('admin-area', [
        \App\Http\Middleware\EnsureUserIsAdmin::class,
        \App\Http\Middleware\LogAdminActivity::class,
    ]);
})
Route::middleware('admin-area')->group(function () {
    // ...
});

ويحتوي لارافيل على مجموعتين جاهزتين هما web وapi، وتُطبَّقان تلقائياً على ملفَّي routes/web.php وroutes/api.php على التوالي. مجموعة web تضم إدارة الجلسات والكوكيز والحماية من تزوير الطلبات وربط النماذج، بينما تضم مجموعة api ربط النماذج فقط. ولإضافة middleware إليهما:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [
        \App\Http\Middleware\SetLocale::class,
    ]);

    $middleware->api(prepend: [
        \App\Http\Middleware\EnsureTokenIsValid::class,
    ]);

    // استبدال أحد عناصر المجموعة بآخر
    $middleware->web(replace: [
        \Illuminate\Session\Middleware\StartSession::class => \App\Http\Middleware\StartCustomSession::class,
    ]);

    // أو حذفه نهائياً
    $middleware->web(remove: [
        \Illuminate\Session\Middleware\StartSession::class,
    ]);
})

تسجيل الـ middleware داخل الكونترولر

هناك ثلاث حالات لتطبيق الـ middleware على دوال الكونترولر:

  • على جميع الدوال دون استثناء.
  • only: يُطبّق فقط على الدوال المحددة.
  • except: يُطبّق على جميع الدوال باستثناء الدوال المحددة.

الطريقة الحديثة: واجهة HasMiddleware

الطريقة القديمة كانت استدعاء $this->middleware() داخل الـ constructor، لكنها حُذفت ابتداءً من لارافيل 11. البديل هو تطبيق واجهة HasMiddleware التي تفرض وجود دالة ثابتة باسم middleware:

<?php

namespace App\Http\Controllers;

use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;

class PostController extends Controller implements HasMiddleware
{
    /**
     * Get the middleware that should be assigned to the controller.
     */
    public static function middleware(): array
    {
        return [
            'auth',                                        // على جميع الدوال
            new Middleware('log', only: ['index']),        // على index فقط
            new Middleware('subscribed', except: ['store']), // على الجميع عدا store
        ];
    }

    public function index() { /* ... */ }
    public function store() { /* ... */ }
}

ويمكن أيضاً تمرير middleware على شكل closure مباشرة دون إنشاء كلاس مستقل:

use Closure;
use Illuminate\Http\Request;

public static function middleware(): array
{
    return [
        function (Request $request, Closure $next) {
            // منطق بسيط لا يستحق كلاساً كاملاً
            return $next($request);
        },
    ];
}

الأحدث: PHP Attributes في لارافيل 13

أضاف لارافيل 13 طريقة أوضح باستخدام الـ attributes، بحيث تكتب قواعد الحماية فوق الكلاس أو فوق الدالة مباشرة، فتصبح مرئية في مكانها الطبيعي:

<?php

namespace App\Http\Controllers;

use Illuminate\Routing\Attributes\Controllers\Middleware;

#[Middleware('auth')]
class PostController extends Controller
{
    #[Middleware('permission:create articles')]
    public function create() { /* ... */ }

    public function index() { /* ... */ }
}

الطريقة القديمة (لارافيل 10 وما قبله)

public function __construct()
{
    $this->middleware('auth');                          // على جميع الدوال
    $this->middleware('auth')->only(['show']);          // على show فقط
    $this->middleware('auth')->except(['show']);        // على الجميع عدا show
}

تمرير وسائط إلى الـ middleware

كثيراً ما تحتاج middleware واحداً يخدم عدة حالات، مثل التحقق من دور المستخدم. تُمرَّر الوسائط الإضافية بعد المعامل $next مباشرة:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureUserHasRole
{
    public function handle(Request $request, Closure $next, string $role): Response
    {
        if (! $request->user()->hasRole($role)) {
            abort(403);
        }

        return $next($request);
    }
}

وتُمرَّر القيمة في المسار بعد نقطتين رأسيتين :، وتُفصل القيم المتعددة بفاصلة:

Route::put('/post/{id}', [PostController::class, 'update'])
    ->middleware(EnsureUserHasRole::class.':editor');

Route::put('/post/{id}', [PostController::class, 'update'])
    ->middleware(EnsureUserHasRole::class.':editor,publisher');

// أو باستخدام الاختصار
Route::put('/post/{id}', [PostController::class, 'update'])
    ->middleware('role:editor,publisher');

ولاستقبال عدد غير محدد من الأدوار استخدم المعاملات المتغيرة:

public function handle(Request $request, Closure $next, string ...$roles): Response
{
    if (! $request->user()->hasAnyRole($roles)) {
        abort(403);
    }

    return $next($request);
}

الـ Terminable Middleware

أحياناً تحتاج تنفيذ عمل بعد إرسال الاستجابة إلى المتصفح فعلياً، مثل تسجيل إحصائيات أو كتابة سجلات، دون أن ينتظر المستخدم انتهاء هذا العمل. عرّف دالة terminate في الـ middleware، وسيستدعيها لارافيل تلقائياً بعد إرسال الاستجابة (بشرط أن يستخدم الخادم FastCGI مثل PHP-FPM):

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class LogRequestDuration
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    /**
     * Handle tasks after the response has been sent to the browser.
     */
    public function terminate(Request $request, Response $response): void
    {
        Log::info('request.finished', [
            'uri'    => $request->getRequestUri(),
            'status' => $response->getStatusCode(),
        ]);
    }
}

ملاحظة دقيقة: عند استدعاء terminate ينشئ لارافيل نسخة جديدة من الـ middleware من الـ container. فإن أردت أن تكون النسخة نفسها المستخدمة في handle (لتمرير حالة بينهما)، سجّله كـ singleton في AppServiceProvider:

public function register(): void
{
    $this->app->singleton(\App\Http\Middleware\LogRequestDuration::class);
}

ترتيب التنفيذ والأولوية priority

يُنفَّذ الـ middleware واحداً تلو الآخر حسب ترتيب تسجيله: العام أولاً، ثم المجموعة، ثم middleware المسار. والترتيب ليس تفصيلاً شكلياً — تخيّل middleware للصلاحيات يعمل قبل middleware المصادقة، عندها سيحاول قراءة $request->user() وهي فارغة.

في الحالات النادرة التي تحتاج فيها فرض ترتيب معيّن بغض النظر عن ترتيب كتابته في المسار، استخدم priority:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->priority([
        \Illuminate\Cookie\Middleware\EncryptCookies::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
        \App\Http\Middleware\EnsureUserHasRole::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ]);
})

ولإدراج middleware في القائمة الحالية دون استبدالها كلها:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->prependToPriorityList(
        before: \Illuminate\Routing\Middleware\SubstituteBindings::class,
        prepend: \App\Http\Middleware\EnsureTokenIsValid::class,
    );

    $middleware->appendToPriorityList(
        after: \Illuminate\Routing\Middleware\SubstituteBindings::class,
        append: \App\Http\Middleware\EnsureUserIsSubscribed::class,
    );
})

الاختصارات (Aliases) الجاهزة في لارافيل

يوفّر لارافيل مجموعة اختصارات جاهزة يمكنك استخدامها مباشرة دون تسجيل:

أشهر اختصارات الـ middleware الجاهزة
الاختصار الكلاس الوظيفة
auth Illuminate\Auth\Middleware\Authenticate يشترط تسجيل الدخول
guest Illuminate\Auth\Middleware\RedirectIfAuthenticated للزوّار غير المسجلين فقط
verified Illuminate\Auth\Middleware\EnsureEmailIsVerified يشترط تفعيل البريد الإلكتروني
can Illuminate\Auth\Middleware\Authorize يتحقق من صلاحية عبر Gate أو Policy
throttle Illuminate\Routing\Middleware\ThrottleRequests تحديد معدل الطلبات
signed Illuminate\Routing\Middleware\ValidateSignature التحقق من توقيع الرابط
password.confirm Illuminate\Auth\Middleware\RequirePassword يطلب تأكيد كلمة المرور
cache.headers Illuminate\Http\Middleware\SetCacheHeaders ضبط ترويسات الكاش

مثال على الاستخدام المباشر:

Route::post('/comments', [CommentController::class, 'store'])
    ->middleware(['auth', 'verified', 'throttle:10,1']);

اختبار الـ middleware

الـ middleware جزء من طبقة الحماية، ويستحق اختباراً. أسهل طريقة هي اختبار المسار المحمي نفسه:

<?php

use App\Models\User;

it('redirects mobile visitors to the mobile subdomain', function () {
    $this->withServerVariables(['HTTP_USER_AGENT' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)'])
        ->get('/contact')
        ->assertRedirect('https://m.example.test/contact');
});

it('allows desktop visitors through', function () {
    $this->withServerVariables(['HTTP_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'])
        ->get('/contact')
        ->assertOk();
});

it('blocks guests from the dashboard', function () {
    $this->get('/dashboard')->assertRedirect('/login');

    $this->actingAs(User::factory()->create())
        ->get('/dashboard')
        ->assertOk();
});

وإذا أردت تعطيل الـ middleware مؤقتاً أثناء اختبار شيء آخر، استخدم $this->withoutMiddleware() — لكن لا تجعلها عادة، فأنت بذلك تختبر تطبيقاً مختلفاً عن الحقيقي.

أخطاء شائعة وحلولها

  • الـ middleware لا يعمل إطلاقاً: غالباً لم يُسجَّل. تحقق من bootstrap/app.php (أو Kernel.php في الإصدارات القديمة)، ومن تطابق اسم الاختصار حرفياً بين التسجيل والمسار.
  • ما زال السلوك القديم يظهر: المسارات مخزّنة في الكاش. نفّذ php artisan route:clear وphp artisan config:clear.
  • نسيان return قبل $next($request): النتيجة صفحة بيضاء فارغة، لأن الـ middleware لم يُرجع أي استجابة.
  • حلقة إعادة توجيه لا نهائية: يحدث عندما يعيد الـ middleware التوجيه إلى مسار محمي بالـ middleware نفسه. استثنِ مسار الوجهة دائماً.
  • $request->user() فارغة: الـ middleware يعمل قبل StartSession أو قبل المصادقة. راجع الترتيب أو استخدم priority.
  • خطأ في نوع الإرجاع: استخدم Symfony\Component\HttpFoundation\Response وليس Illuminate\Http\Response في التوقيع.
  • الخلط بين redirect() وabort(403): استخدم إعادة التوجيه عندما يستطيع المستخدم فعل شيء لتصحيح الوضع (تسجيل الدخول مثلاً)، واستخدم abort عندما يكون مصادقاً لكنه ببساطة غير مخوّل.
  • middleware عام يعمل على ملفات الـ assets والـ API: راجع هل تحتاجه عالمياً فعلاً، أم يكفي على مجموعة web.

محاذير وأفضل الممارسات

  • الأداء أولاً: الـ middleware العام يعمل على كل طلب، فإن استهلك موارد كثيرة (استعلام قاعدة بيانات، نداء API خارجي) فسيبطئ التطبيق كله. استخدم الكاش، أو انقل العملية إلى Queue أو إلى terminate().
  • الترتيب مهم: ضع الـ middleware الخفيف والسريع في الأعلى، والذي يرفض أكبر عدد من الطلبات مبكراً أولاً، حتى لا تُنفَّذ عمليات ثقيلة لطلب سيُرفض في النهاية.
  • مسؤولية واحدة لكل middleware: middleware يتحقق من الدور، وآخر يسجّل النشاط. لا تحشر كل شيء في كلاس واحد.
  • لا تعدّل الطلب بصمت: تغيير قيم $request داخل middleware يجعل تتبع الأخطاء لاحقاً صعباً جداً.
  • احذر التعارض مع مكونات الإطار: لارافيل مبني فوق مكوّنات Symfony، وعند كتابة middleware أنت تكتب تحت الإطار الأساسي؛ فتجنّب لمس الجلسة أو الكوكيز أو الترويسات بطريقة تتعارض مع الـ middleware المدمج.
  • اختر المستوى المناسب: عام للسلوك الذي لا استثناء له، ومجموعة للسلوك المشترك بين قسم كامل من الموقع، ومسار للحالات الخاصة.
  • لا تضع صلاحيات دقيقة في الـ middleware: استخدم Policies عندما يعتمد القرار على السجل نفسه.
  • اكتب اختباراً لكل middleware أمني: أي middleware يمنع الوصول يجب أن يكون له اختبار يثبت أنه يمنع فعلاً.

الخلاصة

ملخّص العمل مع الـ middleware في أربع خطوات:

1. الإنشاء

php artisan make:middleware MobileRedirect

2. تحديد النوع: before أم after

public function handle(Request $request, Closure $next): Response
{
    // before: منطق يُنفّذ قبل الكونترولر
    if ($request->query('mobile') === '1') {
        return redirect()->away('https://m.example.test');
    }

    $response = $next($request);

    // after: منطق يُنفّذ بعد توليد الاستجابة
    return $response;
}

3. التسجيل

// لارافيل 11 وما بعده — bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->append(\App\Http\Middleware\MobileRedirect::class);      // عام
    $middleware->alias(['mobile.redirect' => \App\Http\Middleware\MobileRedirect::class]); // اختصار
})

// لارافيل 10 وما قبله — app/Http/Kernel.php
protected $middleware = [
    \App\Http\Middleware\MobileRedirect::class,
];

protected $middlewareAliases = [
    'mobile.redirect' => \App\Http\Middleware\MobileRedirect::class,
];

4. التطبيق

// على مسار
Route::get('/contact', [ContactController::class, 'index'])->middleware('mobile.redirect');

// على مجموعة
Route::middleware('mobile.redirect')->group(function () {
    Route::get('/contact', [ContactController::class, 'index']);
    Route::get('/about', [AboutController::class, 'index']);
});

// داخل الكونترولر (لارافيل 11+)
class PostController extends Controller implements HasMiddleware
{
    public static function middleware(): array
    {
        return [
            'auth',
            new Middleware('mobile.redirect', only: ['show']),
            new Middleware('subscribed', except: ['index']),
        ];
    }
}

أسئلة شائعة

ما الفرق بين الـ middleware والـ Form Request؟

الـ middleware يقرر هل يُسمح للطلب بالوصول أصلاً، أما الـ Form Request فيتحقق من صحة البيانات بعد وصول الطلب إلى الكونترولر. الأول بوابة، والثاني مدقق مدخلات.

ما الفرق بين الـ middleware والـ Gates و Policies؟

الـ middleware مناسب للقرارات العامة على مستوى المسار (هل المستخدم مسجّل؟ هل هو مشرف؟)، بينما الـ Policies مناسبة للقرارات المرتبطة بسجل بعينه (هل يملك هذا المستخدم هذا المقال؟).

هل يمكن تنفيذ middleware على الـ API فقط؟

نعم، عبر $middleware->api(append: [...]) في bootstrap/app.php، أو بتطبيقه على مجموعة مسارات داخل routes/api.php.

أين ذهب ملف Kernel.php؟

حُذف في لارافيل 11 ضمن تبسيط بنية التطبيق، وانتقلت إعداداته إلى bootstrap/app.php. المشاريع المُرقّاة من لارافيل 10 قد تحتفظ بالملف وهو ما زال يعمل، لكن المشاريع الجديدة لا تحتوي عليه.

كم عدد الـ middleware الذي يمكن تطبيقه على مسار واحد؟

لا يوجد حد تقني، لكن كل middleware يضيف وقتاً إلى الطلب. إن وجدت نفسك تطبّق ستة أو سبعة على المسار نفسه، فغالباً بعضها يستحق أن يُدمج في مجموعة واحدة.