Slim 框架源碼解讀

0x00 前言

Slim 是由《PHP The Right Way》做者開發的一款 PHP 微框架,代碼量不算多(比起其它重型框架來講),號稱能夠一下午就閱讀完(我以爲前提是熟悉 Slim 所用的組件)。不過比起其它框架來講真的還算容易閱讀的了,因此是比較適合我這種新手學習一款框架。由於文章篇幅限制因此採用抓大放小的方式因此會避過一些不重要的內容(我纔不會告訴你有些地方我還沒看很明白/(ㄒoㄒ)/)。php

0x01 生命週期

Slim 生命週期

0x02 從入口文件開始

Slim 項目的 README 裏咱們能夠看見官方所給出的入口文件 index.php 的 demo,真的是很微(⊙﹏⊙)。html

<?php

require 'vendor/autoload.php';

$app = new Slim\App();

$app->get('/hello/{name}', function ($request, $response, $args) {
    return $response->getBody()->write("Hello, " . $args['name']);
});

$app->run();
複製代碼

上面這段代碼做用以下git

  • 引入 composer 的自動加載腳本 vendor/autoload.php
  • 實例化 App
  • 定義一個閉包路由
  • App 實例執行 run 方法

很容易看出整段代碼最重要的即是 App 類,下面咱們來分析一下 App 類。github

0x03 構造一切的核心 App

首先咱們看看 App 的構造函數segmentfault

/** * Create new application * * @param ContainerInterface|array $container Either a ContainerInterface or an associative array of app settings * @throws InvalidArgumentException when no container is provided that implements ContainerInterface */
public function __construct($container = []) {
    if (is_array($container)) {
        $container = new Container($container);
    }
    if (!$container instanceof ContainerInterface) {
        throw new InvalidArgumentException('Expected a ContainerInterface');
    }
    $this->container = $container;
}
複製代碼

這裏咱們發現 App 依賴一個容器接口 ContainerInterface。若是沒有傳遞容器,構造函數將實例化 Container 類,做爲 App 的容器。由於 App 依賴的是 ContainerInterface 接口而不是具體實現,因此咱們可使用任意實現了 ContainerInterface 接口的容器做爲參數注入 App 可是由於咱們如今研究 Slim 框架因此仍是要分析 Container 類。數組

0x04 容器 Container

Slim 的容器是基於 pimple/pimple 這個容器實現的(想了解 Pimple 容器能夠看這篇文章 PHP容器--Pimple運行流程淺析),Container 類增長了配置用戶設置、註冊默認服務的功能並實現了 ContainerInterface 接口。部分代碼以下:閉包

private $defaultSettings = [
	// 篇幅限制省略不貼
];

public function __construct(array $values = []) {
    parent::__construct($values);
	
    $userSettings = isset($values['settings']) ? $values['settings'] : [];
    $this->registerDefaultServices($userSettings);
}

// 註冊默認服務
private function registerDefaultServices($userSettings) {
    $defaultSettings = $this->defaultSettings;

    /** * 向容器中註冊 settings 服務 * 該服務將返回 App 相關的設置 * * @return array|\ArrayAccess */
    $this['settings'] = function () use ($userSettings, $defaultSettings) {
        // array_merge 將 $defaultSettings 和 $userSettings 合併
        // $defaultSettings 與 $userSettings 中相同的鍵名會覆蓋爲 $userSettings 的值
        return new Collection(array_merge($defaultSettings, $userSettings));
    };

    $defaultProvider = new DefaultServicesProvider();
    $defaultProvider->register($this);
}
複製代碼

實例化該容器時的任務就是將 $values 數組包含的服務註冊到容器裏,若是 $values 存在 settings 則將其和$defaultSettings 合併後再註冊到容器中,最後經過 DefaultServicesProvider 將默認的服務都註冊到容器裏。app

0x05 註冊默認服務 DefaultServicesProvider

DefaultServicesProviderregister 方法向容器註冊了許多服務包括 environmentrequestresponserouter 等,因爲篇幅限制下面只展現 register 方法裏比較重要的片斷。composer

if (!isset($container['environment'])) {
    /** * This service MUST return a shared instance * of \Slim\Interfaces\Http\EnvironmentInterface. * * @return EnvironmentInterface */
    $container['environment'] = function () {
        return new Environment($_SERVER);
    };
}

