Discuz! Q 原始碼淺析

xing393939發表於2020-07-21

前言

簡單分析了一下Discuz! Q 的原始碼,純屬個人學習,不對的地方望指正哈。

用到的laravel元件

在vendor/discuz/core/composer.json中可以看到

  • “illuminate/container”: ioc服務容器
  • “illuminate/database”: 資料庫
  • “illuminate/bus”: 事件匯流排
  • “illuminate/events”: 事件
  • “illuminate/config”: 配置服務
  • “illuminate/view”: 檢視
  • “illuminate/session”: session
  • 路由沒有用laravel,而是”nikic/fast-route”
  • 中介軟體沒有用laravel,而是”laminas/laminas-stratigility”,遵循 PSR-7 HTTP 規範
  • 處理請求用的”laminas/laminas-httphandlerrunner”,遵循 PSR-15 HTTP 處理器規範

入口檔案分析

在public/index.php中

  1. $app = new Discuz\Foundation\Application(dirname(__DIR__)); //例項化一個容器,並繫結了事件服務和別名繫結
  2. $app->make(Discuz\Http\Server::class)->listen(); //例項化Server,並處理請求(重心在這裡)

Discuz\Http\Server::class 分析

public function listen()
{
    // 這裡繫結了核心的服務類,包括ApiServiceProvider、WebServiceProvider,這兩個Provider會載入routes/下的路由配置和註冊中介軟體
    $this->siteBoot();

    $pipe = new MiddlewarePipe();
    //new RequestHandler裡用到了ApiServiceProvider、WebServiceProvider註冊的中介軟體
    $pipe->pipe(new RequestHandler([
        '/api' => 'discuz.api.middleware',
        '/' => 'discuz.web.middleware'
    ], $this->app));

    $psr17Factory = new Psr17Factory();
    $request = (new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory))->fromGlobals();

    $this->app->instance('request', $request);
    $this->app->alias('request', ServerRequestInterface::class);

    // 上面的例項化的中介軟體物件和Request物件都注入到RequestHandlerRunner
    $runner = new RequestHandlerRunner(
        $pipe,
        new SapiEmitter,
        function () use ($request) {
            return $request;
        },
        function (Throwable $e) {
            $generator = new ErrorResponseGenerator;
            return $generator($e, new ServerRequest, new Response);
        }
    );

    // run方法有兩個重要步驟:1. 處理請求$this->handler->handle($request);2. 收尾工作$this->emitter->emit($response);
    $runner->run();
}

$this->handler->handle($request) 在 Laminas\Stratigility\MiddlewarePipe

  • 主要是中介軟體的處理流程,利用SplQueue的先進先出佇列來實現,在ApiServiceProvider、WebServiceProvider中註冊了中介軟體,enqueue進SplQueue佇列,然後一個一個的dequeue出佇列並處理請求。
  • 最後一箇中介軟體是DispatchRoute::class,它則是用來根據請求的url找到對應的控制器開始業務處理。
  • PS:中介軟體的處理不再像laravel那樣用到array_reduce的特性,我寫了一個簡單的demo模擬了下:
class Next
{
    private $fallbackHandler;
    private $queue;

    public function __construct($queue, $fallbackHandler)
    {
        $this->queue = clone $queue;
        $this->fallbackHandler = $fallbackHandler;
    }

    public function handle($request)
    {
        if ($this->queue->isEmpty()) {
            $this->queue = null;
            $fallbackHandler = $this->fallbackHandler;
            return $fallbackHandler($request);
        }
        $middleware = $this->queue->dequeue();
        $next = clone $this;
        $this->queue = null;

        return $middleware($request, $next);
    }
}

class SplQueuePipeline
{
    private $pipeline;

    public function __construct()
    {
        $this->pipeline = new SplQueue();
    }

    public function __clone()
    {
        $this->pipeline = clone $this->pipeline;
    }

    public function handle($request)
    {
        return $this->process($request, function ($r) {
            return $r;
        });
    }

    public function process($request, $handler)
    {
        return (new Next($this->pipeline, $handler))->handle($request);
    }

    public function pipe($middleware)
    {
        $this->pipeline->enqueue($middleware);
    }
}

$pipe = new SplQueuePipeline();
$pipe->pipe(function ($request, $next) {
    var_dump("pipe 1");
    return $next->handle($request);
});
$pipe->pipe(function ($request, $next) {
    var_dump("pipe 2");
    return $next->handle($request);
});
$rs = $pipe->handle("request");
var_dump($rs);

疑問:app\Api\Middleware目錄下的中介軟體是如何生效的

  1. App\Listeners\AddApiMiddleware 將 app\Api\Middleware 目錄的中介軟體進行註冊。
  2. App\Providers\EventServiceProvider 註冊了 Discuz\Api\Events\ConfigMiddleware 事件的監聽,listener 就是 App\Listeners\AddApiMiddleware。
  3. App\Providers\EventServiceProvider 定義在配置檔案的 providers 陣列內。而這些provider 的註冊在上面提到的$this->siteBoot();中完成。
  4. 那 ConfigMiddleware 事件又是怎麼產生的呢?在上面提到的 ApiServiceProvider 中,註冊完系統中介軟體後會觸發 ConfigMiddleware 事件。這個流程我模擬了一個簡單的demo如下:
class EventDispatcher
{
    private $bindings = [];

    public function listen($eventClass, $listenerClass)
    {
        if (empty($this->bindings[$eventClass])) {
            $this->bindings[$eventClass] = [];
        }
        $this->bindings[$eventClass][] = $listenerClass;
    }

    public function dispatch($obj)
    {
        $eventClass = get_class($obj);
        if (!empty($this->bindings[$eventClass])) {
            foreach ($this->bindings[$eventClass] as $listenerClass) {
                $reflector = new ReflectionClass($listenerClass);
                $instance = $reflector->newInstance();
                $method = $reflector->getMethod('handle');
                $method->invoke($instance, $obj);
            }
        }
    }
}

class AEvent
{
    public $pipe;

    public function __construct($obj)
    {
        $this->pipe = $obj;
    }
}

class AListener
{
    public function handle(AEvent $obj)
    {
        $obj->pipe->arr[] = "pipe 2";
    }
}

$obj = new stdClass();
$obj->arr = ['pipe 1'];
$eventDispatcher = new EventDispatcher();
$eventDispatcher->listen(AEvent::class, AListener::class);
$eventDispatcher->dispatch(new AEvent($obj));
print_r($obj->arr);
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章