الوصلات الوسيطة

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

نموذج بصل الوصلات الوسيطة


            ┌──────────────────────────────────────────────────────┐
            │                     middleware1                      │ 
            │     ┌──────────────────────────────────────────┐     │
            │     │               middleware2                │     │
            │     │     ┌──────────────────────────────┐     │     │
            │     │     │         middleware3          │     │     │        
            │     │     │     ┌──────────────────┐     │     │     │
            │     │     │     │                  │     │     │     │
   ── Reqeust ───────────────────> Controller ── Response ───────────────────────────> Client
            │     │     │     │                  │     │     │     │
            │     │     │     └──────────────────┘     │     │     │
            │     │     │                              │     │     │
            │     │     └──────────────────────────────┘     │     │
            │     │                                          │     │
            │     └──────────────────────────────────────────┘     │
            │                                                      │
            └──────────────────────────────────────────────────────┘

تشكل الوصلات الوسيطة والمت Controllers نموذج بصل كلاسيكي، حيث تشبه الوصلات الوسيطة طبقات قش البصل، بينما المتحكم هو قلب البصل. كما هو موضح في الشكل، يمر الطلب مثل السهم عبر الوصلات الوسيطة 1 و 2 و 3 حتى يصل إلى المتحكم، الذي يُرجع استجابة، ثم تمر الاستجابة مرة أخرى عبر الوصلات الوسيطة 3 و 2 و 1 قبل أن تعود إلى العميل. بمعنى آخر، يمكننا الحصول على كل من الطلب والاستجابة في كل وصلة وسيطة.

اعتراض الطلبات

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


            ┌────────────────────────────────────────────────────────────┐
            │                         middleware1                        │ 
            │     ┌────────────────────────────────────────────────┐     │
            │     │                   middleware2                  │     │
            │     │          ┌──────────────────────────────┐      │     │
            │     │          │        middleware3           │      │     │       
            │     │          │    ┌──────────────────┐      │      │     │
            │     │          │    │                  │      │      │     │
   ── Reqeust ─────────┐     │    │    Controller    │      │      │     │
            │     │ Response │    │                  │      │      │     │
   <───────────────────┘     │    └──────────────────┘      │      │     │
            │     │          │                              │      │     │
            │     │          └──────────────────────────────┘      │     │
            │     │                                                │     │
            │     └────────────────────────────────────────────────┘     │
            │                                                            │
            └────────────────────────────────────────────────────────────┘

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

واجهة الوصلات الوسيطة

يجب على الوصلات الوسيطة تنفيذ واجهة Webman\MiddlewareInterface.

interface MiddlewareInterface
{
    /**
     * Process an incoming server request.
     *
     * Processes an incoming server request in order to produce a response.
     * If unable to produce the response itself, it may delegate to the provided
     * request handler to do so.
     */
    public function process(Request $request, callable $handler): Response;
}

هذا يعني أنه يجب تنفيذ طريقة process، ويجب أن تُرجع طريقة process كائن support\Response، افتراضياً يتم إنشاء هذا الكائن بواسطة $handler($request) (الطلب سيستمر في الانتقال إلى قلب البصل)، ويمكن أيضاً أن يكون من الاستجابة التي أنشأتها دوال المساعدة مثل response() json() xml() redirect() (يتوقف الطلب عن الانتقال إلى قلب البصل).

الحصول على الطلب والاستجابة في الوصلات الوسيطة

داخل الوصلات الوسيطة، يمكننا الحصول على الطلب، كما يمكننا الحصول على الاستجابة بعد تنفيذ المتحكم، لذلك تنقسم الفعالية الداخلية للوصلات الوسيطة إلى ثلاثة أجزاء.

  1. مرحلة تجاوز الطلب، أي مرحلة ما قبل معالجة الطلب
  2. مرحلة معالجة الطلب بواسطة المتحكم، أي مرحلة معالجة الطلب
  3. مرحلة تجاوز الاستجابة، أي مرحلة ما بعد معالجة الطلب

تتمثل هذه المراحل الثلاث داخل الوصلات الوسيطة كما يلي:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Test implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        echo 'هذه هي مرحلة تجاوز الطلب، أي مرحلة ما قبل معالجة الطلب';

        $response = $handler($request); // يستمر في الانتقال إلى قلب البصل حتى يتم تنفيذ المتحكم والحصول على الاستجابة

        echo 'هذه هي مرحلة تجاوز الاستجابة، أي مرحلة ما بعد معالجة الطلب';

        return $response;
    }
}