if (!isset($container['request'])) {
    /** * PSR-7 Request object * * @param Container $container * * @return ServerRequestInterface */
    $container['request'] = function ($container) {
        return Request::createFromEnvironment($container->get('environment'));
    };
}

if (!isset($container['response'])) {
    /** * PSR-7 Response object * * @param Container $container * * @return ResponseInterface */
    $container['response'] = function ($container) {
        $headers = new Headers(['Content-Type' => 'text/html; charset=UTF-8']);
        $response = new Response(200, $headers);

        return $response->withProtocolVersion($container->get('settings')['httpVersion']);
    };
}

if (!isset($container['router'])) {
    /** * This service MUST return a SHARED instance * of \Slim\Interfaces\RouterInterface. * * @param Container $container * * @return RouterInterface */
    $container['router'] = function ($container) {
        $routerCacheFile = false;
        if (isset($container->get('settings')['routerCacheFile'])) {
            $routerCacheFile = $container->get('settings')['routerCacheFile'];
        }

        $router = (new Router)->setCacheFile($routerCacheFile);
        if (method_exists($router, 'setContainer')) {
            $router->setContainer($container);
        }

        return $router;
    };
}
複製代碼

0x06 註冊路由

在入口文件中咱們能夠看見經過 $app->get(...) 註冊路由的方式,在 App 類裏咱們看見以下代碼:框架

/******************************************************************************** * Router proxy methods *******************************************************************************/

/** * Add GET route * * @param string $pattern The route URI pattern * @param callable|string $callable The route callback routine * * @return \Slim\Interfaces\RouteInterface */
public function get($pattern, $callable) {
    return $this->map(['GET'], $pattern, $callable);
}
/** * Add route with multiple methods * * @param string[] $methods Numeric array of HTTP method names * @param string $pattern The route URI pattern * @param callable|string $callable The route callback routine * * @return RouteInterface */
public function map(array $methods, $pattern, $callable) {
    // 如果閉包路由則經過 bindTo 方法綁定閉包的 $this 爲容器
    if ($callable instanceof Closure) {
        $callable = $callable->bindTo($this->container);
    }
    // 經過容器獲取 Router 並新增一條路由
    $route = $this->container->get('router')->map($methods, $pattern, $callable);
    // 將容器添加進路由
    if (is_callable([$route, 'setContainer'])) {
        $route->setContainer($this->container);
    }
    // 設置 outputBuffering 配置項
    if (is_callable([$route, 'setOutputBuffering'])) {
        $route->setOutputBuffering($this->container->get('settings')['outputBuffering']);
    }

    return $route;
}
複製代碼

App 類中的 getpostputpatchdeleteoptionsany 等方法都是對 Routermap 方法簡單封裝,讓我好奇的那路由組是怎麼實現的?下面咱們看看 Slim\Appgroup 方法,示例以下:

/** * Route Groups * * This method accepts a route pattern and a callback. All route * declarations in the callback will be prepended by the group(s) * that it is in. * * @param string $pattern * @param callable $callable * * @return RouteGroupInterface */
public function group($pattern, $callable) {
    // pushGroup 將構造一個 RouteGroup 實例並插入 Router 的 routeGroups 棧中,而後返回該 RouteGroup 實例,即 $group 爲 RouteGroup 實例
    $group = $this->container->get('router')->pushGroup($pattern, $callable);
    // 設置路由組的容器
    $group->setContainer($this->container);
    // 執行 RouteGroup 的 __invoke 方法
    $group($this);
    // Router 的 routeGroups 出棧
    $this->container->get('router')->popGroup();
    return $group;
}
複製代碼

上面代碼中最重要的是 $group($this); 這句執行了什麼?咱們跳轉到 RouteGroup 類中找到 __invoke 方法,代碼以下:

/** * Invoke the group to register any Routable objects within it. * * @param App $app The App instance to bind/pass to the group callable */
public function __invoke(App $app = null) {
    // 處理 callable,不詳細解釋請看 CallableResolverAwareTrait 源代碼
    $callable = $this->resolveCallable($this->callable);
    // 將 $app 綁定到閉包的 $this
    if ($callable instanceof Closure && $app !== null) {
        $callable = $callable->bindTo($app);
    }
	// 執行 $callable 並將 $app 傳參
    $callable($app);
}
複製代碼

