在昨天的主題中,我們創造了一個 Hello World ,老實說一成不變的網頁還真的有些乏味呢,對吧?今天我們就讓網頁加點變化。
PHP 提供了一系列的超全域變數(Superblobals)用來處理資料,例如 $_GET 及 $_POST 這些變數是從 PHP 執行前就被定義的,且可以在任何地方使用。
我們將昨天的程式做一點點修改:
// rafax/public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';
echo 'Hello ' . $_GET['name'] . PHP_EOL;
啟用內置伺服器後,我們可以利用 http://localhost:10000/?name=Vincent
來讓網頁顯示 "Hello Vincent"。
curl "localhost:10000/?name=Vincent"
# Hello Vincent
可是今天有個糊塗混蛋,我們估且先稱他為「隔壁老王」,他忘了輸入自己的名字,這樣會發生什麼情況呢?
curl localhost:10000
# <br />
# <b>Notice</b>: Undefined index: name in <b>/Users/chivincent/PhpstormProjects/rafax/public/index.php</b> on line <b>5</b><br />
# Hello
Oh NO!我們的程式爆炸了 QAQQ。
PHP 對我們提出 Notice 層級的錯誤,表示我們嘗試存取未定義的索引值 name。
為了解決這個錯誤,我們需要對程式稍作修改
// rafax/public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';
echo 'Hello ' . $_GET['name'] ?? 'World' . PHP_EOL;
在這裡,我們用了 ??
(NULL 合併運算子),這是 PHP 7 的新特性,表示當 $_GET['name']
為 NULL 或未定義時,用其右邊的值取代之。
另一方面,我們最好在 composer.json
中提醒使用者,我們使用了 PHP 7 以上的特性:(註:此處省略了 composer.json
大部份未更動的部份)
{
"require": {
"php": ">=7.0.0"
}
}
絕對 不要相信使用者的任何資料。
在畫面上未經處理直接顯示使用者輸入的資料無疑是件愚蠢的事,這會讓你的應用程式存在 XSS (Cross-site Scripting,跨網站指令碼) 安全弱點。
幸好,我們可以利用 PHP 內建的 htmlspecialchars()
這個函式對字串進行預處理。
// rafax/public/index.php
<?php
require __DIR__ . '/../vendor/autoload.php';
$name = $_GET['name'] ?? 'World';
$name = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
header('Content-Type: text/html; charset=utf-8');
echo "Hello $name" . PHP_EOL;
老實說,如果在每次輸出資料時都必須套用 htmlspecialchars()
其實是相當麻煩的。
在未來的系列文中,我們將會用模板(template)自動化地避免 XSS 攻擊的可能性。
htmlspecialchars()
而非 htmlentities()
當輸入內容僅使用 ASCII 時,則 htmlspecialchars()
及 htmlentities()
的輸出會是完全相同的。
然而當處理 UTF-8 或是具有中文內容時 htmlentities()
,有可能會出現亂碼的情況,更糟的是,在某些很罕見的情況下(舊式瀏覽器在編碼不明確加上使用舊版 PHP 時),仍有可能在使用 htmlentities()
的情況下觸發 XSS。
聊完安全議題後,讓我們談談「可測試性」。在堅實的測試下,我們有憑據證實自己的程式是足夠強韌,同時也讓自己擁有重構(Refactoring)的基礎。
先讓我們針對既有的程式進行測試看看,在測試之前,你需要用到 phpunit/phpunit 這套簡易卻強大的單元測試框架。
composer require --dev phpunit/phpunit
同時你可能會習慣用 namespace 分隔不同的專案,我們需要對既有的 composer.json
進行一些微幅的修改
{
"require-dev": {
"phpunit/phpunit": "^6.5"
},
"autoload": {
"psr-4": {
"Rafax\\Tests\\": "tests/rafax/"
}
}
}
同時建立測試稿 rafax/tests/rafax/IndexTest.php
// rafax/tests/rafax/IndexTest.php
<?php
namespace Rafax\Tests;
use PHPUnit\Framework\TestCase;
class IndexTest extends TestCase
{
public function testVincent()
{
$_GET['name'] = 'Vincent';
ob_start();
include __DIR__ . '/../../public/index.php';
$content = ob_get_clean();
$this->assertEquals('Hello Vincent'.PHP_EOL, $content);
}
}
最後,利用 php vendor/bin/phpunit --stderr tests/rafax/IndexTest.php
指令進行單元測試
光是為了考慮 header('Content-Type: text/html; charset=utf-8')
,我們必須在測試時額外加上 --stderr
參數避免 PHP 錯誤訊息出現。
再者,為了取用資料輸入及輸出,我們利用 PHP 的原生函式 ob_start()
及 ob_get_clean()
開啟並取得輸出暫存(Output Buffer)才得以測試我們的小程式。
這還只是一個 10 行不到的小程式,卻動用了一大堆不可複用的方法進行測試,這對於程式設計沒有絲毫幫助,也無法愉快地寫程式。
為了解決這個問題,我們將在明天使用 Symfony HttpFoundation 來幫助我們進行框架的構建,並且以物件導向程式設計的方式進行 HTTP 協定下的資料交換。