مثال: وصلة وسيطة للتحقق من الهوية

أنشئ ملف app/middleware/AuthCheckTest.php (إذا كان الدليل غير موجود، يرجى إنشاءه) كما يلي:

<?php
namespace app\middleware;

use ReflectionClass;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class AuthCheckTest implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        if (session('user')) {
            // لقد تم تسجيل الدخول، الطلب يستمر في الانتقال إلى قلب البصل
            return $handler($request);
        }

        // من خلال الانعكاس للحصول على طرق المتحكم التي لا تحتاج إلى تسجيل دخول
        $controller = new ReflectionClass($request->controller);
        $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];

        // الطريقة التي يتم الوصول إليها تحتاج إلى تسجيل دخول
        if (!in_array($request->action, $noNeedLogin)) {
            // اعترض الطلب، وأرجع استجابة إعادة توجيه، توقف الطلب عن الانتقال إلى قلب البصل
            return redirect('/user/login');
        }

        // لا تحتاج إلى تسجيل دخول، الطلب يستمر في الانتقال إلى قلب البصل
        return $handler($request);
    }
}

إنشاء متحكم جديد app/controller/UserController.php

<?php
namespace app\controller;
use support\Request;

class UserController
{
    /**
     * طرق لا تحتاج إلى تسجيل دخول
     */
    protected $noNeedLogin = ['login'];

    public function login(Request $request)
    {
        $request->session()->set('user', ['id' => 10, 'name' => 'webman']);
        return json(['code' => 0, 'msg' => 'تسجيل الدخول بنجاح']);
    }

    public function info()
    {
        return json(['code' => 0, 'msg' => 'حسناً', 'data' => session('user')]);
    }
}

تنبيه
يحتوي $noNeedLogin على الطرق التي يمكن الوصول إليها في المتحكم الحالي دون الحاجة إلى تسجيل دخول

في config/middleware.php، أضف الوصلة الوسيطة العالمية كما يلي:

return [
    // الوصلات الوسيطة العالمية
    '' => [
        // ... تم إغفال الوصلات الوسيطة الأخرى هنا
        app\middleware\AuthCheckTest::class,
    ]
];

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

مثال: وصلة وسيطة لطلبات CORS

أنشئ ملف app/middleware/AccessControlTest.php (إذا كان الدليل غير موجود، يرجى إنشاءه) كما يلي:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class AccessControlTest implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        // إذا كانت هناك طلبات OPTIONS، يتم إرجاع استجابة فارغة، خلاف ذلك يستمر في الانتقال إلى قلب البصل ويحدث استجابة
        $response = $request->method() == 'OPTIONS' ? response('') : $handler($request);

        // إضافة رؤوس HTTP ذات الصلة بالتحكم في الوصول عبر الإنترنت إلى الاستجابة
        $response->withHeaders([
            'Access-Control-Allow-Credentials' => 'true',
            'Access-Control-Allow-Origin' => $request->header('origin', '*'),
            'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
            'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
        ]);

        return $response;
    }
}

تلميح
قد تؤدي الطلبات عبر الإنترنت إلى طلبات OPTIONS، ولا نريد أن تدخل طلبات OPTIONS إلى المتحكم، لذا قمنا بإرجاع استجابة فارغة مباشرة لهذه الطلبات (response('')) لتنفيذ الاعتراض على الطلبات.
إذا كانت واجهتك بحاجة لضبط التوجيه، يرجى استخدام Route::any(..) أو Route::add(['POST', 'OPTIONS'], ..) لضبطها.

في config/middleware.php، أضف الوصلة الوسيطة العالمية كما يلي:

return [
    // الوصلات الوسيطة العالمية
    '' => [
        // ... تم إغفال الوصلات الوسيطة الأخرى هنا
        app\middleware\AccessControlTest::class,
    ]
];

تنبيه
إذا كان طلب ajax قد حدد رأس مخصص، يجب إضافة هذا الرأس المخصص في حقل Access-Control-Allow-Headers في الوصلة الوسيطة، وإلا ستكون الاستجابة غير مسموح بها بـ Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.

الشرح

  • تتقسم الوصلات الوسيطة إلى وصلات وسائط عالمية، ووصلات وسائط تطبيقية (تكون فعالة فقط في وضع تعدد التطبيقات، انظر تعدد التطبيقات)، ووصلات وسائط توجيهية.
  • موقع ملف إعداد الوصلات الوسيطة هو config/middleware.php.
  • يتم إعداد الوصلات الوسيطة العالمية تحت المفتاح ''.
  • يتم إعداد الوصلات الوسيطة التطبيقية تحت اسم تطبيق معين، مثل:
