前端控制器
到目前為止,我們的應用程式非常簡單,因為只有一個頁面。為了讓事情變得更有趣一點,讓我們瘋狂一下,新增另一個頁面來說再見
1 2 3 4 5 6 7 8 9 10
// framework/bye.php
require_once __DIR__.'/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response('Goodbye!');
$response->send();
您可以親自看到,大部分程式碼與我們為第一個頁面編寫的程式碼完全相同。讓我們提取通用程式碼,以便在所有頁面之間共享。程式碼共享聽起來像是建立我們第一個「真正」框架的好計畫!
PHP 進行重構的方式可能是建立一個包含檔案
1 2 3 4 5 6 7 8
// framework/init.php
require_once __DIR__.'/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response();
讓我們看看它的實際運作
1 2 3 4 5 6 7
// framework/index.php
require_once __DIR__.'/init.php';
$name = $request->query->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));
$response->send();
以及「再見」頁面
1 2 3 4 5
// framework/bye.php
require_once __DIR__.'/init.php';
$response->setContent('Goodbye!');
$response->send();
我們確實已將大部分共享程式碼移至中央位置,但感覺不像是一個好的抽象,不是嗎?我們仍然有所有頁面的 send()
方法,我們的頁面看起來不像模板,而且我們仍然無法正確測試此程式碼。
此外,新增頁面意味著我們需要建立一個新的 PHP 腳本,其名稱透過 URL (http://127.0.0.1:4321/bye.php
) 暴露給最終使用者。PHP 腳本名稱與用戶端 URL 之間存在直接對應關係。這是因為請求的分派直接由網頁伺服器完成。將此分派移至我們的程式碼以獲得更好的彈性可能是個好主意。這可以透過將所有用戶端請求路由到單一 PHP 腳本來實現。
提示
向最終使用者公開單一 PHP 腳本是一種稱為「前端控制器」的設計模式。
這樣的腳本可能如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// framework/front.php
require_once __DIR__.'/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response();
$map = [
'/hello' => __DIR__.'/hello.php',
'/bye' => __DIR__.'/bye.php',
];
$path = $request->getPathInfo();
if (isset($map[$path])) {
require $map[$path];
} else {
$response->setStatusCode(404);
$response->setContent('Not Found');
}
$response->send();
以下是新的 hello.php
腳本範例
1 2 3
// framework/hello.php
$name = $request->query->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));
在 front.php
腳本中,$map
將 URL 路徑與其對應的 PHP 腳本路徑相關聯。
作為額外的好處,如果用戶端請求的路徑未在 URL 對應中定義,我們會傳回自訂的 404 頁面。您現在可以掌控您的網站了。
若要存取頁面,您現在必須使用 front.php
腳本
http://127.0.0.1:4321/front.php/hello?name=Fabien
http://127.0.0.1:4321/front.php/bye
/hello
和 /bye
是頁面路徑。
提示
大多數網頁伺服器(如 Apache 或 nginx)都能夠重寫傳入的 URL 並移除前端控制器腳本,以便您的使用者能夠輸入 http://127.0.0.1:4321/hello?name=Fabien
,這樣看起來更好。
訣竅是使用 Request::getPathInfo()
方法,該方法透過移除前端控制器腳本名稱(包括其子目錄,僅在需要時 - 請參閱上述提示)來傳回請求的路徑。
提示
您甚至不需要設定網頁伺服器來測試程式碼。相反地,將 $request = Request::createFromGlobals();
呼叫替換為類似 $request = Request::create('/hello?name=Fabien');
的內容,其中引數是您想要模擬的 URL 路徑。
現在,網頁伺服器始終為所有頁面存取相同的腳本 (front.php
),我們可以透過將所有其他 PHP 檔案移至網頁根目錄之外來進一步保護程式碼
1 2 3 4 5 6 7 8 9 10 11
example.com
├── composer.json
├── composer.lock
├── src
│ └── pages
│ ├── hello.php
│ └── bye.php
├── vendor
│ └── autoload.php
└── web
└── front.php
現在,將您的網頁伺服器根目錄設定為指向 web/
,所有其他檔案將不再可從用戶端存取。
若要在瀏覽器中測試您的變更 (https://127.0.0.1:4321/hello?name=Fabien
),請執行 Symfony 本地網頁伺服器
1
$ symfony server:start --port=4321 --passthru=front.php
注意
為了使這個新結構能夠運作,您將必須調整各種 PHP 檔案中的某些路徑;變更留給讀者作為練習。
每個頁面中重複的最後一件事是對 setContent()
的呼叫。我們可以透過回顯內容並直接從前端控制器腳本呼叫 setContent()
,將所有頁面轉換為「模板」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// example.com/web/front.php
// ...
$path = $request->getPathInfo();
if (isset($map[$path])) {
ob_start();
include $map[$path];
$response->setContent(ob_get_clean());
} else {
$response->setStatusCode(404);
$response->setContent('Not Found');
}
// ...
而 hello.php
腳本現在可以轉換為模板
1 2 3 4
<!-- example.com/src/pages/hello.php -->
<?php $name = $request->query->get('name', 'World') ?>
Hello <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>
我們有了框架的第一個版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response();
$map = [
'/hello' => __DIR__.'/../src/pages/hello.php',
'/bye' => __DIR__.'/../src/pages/bye.php',
];
$path = $request->getPathInfo();
if (isset($map[$path])) {
ob_start();
include $map[$path];
$response->setContent(ob_get_clean());
} else {
$response->setStatusCode(404);
$response->setContent('Not Found');
}
$response->send();
新增頁面是一個兩步驟的過程:在對應中新增一個條目,並在 src/pages/
中建立一個 PHP 模板。從模板中,透過 $request
變數取得請求資料,並透過 $response
變數調整回應標頭。
注意
如果您決定在這裡停止,您可以透過將 URL 對應提取到組態檔來增強您的框架。