Laravel核心解讀 — Request

kevinyan發表於2019-02-16

Request

很多框架都會將來自客戶端的請求抽象成類方便應用程式使用,在Laravel中也不例外。IlluminateHttpRequest類在Laravel框架中就是對客戶端請求的抽象,它是構建在Symfony框架提供的Request元件基礎之上的。今天這篇文章就簡單來看看Laravel是怎麼建立請求Request物件的,而關於Request物件為應用提供的能力我並不會過多去說,在我講完建立過程後你也就知道去原始碼哪裡找Request物件提供的方法了,網上有些速查表列舉了一些Request提供的方法不過不夠全並且有的也沒有解釋,所以我還是推薦在開發中如果好奇Request是否已經實現了你想要的能力時去Request的原始碼裡看下有沒有提供對應的方法,方法註釋裡都清楚地標明瞭每個方法的執行結果。下面讓我們進入正題吧。

建立Request物件

我們可以在Laravel應用程式的index.php檔案中看到,在Laravel應用程式正式啟動完成前Request物件就已經被建立好了:

//public/index.php
$app = require_once __DIR__.`/../bootstrap/app.php`;

$kernel = $app->make(IlluminateContractsHttpKernel::class);

$response = $kernel->handle(
    //建立request物件
    $request = IlluminateHttpRequest::capture()
);

客戶端的HTTP請求是IlluminateHttpRequest類的物件

class Request extends SymfonyRequest implements Arrayable, ArrayAccess
{
    //新建Request例項
    public static function capture()
    {
        static::enableHttpMethodParameterOverride();

        return static::createFromBase(SymfonyRequest::createFromGlobals());
    }
}

通過IlluminateHttpRequest類的原始碼可以看到它是繼承自Symfony Request類的,所以IlluminateHttpRequest類中實現的很多功能都是以Symfony Reques提供的功能為基礎來實現的。上面的程式碼就可以看到capture方法新建Request物件時也是依賴於Symfony Request類的例項的。

namespace SymfonyComponentHttpFoundation;
class Request
{
    /**
     * 根據PHP提供的超級全域性陣列來建立Smyfony Request例項
     *
     * @return static
     */
    public static function createFromGlobals()
    {
        // With the php`s bug #66606, the php`s built-in web server
        // stores the Content-Type and Content-Length header values in
        // HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH fields.
        $server = $_SERVER;
        if (`cli-server` === PHP_SAPI) {
            if (array_key_exists(`HTTP_CONTENT_LENGTH`, $_SERVER)) {
                $server[`CONTENT_LENGTH`] = $_SERVER[`HTTP_CONTENT_LENGTH`];
            }
            if (array_key_exists(`HTTP_CONTENT_TYPE`, $_SERVER)) {
                $server[`CONTENT_TYPE`] = $_SERVER[`HTTP_CONTENT_TYPE`];
            }
        }

        $request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $server);

        if (0 === strpos($request->headers->get(`CONTENT_TYPE`), `application/x-www-form-urlencoded`)
            && in_array(strtoupper($request->server->get(`REQUEST_METHOD`, `GET`)), array(`PUT`, `DELETE`, `PATCH`))
        ) {
            parse_str($request->getContent(), $data);
            $request->request = new ParameterBag($data);
        }

        return $request;
    }
    
}

上面的程式碼有一處需要額外解釋一下,自PHP5.4開始PHP內建的builtin web server可以通過命令列直譯器來啟動,例如:

php -S localhost:8000 -t htdocs

-S <addr>:<port> Run with built-in web server.
-t <docroot>     Specify document root <docroot> for built-in web server.

但是內建web server有一個bug是將CONTENT_LENGTHCONTENT_TYPE這兩個請求首部儲存到了HTTP_CONTENT_LENGTHHTTP_CONTENT_TYPE中,為了統一內建伺服器和真正的server中的請求首部欄位所以在這裡做了特殊處理。

Symfony Request 例項的建立是通過PHP中的超級全域性陣列來建立的,這些超級全域性陣列有$_GET$_POST$_COOKIE$_FILES$_SERVER涵蓋了PHP中所有與HTTP請求相關的超級全域性陣列,建立Symfony Request例項時會根據這些全域性陣列建立Symfony Package裡提供的ParamterBag ServerBag FileBag HeaderBag例項,這些Bag都是Symfony提供地針對不同HTTP組成部分的訪問和設定API, 關於Symfony提供的ParamterBag這些例項有興趣的讀者自己去原始碼裡看看吧,這裡就不多說了。

class Request
{

