跳到內容

前端控制器

編輯此頁

到目前為止,我們的應用程式非常簡單,因為只有一個頁面。為了讓事情變得更有趣一點,讓我們瘋狂一下,新增另一個頁面來說再見

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 對應提取到組態檔來增強您的框架。

本作品,包括程式碼範例,皆依據 Creative Commons BY-SA 3.0 授權條款授權。
目錄
    版本