return [
    // الوصلات الوسيطة العالمية
    '' => [
        app\middleware\AuthCheckTest::class,
        app\middleware\AccessControlTest::class,
    ],
    // الوصلات الوسيطة للتطبيق API (تكون فعالة فقط في وضع تعدد التطبيقات)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

الوصلات الوسيطة لطرق التحكم والوصلات الوسيطة للطرق

باستخدام التعليقات التوضيحية، يمكننا تعيين وصلة وسيطة معينة لمتحكم معين أو لطريقة المتحكم.

<?php
namespace app\controller;
use app\middleware\Controller1Middleware;
use app\middleware\Controller2Middleware;
use app\middleware\Method1Middleware;
use app\middleware\Method2Middleware;
use support\annotation\Middleware;
use support\Request;

#[Middleware(Controller1Middleware::class, Controller2Middleware::class)]
class IndexController
{
    #[Middleware(Method1Middleware::class, Method2Middleware::class)]
    public function index(Request $request): string
    {
        return 'hello';
    }
}

الوصلات الوسيطة للتوجيه

يمكننا تعيين وصلة وسيطة معينة لطلب أو مجموعة من الطلبات.
على سبيل المثال، في config/route.php، أضف التكوين التالي:

<?php
use support\Request;
use Webman\Route;

Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

Route::group('/blog', function () {
   Route::any('/create', function () {return response('create');});
   Route::any('/edit', function () {return response('edit');});
   Route::any('/view/{id}', function ($r, $id) {response("view $id");});
})->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

تمرير بارامترات إلى الوصلات الوسيطة من خلال الباني

يدعم ملف الإعداد إنشاء الوصلات الوسيطة مباشرة أو دوال غير مسماة، مما يتيح تمرير بارامترات إلى الوصلات الوسيطة من خلال الباني.
مثلما يوجد في config/middleware.php يمكن تكوينه بهذا الشكل:

return [
    // الوصلات الوسيطة العالمية
    '' => [
        new app\middleware\AuthCheckTest($param1, $param2, ...),
        function(){
            return new app\middleware\AccessControlTest($param1, $param2, ...);
        },
    ],
    // الوصلات الوسيطة للتطبيق API (تكون فعالة فقط في وضع تعدد التطبيقات)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

على نفس المنوال، يمكن أيضاً تمرير بارامترات إلى الوصلات الوسيطة من خلال الباني مثلما هو الحال في config/route.php:

Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
    new app\middleware\MiddlewareA($param1, $param2, ...),
    function(){
        return new app\middleware\MiddlewareB($param1, $param2, ...);
    },
]);

مثال على استخدام البارامترات في الوصلات الوسيطة

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class MiddlewareA implements MiddlewareInterface
{
    protected $param1;

    protected $param2;

    public function __construct($param1, $param2)
    {
        $this->param1 = $param1;
        $this->param2 = $param2;
    }

    public function process(Request $request, callable $handler) : Response
    {
        var_dump($this->param1, $this->param2);
        return $handler($request);
    }
}

ترتيب تنفيذ الوصلات الوسيطة

  • يتم تنفيذ الوصلات الوسيطة بترتيب الوصلة الوسيطة العالمية -> الوصلة الوسيطة التطبيقية -> الوصلة الوسيطة للمتحكم -> الوصلة الوسيطة للتوجيه -> الوصلة الوسيطة للطرق.
  • عند وجود عدة وصلات وسيطة في نفس المستوى، يتم تنفيذها وفقاً لترتيب الإعداد الفعلي في نفس المستوى.
  • بشكل افتراضي، لن تؤدي الطلبات التي تعود برمز الحالة 404 إلى تفعيل أي وصلة وسيطة (على الرغم من أنه لا يزال بإمكانك إضافة وصلة وسيطة عبر Route::fallback(function(){})->middleware()).

تمرير البارامترات إلى الوصلات الوسيطة عبر التوجيه (route->setParams)

تكوين التوجيه config/route.php

<?php
use support\Request;
use Webman\Route;

Route::any('/test', [app\controller\IndexController::class, 'index'])->setParams(['some_key' =>'some value']);

