能成功接觸到套件核心之後,我們終於可以來實際寫網頁框架的程式了!
首先我們要做的事情,就是接收使用者的請求,並且進行分析
我們來看看原本 Laravel 裡面的 public/index.php
是怎麼做到這件事情的
use Illuminate\Http\Request;
//...
// Bootstrap Laravel and handle the request...
(require_once __DIR__.'/../bootstrap/app.php')
->handleRequest(Request::capture());
這邊使用了 Laravel 自定義的 Request
類別,我們看看這邊的 capture()
實作為何:
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
/**
* Create a new Illuminate HTTP request from server variables.
*
* @return static
*/
public static function capture()
{
static::enableHttpMethodParameterOverride();
return static::createFromBase(SymfonyRequest::createFromGlobals());
}
enableHttpMethodParameterOverride()
的實作如下,基本上以註解的內容為主,程式只是簡單的加上一個標記而已:
/**
* Enables support for the _method request parameter to determine the intended HTTP method.
*
* Be warned that enabling this feature might lead to CSRF issues in your code.
* Check that you are using CSRF tokens when required.
* If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered
* and used to send a "PUT" or "DELETE" request via the _method request parameter.
* If these methods are not protected against CSRF, this presents a possible vulnerability.
*
* The HTTP method can only be overridden when the real HTTP method is POST.
*/
public static function enableHttpMethodParameterOverride(): void
{
self::$httpMethodParameterOverride = true;
}
看到這麼大量的註解,可能有一些人會擔心這是專案內的壞味道。不過我個人不這麼認為:
雖然這裡的程式只有短短一行,註解遠比程式要多上很多,但是對於需要理解這段邏輯的人來說,這些註解我認為是非常有幫助的!
接著我們繼續往下看,這邊在定義參數的時候,使用了 SymfonyRequest::createFromGlobals()
這個函數。這個就不是 Laravel 所撰寫的類別了,而是使用 Symfony\Component\HttpFoundation\Request
!
我們來看看這個套件的說明,這個套件的 GitHub Repo 在 https://github.com/symfony/http-foundation ,說明文件則位於 https://symfony.com/doc/current/components/http_foundation.html
在 Symfony 的官方文件裡,我們可以看到 createFromGlobals()
所做的事情幾乎等同於
$request = new Request(
$_GET,
$_POST,
[],
$_COOKIE,
$_FILES,
$_SERVER
);
也就是說,在分析使用者的請求這件事情上,其實 Laravel 並不是自己從無到有的手刻出分析的邏輯出來,而是利用 Symfony 已經撰寫好的請求物件,加上他自己的邏輯,來設計出 Laravel 自己的請求物件!
我們來看看
class Request extends SymfonyRequest implements Arrayable, ArrayAccess
這邊可以看到,Laravel 直接讓客製化的請求類別繼承 Symfony 的類別,這樣就可以省掉很多邏輯了!
那麼,拿到 SymfonyRequest
之後要怎麼轉換成 Laravel 的 Request
物件呢?
我們來看看 createFromBase()
的實作
public static function createFromBase(SymfonyRequest $request)
{
$newRequest = new static(
$request->query->all(), $request->request->all(), $request->attributes->all(),
$request->cookies->all(), (new static)->filterFiles($request->files->all()) ?? [], $request->server->all()
);
$newRequest->headers->replace($request->headers->all());
$newRequest->content = $request->content;
if ($newRequest->isJson()) {
$newRequest->request = $newRequest->json();
}
return $newRequest;
}
這邊的 new static
會呼叫到 __construct()
,由於 Request
裡面沒有定義 __construct()
,所以是使用到 SymfonyRequest
的 __construct()
。
除了建立一個幾乎一樣的請求物件,並且重新覆蓋 headers
和 content
以外,有一段很有意思的邏輯:
if ($newRequest->isJson()) {
$newRequest->request = $newRequest->json();
}
首先,先判斷請求是不是 json,這定義在 InteractsWithContentTypes
這個 trait 裡面:
/**
* Determine if the request is sending JSON.
*
* @return bool
*/
public function isJson()
{
return Str::contains($this->header('CONTENT_TYPE') ?? '', ['/json', '+json']);
}
另外一提,這個 trait 只有在 Request
這個類別內被使用。可以看出這樣拆分的目的只有一個,就是讓 Request
這個類別不包含那麼多細節而已。
如果 CONTENT_TYPE
裡面包含 json
,那麼就將 request
內容做 json 格式的轉換:
public function json($key = null, $default = null)
{
if (! isset($this->json)) {
$this->json = new InputBag((array) json_decode($this->getContent() ?: '[]', true));
}
if (is_null($key)) {
return $this->json;
}
return data_get($this->json->all(), $key, $default);
}
這邊使用了 PHP 原生的 json_decode
進行轉換。
今天對 Laravel 怎麼實作請求處理的部分,我們先看到這邊。各位明天見!