iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 4
0
自我挑戰組

重新理解 PHP:從頭打造 Web Framework系列 第 6

設計模式 - 前端控制器模式(Front Controller Pattern)

簡介

在傳統的 PHP 網頁程式中,我們可能會用許多的檔案來代表不同的邏輯與功能。
舉例來說,用戶首先會進到 index.php,此時點選「註冊」會將用戶導向 register.php,或是點選「登入」將用戶導向 login.php
這種作法有助於「分離功能」,以功能為取向將不同功能的頁面依照不同的檔案進行分割,是快速入門 PHP 很重要的因素。

來點情境好了:
一個初學 PHP 不久的新人(老王)剛到公司報到,他偶然間被交辦了一項任務。
QA Team 發現用戶在註冊後可能會收不到認證信,並且將這個 Bug 提交進 Issue Tracker。
老王只需要開啟 register.php 然後找到認證信相關的邏輯,就能夠解掉這個 Bug。

然而隨著應用程式的擴大,許多的邏輯會被複用。
於是工程師開始引入模組(Module)的概念,把重複的邏輯--例如資料庫連接--寫成同一個檔案,有需要時再用 requireinclude 引入。
好景不長,隨著組織的擴編,會漸漸發現這些模組可能越來越不敷使用,例如 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() 正確解析
  • 網頁伺服器 Rewrite 規則不容易撰寫

為了解決這樣的情況,通常會這樣設計 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。

參考資料

  1. Wiki: Front controller

上一篇
加入 Symfony HttpFoundation 元件
下一篇
整合前端控制器模式
系列文
重新理解 PHP:從頭打造 Web Framework9

尚未有邦友留言

立即登入留言