在傳統的 PHP 網頁程式中,我們可能會用許多的檔案來代表不同的邏輯與功能。
舉例來說,用戶首先會進到 index.php
,此時點選「註冊」會將用戶導向 register.php
,或是點選「登入」將用戶導向 login.php
。
這種作法有助於「分離功能」,以功能為取向將不同功能的頁面依照不同的檔案進行分割,是快速入門 PHP 很重要的因素。
來點情境好了:
一個初學 PHP 不久的新人(老王)剛到公司報到,他偶然間被交辦了一項任務。
QA Team 發現用戶在註冊後可能會收不到認證信,並且將這個 Bug 提交進 Issue Tracker。
老王只需要開啟 register.php
然後找到認證信相關的邏輯,就能夠解掉這個 Bug。
然而隨著應用程式的擴大,許多的邏輯會被複用。
於是工程師開始引入模組(Module)的概念,把重複的邏輯--例如資料庫連接--寫成同一個檔案,有需要時再用 require
或 include
引入。
好景不長,隨著組織的擴編,會漸漸發現這些模組可能越來越不敷使用,例如 PM 希望在每個頁面上加入 Google Analytics 的 script,僅管已經有現成的 GA 模組,卻還要一個個地加進每個檔案中。
於是有一派工程師認為,是否可以將使用者的每個請求都集中到一個檔案(index.php
),再由這個檔案分發不同的回應?
如此一來,如果需要全域地加入某個功能,可以直接在 index.php
中加入,就可以在每個被分發的檔案中自由使用。
在 PHP 中,我們可能是用以下的方式實現:
// index.php
<?php
require __DIR__ . '/vendor/autoload.php';
$map = [
'/index' => __DIR__ . '/views/index.php';
'/login' => __DIR__ . '/views/login.php';
'/register' => __DIR__ . '/views/register.php';
];
if (array_key_exists($_GET['page'], $map)) {
include $map[$_GET['page']];
} else {
include $map['/index'];
}
如此一來,我們使用 /index.php?page=/login
時,便可以取得登入頁面的資料。
而且也只需要在 index.php
中引入 composer autoloader 一次,就可以在所有的地方使用。
雖然應用程式變得複雜了,然而這樣卻可以大大簡化在團隊合作時的難度:只要知道規則,就能夠合作。
// vulerbility.php
<?php
require __DIR__ . '/vendor/autoload.php';
include $_GET['page'];
絕對 不要相信來自使用者的資料。
這樣的寫法雖然也能夠達成 Front Controller Pattern,但是卻可能引發 LFI(Locale File Include)弱點。
LFI 弱點不僅會暴露 PHP 的原始碼,更有可能會造成 Remote Code Execution(遠端程式碼執行)
為了避免這種情況,在做 Front Controller Pattern 時請務必使用白名單機制。
實務上來說,我們並不會使用 $_GET
參數控制 Front Controller,主因為 $_GET
參數並不屬於 URL 路徑的一環,會造成以下情形:
parse_url()
正確解析為了解決這樣的情況,通常會這樣設計 Front Controller
// index.php
<?php
require __DIR__ . '/vendor/autoload.php';
$map = [
'/index' => __DIR__ . '/views/index.php';
'/login' => __DIR__ . '/views/login.php';
'/register' => __DIR__ . '/views/register.php';
];
$path = urldecode(
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
);
if (array_key_exists($path, $map)) {
include $map[$_GET['page']];
} else {
include __DIR__ . '/views/index.php'
}
如此一來,便可以使用 http://localhost/index.php/login
存取路徑,再搭配網頁伺服器的 Rewrite 規則將 index.php
字段去除,便可以得到很簡潔的 URL。