    /**
     * @param array                $query      The GET parameters
     * @param array                $request    The POST parameters
     * @param array                $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
     * @param array                $cookies    The COOKIE parameters
     * @param array                $files      The FILES parameters
     * @param array                $server     The SERVER parameters
     * @param string|resource|null $content    The raw body data
     */
    public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null)
    {
        $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content);
    }
    
    public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null)
    {
        $this->request = new ParameterBag($request);
        $this->query = new ParameterBag($query);
        $this->attributes = new ParameterBag($attributes);
        $this->cookies = new ParameterBag($cookies);
        $this->files = new FileBag($files);
        $this->server = new ServerBag($server);
        $this->headers = new HeaderBag($this->server->getHeaders());

        $this->content = $content;
        $this->languages = null;
        $this->charsets = null;
        $this->encodings = null;
        $this->acceptableContentTypes = null;
        $this->pathInfo = null;
        $this->requestUri = null;
        $this->baseUrl = null;
        $this->basePath = null;
        $this->method = null;
        $this->format = null;
    }
    
}

可以看到Symfony Request類除了上邊說到的那幾個,還有很多屬性,這些屬性在一起構成了對HTTP請求完整的抽象,我們可以通過例項屬性方便地訪問MethodCharset等這些HTTP請求的屬性。

拿到Symfony Request例項後, Laravel會克隆這個例項並重設其中的一些屬性:

namespace IlluminateHttp;
class Request extends ....
{
    //在Symfony request instance的基礎上建立Request例項
    public static function createFromBase(SymfonyRequest $request)
    {
        if ($request instanceof static) {
            return $request;
        }

        $content = $request->content;

        $request = (new static)->duplicate(
            $request->query->all(), $request->request->all(), $request->attributes->all(),
            $request->cookies->all(), $request->files->all(), $request->server->all()
        );

        $request->content = $content;

        $request->request = $request->getInputSource();

        return $request;
    }
    
    public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null)
    {
        return parent::duplicate($query, $request, $attributes, $cookies, $this->filterFiles($files), $server);
    }
}
    //Symfony Request中的 duplicate方法
    public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null)
    {
        $dup = clone $this;
        if (null !== $query) {
            $dup->query = new ParameterBag($query);
        }
        if (null !== $request) {
            $dup->request = new ParameterBag($request);
        }
        if (null !== $attributes) {
            $dup->attributes = new ParameterBag($attributes);
        }
        if (null !== $cookies) {
            $dup->cookies = new ParameterBag($cookies);
        }
        if (null !== $files) {
            $dup->files = new FileBag($files);
        }
        if (null !== $server) {
            $dup->server = new ServerBag($server);
            $dup->headers = new HeaderBag($dup->server->getHeaders());
        }
        $dup->languages = null;
        $dup->charsets = null;
        $dup->encodings = null;
        $dup->acceptableContentTypes = null;
        $dup->pathInfo = null;
        $dup->requestUri = null;
        $dup->baseUrl = null;
        $dup->basePath = null;
        $dup->method = null;
        $dup->format = null;

        if (!$dup->get(`_format`) && $this->get(`_format`)) {
            $dup->attributes->set(`_format`, $this->get(`_format`));
        }

        if (!$dup->getRequestFormat(null)) {
            $dup->setRequestFormat($this->getRequestFormat(null));
        }

        return $dup;
    }

Request物件建立好後在Laravel應用中我們就能方便的應用它提供的能力了,在使用Request物件時如果你不知道它是否實現了你想要的功能,很簡單直接去IlluminateHttpRequest的原始碼檔案裡檢視就好了,所有方法都列在了這個原始碼檔案裡,比如:

/**
 * Get the full URL for the request.
 * 獲取請求的URL(包含host, 不包括query string)
 *
 * @return string
 */
public function fullUrl()
{
    $query = $this->getQueryString();

    $question = $this->getBaseUrl().$this->getPathInfo() == `/` ? `/?` : `?`;

    return $query ? $this->url().$question.$query : $this->url();
}

/**
 * Get the full URL for the request with the added query string parameters.
 * 獲取包括了query string 的完整URL
 *
 * @param  array  $query
 * @return string
 */
public function fullUrlWithQuery(array $query)
{
    $question = $this->getBaseUrl().$this->getPathInfo() == `/` ? `/?` : `?`;

    return count($this->query()) > 0
        ? $this->url().$question.http_build_query(array_merge($this->query(), $query))
        : $this->fullUrl().$question.http_build_query($query);
}

Request經過的驛站

建立完Request物件後, Laravel的Http Kernel會接著往下執行:載入服務提供器引導Laravel應用、啟動應用、讓Request經過基礎的中介軟體、通過Router匹配查詢Request對應的路由、執行匹配到的路由、Request經過路由上到中介軟體到達控制器方法。

總結

隨著Request最終到達對應的控制器方法後它的使命基本上也就完成了, 在控制器方法裡從Request中獲取輸入引數然後執行應用的某一業務邏輯獲得結果,結果會被轉化成Response響應物件返回給發起請求的客戶端。

這篇文章主要梳理了Laravel中Request物件,主要是想讓大家知道如何去查詢Laravel中Request現有提供了哪些能力供我們使用避免我們在業務程式碼裡重新造輪子去實現Request已經提供的方法。

本文已經收錄在系列文章Laravel原始碼學習裡,歡迎訪問閱讀。

相關文章