注: 對 bindTo 方法不熟悉的同窗能夠看我以前寫的博文 PHP CLOURSE(閉包類) 淺析

上面的代碼可能會有點蒙但結合路由組的使用 demo 即可以清楚的知道用途。

$app->group('/users/{id:[0-9]+}', function () {
 $this->map(['GET', 'DELETE', 'PATCH', 'PUT'], '', function ($request, $response, $args) {
 // Find, delete, patch or replace user identified by $args['id']
 });
});
複製代碼

App 類的 group 方法被調用時 $group($this) 便會執行,在 __invoke 方法裏將 $app 實例綁定到了 $callable 中(若是 $callable 是閉包),而後就能夠經過 $this->map(...) 的方式註冊路由,由於閉包中的 $this 即是 $app。若是 $callable 不是閉包,還能夠經過參數的方式獲取 $app 實例,由於在 RouteGroup 類的 __invoke 方法中經過 $callable($app); 來執行 $callable

0x07 註冊中間件

Slim 的中間件包括「全局中間件」和「路由中間件」的註冊都在 MiddlewareAwareTrait 性狀裏,註冊中間件的方法爲 addMiddleware,代碼以下:

/** * Add middleware * * This method prepends new middleware to the application middleware stack. * * @param callable $callable Any callable that accepts three arguments: * 1. A Request object * 2. A Response object * 3. A "next" middleware callable * @return static * * @throws RuntimeException If middleware is added while the stack is dequeuing * @throws UnexpectedValueException If the middleware doesn't return a Psr\Http\Message\ResponseInterface */
protected function addMiddleware(callable $callable) {
    // 若是已經開始執行中間件則不容許再增長中間件
    if ($this->middlewareLock) {
        throw new RuntimeException('Middleware can’t be added once the stack is dequeuing');
    }
    // 中間件爲空則初始化
    if (is_null($this->tip)) {
        $this->seedMiddlewareStack();
    }
    // 中間件打包
    $next = $this->tip;
    $this->tip = function ( ServerRequestInterface $request, ResponseInterface $response ) use ( $callable, $next ) {
        $result = call_user_func($callable, $request, $response, $next);
        if ($result instanceof ResponseInterface === false) {
            throw new UnexpectedValueException(
                'Middleware must return instance of \Psr\Http\Message\ResponseInterface'
            );
        }

        return $result;
    };

    return $this;
}
複製代碼

這個函數的功能主要就是將原中間件閉包和現中間件閉包打包爲一個閉包,想了解更多能夠查看 PHP 框架中間件實現

0x08 開始與終結 Run

在經歷了建立容器、向容器註冊默認服務、註冊路由、註冊中間件等步驟後咱們終於到了 $app->run(); 這最後一步(ㄒoㄒ),下面讓咱們看看這 run 方法:

/******************************************************************************** * Runner *******************************************************************************/

/** * Run application * * This method traverses the application middleware stack and then sends the * resultant Response object to the HTTP client. * * @param bool|false $silent * @return ResponseInterface * * @throws Exception * @throws MethodNotAllowedException * @throws NotFoundException */
public function run($silent = false) {
    // 獲取 Response 實例
    $response = $this->container->get('response');

    try {
        // 開啓緩衝區
        ob_start();
        // 處理請求
        $response = $this->process($this->container->get('request'), $response);
    } catch (InvalidMethodException $e) {
        // 處理無效的方法
        $response = $this->processInvalidMethod($e->getRequest(), $response);
    } finally {
        // 捕獲 $response 之外的輸出至 $output
        $output = ob_get_clean();
    }
    // 決定將 $output 加入到 $response 中的方式
    // 有三種方式:不加入、尾部追加、頭部插入,具體根據 setting 決定,默認爲尾部追加
    if (!empty($output) && $response->getBody()->isWritable()) {
        $outputBuffering = $this->container->get('settings')['outputBuffering'];
        if ($outputBuffering === 'prepend') {
            // prepend output buffer content
            $body = new Http\Body(fopen('php://temp', 'r+'));
            $body->write($output . $response->getBody());
            $response = $response->withBody($body);
        } elseif ($outputBuffering === 'append') {
            // append output buffer content
            $response->getBody()->write($output);
        }
    }
    // 響應處理,主要是對空響應進行處理,對響應 Content-Length 進行設置等,不詳細解釋。
    $response = $this->finalize($response);
    // 發送響應至客戶端
    if (!$silent) {
        $this->respond($response);
    }
    // 返回 $response
    return $response;
}
複製代碼