الوصلة الوسيطة (فرضاً أنها وصلة وسيطة عالمية)

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        // افتراضياً، $request->route تكون null، لذا تحتاج إلى التحقق مما إذا كانت $request->route فارغة
        if ($route = $request->route) {
            $value = $route->param('some_key');
            var_export($value);
        }
        return $handler($request);
    }
}

تمرير البيانات إلى المتحكم من الوصلات الوسيطة

في بعض الأحيان، يحتاج المتحكم إلى استخدام البيانات التي تم إنشاؤها داخل الوصلة الوسيطة؛ يمكننا فعل ذلك عن طريق إضافة خاصية إلى كائن $request لتمرير البيانات إلى المتحكم. على سبيل المثال:

الوصلة الوسيطة

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $request->data = 'some value';
        return $handler($request);
    }
}

المتحكم:

<?php
namespace app\controller;

use support\Request;

class FooController
{
    public function index(Request $request)
    {
        return response($request->data);
    }
}

الحصول على معلومات توجيه الطلب الحالي من الوصلات الوسيطة

يمكننا استخدام $request->route للحصول على كائن التوجيه، من خلال استدعاء الطرق المناسبة للحصول على المعلومات ذات الصلة.

تكوين التوجيه

<?php
use support\Request;
use Webman\Route;

Route::any('/user/{uid}', [app\controller\UserController::class, 'view']);

الوصلة الوسيطة

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $route = $request->route;
        // إذا لم يتطابق الطلب مع أي توجيه (باستثناء التوجيه الافتراضي)، فإن $request->route تكون null
        // على سبيل المثال، إذا زار المتصفح العنوان /user/111، فسيتم طباعة المعلومات التالية:
        if ($route) {
            var_export($route->getPath());       // /user/{uid}
            var_export($route->getMethods());    // ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD','OPTIONS']
            var_export($route->getName());       // user_view
            var_export($route->getMiddleware()); // []
            var_export($route->getCallback());   // ['app\\controller\\UserController', 'view']
            var_export($route->param());         // ['uid'=>111]
            var_export($route->param('uid'));    // 111 
        }
        return $handler($request);
    }
}

تنبيه

الحصول على استثناءات من الوصلات الوسيطة

قد تحدث استثناءات أثناء معالجة الطلبات. يمكنك استخدام $response->exception() للحصول على الاستثناءات في الوصلات الوسيطة.

تكوين التوجيه

<?php
use support\Request;
use Webman\Route;

Route::any('/user/{uid}', function (Request $request, $uid) {
    throw new \Exception('استثناء اختبار');
});

الوصلة الوسيطة:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $response = $handler($request);
        $exception = $response->exception();
        if ($exception) {
            echo $exception->getMessage();
        }
        return $response;
    }
}

الوصلات الوسيطة العالمية الشاملة

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

قم بتكوينها في config/middleware.php كما يلي:

return [
    '@' => [ // إضافة وصلة وسائط عالمية للمشروع الرئيسي وجميع الإضافات
        app\middleware\MiddlewareGlobl::class,
    ], 
    '' => [], // إضافة وصلة وسائط عالمية للمشروع الرئيسي فقط
];

تلميح
يمكن تكوين الوصلات الوسيطة العالمية الشاملة @ ليس فقط في مشروع رئيسي، بل يمكن تكوينها أيضاً في أي إضافة، مثل أن يتم تعيين الوصلات الوسيطة العالمية الشاملة @ في plugin/ai/config/middleware.php، فإن ذلك سيؤثر أيضاً على المشروع الرئيسي وجميع الإضافات.

إضافة وصلة وسائط إلى إضافة معينة

في بعض الأحيان، نريد إضافة وصلة وسائط معينة إلى الإضافات التطبيقية دون تغيير كود الإضافة (لأن الترقية ستؤدي إلى تجاوزها)، يمكننا ذلك من خلال تهيئة الوصلة الوسيطة في المشروع الرئيسي.

قم بتكوينها في config/middleware.php كما يلي:

return [
    'plugin.ai' => [], // إضافة وصلة وسائط إلى إضافة ai
    'plugin.ai.admin' => [], // إضافة وصلة وسائط إلى وحدة الإدارة لإضافة ai (دليل plugin\ai\app\admin)
];

تلميح
يمكن بالطبع إضافة تكوين مماثل إلى إضافة جهة خارجية للتأثير على الإضافات الأخرى، مثلما يتم إضافة التكوين في plugin/foo/config/middleware.php، فإن ذلك سيؤثر على إضافة ai.