建立一個檔案名為 bye.php
,會向我們輸出 Goodbye World
。
// rafax/public/bye.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response('Goodbye World');
$response->prepare($request)->send();
我們不難察覺,bye.php
與 index.php
就像味噌湯與排骨酥湯一樣,相似度高達九成。
於是我們著手重構這兩個檔案,將其優先整合於 index.php
後再分發到兩個檔案(hello.php
與 bye.php
)之中。
// rafax/public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$response = new Response();
// rafax/public/hello.php
<?php
require __DIR__ . '/index.php';
$name = $request->get('name', 'World');
$name = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
$response->setContent("Hello $name");
$response->prepare($request)->send();
// rafax/public/bye.php
<?php
require __DIR__ . '/index.php';
$response->setContent('Goodbye World');
$response->prepare($request)->send();
我們會發現,無論是在 hello.php
或是 bye.php
,都必須要重複調用 require __DIR__ . '/index.php'
及 $response->prepare($request)->send()
。
這實在不是個好現象,這表示我們的模組化並不完全成功,讓我們試試導入前端控制器模式。
// rafax/public/index.php
<?php
require __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])) {
include $map[$path];
} else {
$response->setStatusCode(404);
$response->setContent('Not Found');
}
$response->prepare($request)->send();
// rafax/public/hello.php
<?php
$name = $request->get('name', 'World');
$name = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
$response->setContent("Hello $name");
// rafax/public/bye.php
<?php
$response->setContent('Goodbye World');
現在看起來舒服多了。
不過還有些缺點,誠如各位所知,PHP 不僅僅是個腳本語言,同時也是個模板語言。
也就是說,我們的頁面很有可能是各種「HTML 頁面 + PHP 語法」的集合,而不僅只是純 PHP 腳本。
是否有辦法將 hello.php
及 bye.php
模板化呢?
<!-- rafax/public/hello.php -->
Hello <?= htmlspecialchars($request->get('name', 'World'), ENT_QUOTES, 'UTF-8') ?>
<!-- rafax/public/bye.php -->
Goodbye World
// rafax/public/index.php
<?php
require __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])) {
ob_start();
include $map[$path];
$response->setContent(ob_get_clean());
} else {
$response->setStatusCode(404);
$response->setContent('Not Found');
}
$response->prepare($request)->send();
對於模板,目前還有些地方令人不太滿意:在模板中調用 $request->get('name', 'World')
感覺太過冗餘,如果可以用 $name
以一概之的話應該會讓程式碼更加簡潔。
<!-- rafax/public/hello.php -->
Hello <?= hrmlspecialchars($name ?? 'World', ENT_QUOTES, 'UTF-8') ?>
為了達成這個目的,我們需要自動化地展開(extract)$_GET
參數,將 $_GET['name']
自動變為 $name
。
// rafax/public/index.php
<?php
// ...
if (isset($map[$path])) {
ob_start();
extract($request->query->all(), EXTR_SKIP);
include $map[$path];
$response->setContent(ob_get_clean());
}
// ...
extract()
中加入第二個參數 EXTR_SKIP
?這是因為預設的 extract()
行為會覆蓋掉已存在的變數,可能造成安全弱點。
舉例而言
<?php
extract($_GET);
var_dump($_SERVER['REMOTE_ADDR']);
此時我們若是使用 curl http://localhost:10000/?_SERVER[REMOTE_ADDR]=192.168.1.1
就可以將真正的 $_SERVER['REMOTE_ADDR']
竄改,甚至可以竄改 session 資訊。
還是必須強調:絕對 不能相信來自客戶端的資料。
現在我們的框架看起來友善許多,對吧?
用 Symfony HttpFoundation 建構而成的 Request
與 Response
、簡單又安全的 Router Mapping 、變數的自動展開以及原生的 PHP 模板支援。
現在只剩一個地方需要考慮:所有的檔案全部擠在 public/
下,似乎不是個好主意,不僅造成程式碼凌亂,更無助於未來擴展。
於是我們決定把 hello.php
與 bye.php
兩個模板搬到其它地方,稍微整理一下。
定義資料夾結構如下:
rafax
|--- src/ # Chivincent\Rafax namespace root
|--- Bach
|--- app/ # Chivincent\Bach namespace root
|--- templates/
|--- hello.php
|--- bye.php
|--- public/
|--- index.php
|--- vendor/
|--- composer.json
|--- composer.lock
修改 index.php
如下(省略大部份未更動程式碼)。
// rafax/public/index.php
<?php
// ...
$map = [
'/hello' => __DIR__.'/../Bach/templates/hello.php',
'/bye' => __DIR__.'/../Bach/templates/bye.php',
];
// ...
因為 index.php
目前兼作路由(Routing)功能,故未來啟動 PHP 內置伺服器的指令改為。
php -S localhost:10000 public/index.php