注 1:對 try...catch...finally 不熟悉的同窗能夠看我以前寫的博文 PHP 異常處理三連 TRY CATCH FINALLY

注 2:對 ob_startob_get_clean 函數不熟悉的同窗也能夠看我以前寫的博文 PHP 輸出緩衝區應用

能夠看出上面最重要的就是 process 方法,該方法實現了處理「全局中間件棧」並返回最後的 Response 實例的功能,代碼以下:

/** * Process a request * * This method traverses the application middleware stack and then returns the * resultant Response object. * * @param ServerRequestInterface $request * @param ResponseInterface $response * @return ResponseInterface * * @throws Exception * @throws MethodNotAllowedException * @throws NotFoundException */
public function process(ServerRequestInterface $request, ResponseInterface $response) {
    // Ensure basePath is set
    $router = $this->container->get('router');
    // 路由器設置 basePath
    if (is_callable([$request->getUri(), 'getBasePath']) && is_callable([$router, 'setBasePath'])) {
        $router->setBasePath($request->getUri()->getBasePath());
    }

    // Dispatch the Router first if the setting for this is on
    if ($this->container->get('settings')['determineRouteBeforeAppMiddleware'] === true) {
        // Dispatch router (note: you won't be able to alter routes after this)
        $request = $this->dispatchRouterAndPrepareRoute($request, $router);
    }

    // Traverse middleware stack
    try {
        // 處理全局中間件棧
        $response = $this->callMiddlewareStack($request, $response);
    } catch (Exception $e) {
        $response = $this->handleException($e, $request, $response);
    } catch (Throwable $e) {
        $response = $this->handlePhpError($e, $request, $response);
    }

    return $response;
}
複製代碼

而後咱們看處理「全局中間件棧」的方法 ,在 MiddlewareAwareTrait 裏咱們能夠看見 callMiddlewareStack 方法代碼以下:

// 註釋討論的是在 Slim\APP 類的情景
/** * Call middleware stack * * @param ServerRequestInterface $request A request object * @param ResponseInterface $response A response object * * @return ResponseInterface */
public function callMiddlewareStack(ServerRequestInterface $request, ResponseInterface $response) {
    // tip 是所有中間件合併以後的閉包
    // 若是 tip 爲 null 說明不存在「全局中間件」
    if (is_null($this->tip)) {
        // seedMiddlewareStack 函數的做用是設置 tip 的值
        // 默認設置爲 $this
        $this->seedMiddlewareStack();
    }
    /** @var callable $start */
    $start = $this->tip;
    // 鎖住中間件確保在執行中間件代碼時不會再增長中間件致使混亂
    $this->middlewareLock = true;
    // 開始執行中間件
    $response = $start($request, $response);
    // 取消中間件鎖
    $this->middlewareLock = false;
    return $response;
}
複製代碼

看到上面可能會有疑惑,「路由的分配」和「路由中間件」的處理在哪裏?若是你發現 $app 其實也是「全局中間件」處理的一環就會恍然大悟了,在 Slim\App__invoke 方法裏,咱們能夠看見「路由的分配」和「路由中間件」的處理,代碼以下:

/** * Invoke application * * This method implements the middleware interface. It receives * Request and Response objects, and it returns a Response object * after compiling the routes registered in the Router and dispatching * the Request object to the appropriate Route callback routine. * * @param ServerRequestInterface $request The most recent Request object * @param ResponseInterface $response The most recent Response object * * @return ResponseInterface * @throws MethodNotAllowedException * @throws NotFoundException */
public function __invoke(ServerRequestInterface $request, ResponseInterface $response) {
    // 獲取路由信息
    $routeInfo = $request->getAttribute('routeInfo');

    /** @var \Slim\Interfaces\RouterInterface $router */
    $router = $this->container->get('router');

    // If router hasn't been dispatched or the URI changed then dispatch
    if (null === $routeInfo || ($routeInfo['request'] !== [$request->getMethod(), (string) $request->getUri()])) {
        // Router 分配路由並將路由信息注入至 $request
        $request = $this->dispatchRouterAndPrepareRoute($request, $router);
        $routeInfo = $request->getAttribute('routeInfo');
    }
    // 找到符合的路由
    if ($routeInfo[0] === Dispatcher::FOUND) {
        // 獲取路由實例
        $route = $router->lookupRoute($routeInfo[1]);
        // 執行路由中間件並返回 $response
        return $route->run($request, $response);
    // HTTP 請求方法不容許處理
    } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {
        if (!$this->container->has('notAllowedHandler')) {
            throw new MethodNotAllowedException($request, $response, $routeInfo[1]);
        }
        /** @var callable $notAllowedHandler */
        $notAllowedHandler = $this->container->get('notAllowedHandler');
        return $notAllowedHandler($request, $response, $routeInfo[1]);
    }
    // 找不到路由處理
    if (!$this->container->has('notFoundHandler')) {
        throw new NotFoundException($request, $response);
    }
    /** @var callable $notFoundHandler */
    $notFoundHandler = $this->container->get('notFoundHandler');
    return $notFoundHandler($request, $response);
}
複製代碼

上面的代碼拋開異常和錯誤處理,最主要的一句是 return $route->run($request, $response);Route 類的 run 方法,代碼以下:

/** * Run route * * This method traverses the middleware stack, including the route's callable * and captures the resultant HTTP response object. It then sends the response * back to the Application. * * @param ServerRequestInterface $request * @param ResponseInterface $response * * @return ResponseInterface */
public function run(ServerRequestInterface $request, ResponseInterface $response) {
    // finalize 主要功能是將路由組上的中間件加入到該路由中
    $this->finalize();

    // 調用中間件棧,返回最後處理的 $response
    return $this->callMiddlewareStack($request, $response);
}
複製代碼

其實 RouteApp 在處理中間件都使用了 MiddlewareAwareTrait 性狀,因此在處理中間件的邏輯是同樣的。那如今咱們就看最後一步,Route 類的 __invoke 方法。

/** * Dispatch route callable against current Request and Response objects * * This method invokes the route object's callable. If middleware is * registered for the route, each callable middleware is invoked in * the order specified. * * @param ServerRequestInterface $request The current Request object * @param ResponseInterface $response The current Response object * @return \Psr\Http\Message\ResponseInterface * @throws \Exception if the route callable throws an exception */
public function __invoke(ServerRequestInterface $request, ResponseInterface $response) {
    $this->callable = $this->resolveCallable($this->callable);

    /** @var InvocationStrategyInterface $handler */
    $handler = isset($this->container) ? $this->container->get('foundHandler') : new RequestResponse();

    $newResponse = $handler($this->callable, $request, $response, $this->arguments);

    if ($newResponse instanceof ResponseInterface) {
        // if route callback returns a ResponseInterface, then use it
        $response = $newResponse;
    } elseif (is_string($newResponse)) {
        // if route callback returns a string, then append it to the response
        if ($response->getBody()->isWritable()) {
            $response->getBody()->write($newResponse);
        }
    }

    return $response;
}
複製代碼

這段代碼的主要功能其實就是執行本路由的 callback函數,若 callback 返回 Response 實例便直接返回,不然將 callback 返回的字符串結果寫入到原 $response 中並返回。

0x09 總結

額……感受寫的很差,但總算將整個流程解釋了一遍。有些瑣碎的地方就不解釋了。其實框架的代碼還算好讀,有些地方解釋起來感受反而像多此一舉,因此乾脆貼了不少代碼/(ㄒoㄒ)/~~。說實話將整個框架的代碼通讀一遍對水平的確會有所提高O(∩_∩)O,有興趣的同窗仍是本身通讀一遍較好,因此說這只是一篇蜻蜓點水的水文/(ㄒoㄒ)/~。 歡迎指出文章錯誤和話題討論。

原文連接 - SLIM 框架源碼解讀

相關文章
相關標籤/搜索