平常在開發(fā)工作里胸完,重復早輪子的機會其實不很多。今天去SegmentFault論壇看到時候翘贮,翻到了以前的一個帖子赊窥,說的是如何寫自己的PHP框架。意見不一狸页。但是有幸看到了Symphony作者寫的一個系列博文:How to create your own PHP framework锨能,先動手翻譯看看(原文已經整理在Symphony官網(wǎng))。
介紹
Symphony是一個解決常見web開發(fā)問題的框架芍耘,它由一系列可復用的獨立址遇,解耦,并具有內在聯(lián)系的PHP組件構成斋竞。
與其選擇使用較為底層的組件倔约,你可以使用已經完備的全棧式web框架Symphony,或者窃页,你也可以自己造一個跺株。這個系列教程就是告訴你如何建造自己的框架复濒。
你為什么要建造自己的框架脖卖?
為什么把建造自己的框架放在第一位呢乒省?如果你看看周圍,每個人都會告訴你重復造輪子是個壞主意畦木,因為你可以選擇現(xiàn)成的袖扛,更好的框架。大多數(shù)時候十籍,他們確實是對的蛆封。但是一下幾點可以告訴你,為什么你要自己造輪子:
- 為了學習流行web框架中更底層的知識勾栗,尤其是與Symphony框架相關的惨篱;
- 為了滿足你特定的需求而定制框架(前提是你必須非常清楚你的需求);
- 僅僅為了好玩而學習围俘;
- 為了重構很久以前的框架砸讳,融入流行框架的設計思想;
- 為了向別人炫耀你可以的界牡!
這個教程會一步一步教你如何構造框架簿寂,每一步你都會得到一個投入使用的框架,你可以用它作為自己最初的起點宿亡。慢慢的常遂,它會從一個簡單框架變?yōu)榫哂卸喾N特性的框架,最終你將獲得一個全功能的完備web框架挽荠。
如果沒有足夠的時間讀完整個教程克胳,你看一看 Slix 可以快速上手,這是一個基于Symphony的微型框架圈匆。代碼非常簡潔毯欣,考量了許多Symphony本身的組件
許多流行web框架將他們描述為MVC框架,這篇教程不會告訴你MVC設計模式臭脓,因為Symphony組件可以滿足各種設計模式酗钞,而不僅僅是MCV,當然了来累,如果你看一看MVC語義砚作,這本書會告訴你如何構造MVC當中的Controller。至于Model還有View嘹锁,這要看你個人口味葫录,而且你可以使用第三方庫來滿足需求(Doctrine,Propel 或者 plain-old PDO 來完成Model领猾;PHP 或者 Twig 來完成View)米同。
當決定構造一個框架的時候骇扇,按照MVC的設計模式來未必是一個正確的目標。最為正確的目標應該是Separation of Concerns(需求的分離)面粮,這可能是唯一一個你需要關心的設計模式少孝。Symphony的基礎概念關注點在HTTP的定義上。所以說熬苍,你將要打造的框架應該更加準確的定義為HTTP框架或者說響應/請求框架稍走。
正式開始之前
僅僅閱讀如何構造框架是不夠的。你需要自己動手嘗試教程里的每一個例子柴底。當然婿脸,你需要一個PHP環(huán)境(5.3.9或者更新),一個web服務器(比如Apache柄驻,Nginx狐树,或者PHP自建的web服務器),了解PHP基本知識以及面向對象編程鸿脓。
準備好了么抑钟,開始吧!
Bootstrapping 啟動
在你開始構思你的框架之前答憔,你需要想一想一些conventions(慣例):你的代碼將存貯子在哪里味赃?怎么命名你的class(類),怎么引用外部依賴包虐拓,等等
我們將新建一個目錄心俗,來存放你的代碼:
$ mkdir framework
$ cd framework
Dependency Management 依賴管理
為了安裝Symfony組件,你將使用Composer蓉驹,一個依賴包管理工具城榛。如果你還沒有安裝。點擊這里下載态兴。
我們的項目
這里狠持,我們沒有從0開始構建(from the scratch),我們將不斷的重寫“應用”瞻润,每一次加入一些抽象的成分喘垂。我們先從寫一個最簡單的web應用開始:
// framework/index.php
$input = $_GET['name'];
printf('Hello %s', $input);
如果你使用PHP 5.4,你可以使用PHP自建的服務器來運行這個應用绍撞,地址是http://localhost:4321/index.php?name=Fabien正勒。否則,你需要用到Apache后者Nginx其他web服務器傻铣。
$ php -S 127.0.0.1:4321
下一章章贞,我們將介紹HttpFoundation組件。
HttpFoundation 組件
在開始之前非洲,我們回過頭來想想為什么你需要一個PHP框架而不是純PHP應用(plain-old)鸭限。為什么使用框架蜕径,甚至使用最簡單的代碼片段(code snippet)是一個好主意。還有為什么創(chuàng)造一個基于Symphony組建的框架要好于從零開始搭框架败京。
我們不談論僅僅需要幾個程序員兜喻,就可以利用框架創(chuàng)造大型應用的傳統(tǒng)好處⌒希互聯(lián)網(wǎng)上已經有很多豐富的資源虹统。
盡管我們前一章寫的小應用已經足夠簡單弓坞,它仍然有很多問題:
// framework/index.php
$input = $_GET['name'];
printf('Hello %s', $input);
第一點隧甚,如果name參數(shù)沒有在URL里面定義,你會得到一個PHP warning渡冻,我們這樣解決:
// framework/index.php
$input = isset($_GET['name']) ? $_GET['name'] : 'World';
printf('Hello %s', $input);
但是戚扳,這樣的應用依然是不安全的,因為即使是這樣一個簡單的PHP代碼片段在面對世界上范圍最廣的安全威脅XSS(Cross0Site Scripting) 跨站攻擊面前族吻,也是脆弱的帽借。這里有一個更安全的版本:
$input = isset($_GET['name']) ? $_GET['name'] : 'World’;
header('Content-Type: text/html; charset=utf-8');
printf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'));
你可能已經注意到了,使用 htmlsepcialchars
乏味而且容易出錯(tedious and error prone)超歌。這就是為什么要使用類似Twig模板引擎的原因了砍艾。它可以默認autoescatping,使用準確的escaping要比使用一個簡單的escaping過濾要更好
正如你所見的巍举,假如我們要考慮避免PHP warning/notices 還有讓代碼更安全的話脆荷,我們所寫的代碼已經不是最簡單的了。
更進一步說懊悯,代碼甚至已經不能被簡單的測試了蜓谋。就算沒有太多可以測試的地方,針對這種最簡單的代碼片段使用單元測試是一種不自然的炭分,感覺不漂亮到方式桃焕。這里我們寫了一個試探性的PHPUnit 單元測試:
// framework/test.php
class IndexTest extends \PHPUnit_Framework_TestCase
{
public function testHello()
{
$_GET['name'] = 'Fabien';
ob_start();
include 'index.php';
$content = ob_get_clean();
$this->assertEquals('Hello Fabien', $content);
}
如果我們的應用稍微復雜,我們可能會遇到更多的問題捧毛。如果你對此表示好奇观堂,可以閱讀Symphony versus Flat PHP的文檔。
如果到了這一步呀忧,你還對使用框架來構建項目不放心的話(安全和測試是使用框架最好的理由)师痕,那么你可以回去寫自己的代碼了。
當然荐虐,使用框架不僅僅是為了更好的測試和安全性七兜,更重要的是要記住使用框架可以讓開發(fā)更快速。
使用HttpFoundation組建來面向對象
寫web應用就是和HTTP協(xié)議打交道福扬。所以腕铸,框架的核心應該是圍繞HTTP的規(guī)范惜犀。
HTTP 規(guī)范描述了客戶端(比如瀏覽器)如何與服務端(web服務器)進行交互。 嚴格規(guī)范的消息(well defined message)狠裹,請求和響應虽界,構成了客戶端與服務器之間的對話:客戶端發(fā)送請求到服務器,服務器返回一個響應涛菠。
在PHP中莉御,請求通過全局變量($_GET, $_POST, $_FILE, $_COOKIE, $_SESSION)來獲得,響應通過方法(echo, header, setcookie) 來實現(xiàn)俗冻。
寫出優(yōu)美代碼的第一步就是使用面向對象的理念礁叔,即通過Symphony HttpFoundation組件來取代默認的PHP全局變量和方法。
在使用這個組件之前迄薄,我們需要添加組件的依賴:
$ composer require symfony/http-foundation
運行這個命令將自動下載Symphony HttpFoundation組件琅关,并且將他安裝在當前目錄下的vendor/目錄下。同時也產生了composer.json和composer.lock文件讥蔽,包含了如下內容:
{
"require": {
"symfony/http-foundation": "^2.7"
}
}
上面的代碼展示了composer.json的內容涣易。
Class Autoloading 類的自動加載
當安裝一個新的依賴時,Composer也會自動生成一個vendor/autoloadphp
文件冶伞,讓類能夠自動加載 autoloaded新症。沒有自動加載,你需要在使用這個類之前响禽,require這個類文件徒爹。 但是由于PSR-0,我們可以使用Composer來讓PHP完成繁碎的工作金抡。
現(xiàn)在瀑焦,我們利用 Request類 和 Response類 重寫應用:
// framework/index.php
require_once __DIR__.'/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$input = $request->get('name', 'World');
$response = new Response(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));
$response->send();
createFromGlobals()方法創(chuàng)建了一個基于當前PHP全局變量的Request對象。
send()方法發(fā)送一個Response()對象返回客戶端(在返回內容之后梗肝,返回HTTP header)榛瓮。
在調用send()之前,我們需要再調用prepare()方法($response->prepare($request))來保證我們的響應是符合HTTP規(guī)范的巫击。例如禀晓,如果我們使用HEAD方法,這將會移除響應的內容
這里使用組件的最主要區(qū)別就是你對HTTP 消息有足夠的掌控權坝锰,你可以根據(jù)需求創(chuàng)造任意的請求和響應粹懒。
我們沒有明確設置Content-Type頭部,因為默認情況下顷级,響應的頭部就是UTF-8格式
通過Request請求類凫乖,利用簡單精巧的API,你可以獲取任意請求的消息。
// the URI being requested (e.g. /about) minus any query parameters
$request->getPathInfo();
// retrieve GET and POST variables respectively
$request->query->get('foo');
$request->request->get('bar', 'default value if bar does not exist');
// retrieve SERVER variables
$request->server->get('HTTP_HOST');
// retrieves an instance of UploadedFile identified by foo
$request->files->get('foo');
// retrieve a COOKIE value
$request->cookies->get('PHPSESSID');
// retrieve an HTTP request header, with normalized, lowercase keys
$request->headers->get('host');
$request->headers->get('content_type');
$request->getMethod(); // GET, POST, PUT, DELETE, HEAD
$request->getLanguages(); // an array of languages the client accepts
你也可以模擬一個請求:
$request = Request::create('/index.php?name=Fabian');
通過 Response 類帽芽,你可以生成一個響應(Response):
$response = new Response();
$response->setContent('Hello world!');
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/html');
// configure the HTTP cache headers
$response->setMaxAge(10);
如果要debug一個響應删掀,把它轉化成一個string,它會返回Http協(xié)議形式的header和content.
最后导街,以上的這些Sympony當中的類披泪,他們的安全性是得到了第三方獨立公司的審查(audit)的。作為開源軟件搬瑰,Symphony的源碼接受了來自世界各地的開發(fā)者的貢獻和完善(對于潛在的安全性問題)款票。你最后一次對你創(chuàng)建的框架進行安全審查,是在什么時候泽论?
甚至簡單到獲取客戶端的ip地址都可以變得不安全:
if ($myIp == $_SERVER['REMOTE_ADDR']) {
// the client is a known one, so give it some more privilege
}
上面的代碼已經很好了艾少,除非你在生產服務器的上一層加了逆向代理(reverse proxy)。如果是這樣佩厚,你需要編輯代碼滿足同時在開發(fā)環(huán)境(沒有代理的環(huán)境)以及遠程的生產環(huán)境的正常使用姆钉。
使用Request::getClientIp() 從一開始就會讓你好很多(它涵蓋了上面的情況):
$request = Request::createFromGlobals();
if ($myIp == $request->getClientIp()) {
// the client is a known one, so give it some more privilege
}
同時他還有一個好處说订,它自身就很安全抄瓦。這里的意思就是說,$_SERVER[‘HTTP_X_FORWARDED_FOR’] 這個獲取得到的值是不能被信任打陶冷,因為在實際情況中钙姊,當沒有代理的時候它可以被用戶篡改。所以埂伦,如果你在生產環(huán)境中沒有使用代理煞额,它既容易被系統(tǒng)拒絕處理(因為_SERVER[‘HTTP_X_FORWARDED_FOR’] 被篡改)。如果使用 getClientIp()
就不會有這種情況沾谜,因為你需要使用之前明確使用 setTrustedProxies():
Request::setTrustedProxies(array('10.0.0.1'));
if ($myIp == $request->getClientIp(true)) {
// the client is a known one, so give it some more privilege
}
所以膊毁,getClientIp() 方法適用于各種情況。你可以在所有的項目當中使用它基跑,不管你的服務器配置如何婚温,代碼都可以安全正確的運行。
其實這就是使用模版的好處了媳否,如果你從頭開始寫模版栅螟,你必須要考慮類似的所有情況。那你為什么不利用已經寫好的服務呢篱竭?
如果你想了解更多關于 HttpFoundation Component
, 你可以查閱 HttpFoundation 的API力图,或者閱讀完備的文檔。
到這里掺逼,我們已經寫了我們第一個框架了吃媒,如果你不想再深入下去也可以。 單單使用 Symphony HttpFoundation 組件以及讓你可以寫出更好,更易于測試的代碼了赘那。它也幫你處理了很多開發(fā)過程中遇到過的歷史問題惑朦。
事實上,類似 Drupal 的項目已經適配 HttpFoundation 組件來為他們所用漓概, 這也同樣對你適用漾月。不要重復造輪子。
我忘記告訴你了胃珍,學會使用 Symphony HttpFoundation 組件還有一個好處梁肿,由于它在目前主流框架中的流行(Sympony, Drupal 8, phpBB 4, ezPublish 5, Laravel, Silex, 還有其他),這些框架內部操作性會更好觅彰。上手會更快吩蔑。
前端控制器 The Front Controller
到目前為止,我們的應用就是簡單的單頁面填抬,我們通過新建一個頁面烛芬,讓事情變得更有趣。
// 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();
正如你所看到的飒责,大多數(shù)代碼和第一頁是一樣的赘娄。我們這里提煉出通用的代碼,這樣可以在不同的頁面間使用宏蛉。代碼的共享聽起來似乎是一個構件框架的不錯的計劃遣臼。
PHP風格的重構有點像下面的文件:
// 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();
實踐效果如下
// framework/index.php
require_once DIR.'/init.php';
$input = $request->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));
$response->send();
GoodBye 頁面設置如下
// framework/bye.php
require_once __DIR__.'/init.php';
$response->setContent('Goodbye!');
$response->send();
我們確實需要把大部分重復性的代碼放在一個地方,但是這不是所謂的抽象拾并。我們需要每個頁面都放置一個send方法揍堰,讓頁面以模板的形式表現(xiàn)出來,可以很方便的測試代碼嗅义。
而且屏歹,新建一個新頁面意味著我們需要新的php腳本文件,文件名通過URL(http://127.0.0.1:4321/bye.php)暴露到客戶端之碗。實際上蝙眶,每一個php腳本文件都對應了一個特定的URL,這個過程通過web服務器直接完成继控。如果我們能把這個URL請求的派遣功能交給框架管理械馆,這對我們來說會非常靈活,即框架的路由功能武通。
把單個php腳本文件暴露給客戶端用戶霹崎,是一種叫做 front controller 設計模式。
這樣的腳本文件類似下面這種:
// 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 = array(
'/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的例程
// framework/hello.php
$input = $request->get('name', 'World');
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));
在 front.php 腳本中冶忱,$map 變量把URL和對應的php腳本文件聯(lián)系起來尾菇。
題外話,假如客戶端請求一個路徑,但是這個路徑沒有在 $map 變量中定義派诬,我們則需要返回一個自定義的404頁面劳淆;現(xiàn)在你自己已經可以控制網(wǎng)站了。
如果要訪問某個頁面默赂,你必須在 front.php 腳本中定義沛鸵。
http://127.0.0.1:4321/front.php/hello?name=Fabien
http://127.0.0.1:4321/front.php/bye
/path 和 /bye 是頁面的路徑。
大多數(shù)的 web 服務器比如 Apache 或者 Nginx 都具有重寫請求地址的功能缆八,把 front controller 去掉曲掰,用戶只要輸入 http://127.0.0.1:4321/hello?name=Fabien
就可以直接訪問。
使用 Request::getPathInfo() 能夠獲取去除 front controller 的路徑地址奈辰。
你甚至不需要通過啟動服務器來測試代碼栏妖,采用 $request = Request::create('/hello?name=Fabien'); 即可生成自定義的請求,參數(shù)即自定義的URL路徑奖恰。
現(xiàn)在所有的頁面都會先訪問統(tǒng)一的腳本文件(front.php)吊趾,然后通過把所有其他的代碼放到公共訪問得到目錄以外的地方,可以提高網(wǎng)站的安全性瑟啃。
example.com
├── composer.json
├── composer.lock
├── src
│ └── pages
│ ├── hello.php
│ └── bye.php
├── vendor
│ └── autoload.php
└── web
└── front.php
配置web服務器的根目錄到 web/论泛,這樣其他的文件將不會被客戶端直接訪問。
我們在瀏覽器測試(http://localhost:4321/?name=Fabien)翰守,運行 php 自建的服務器:
$ php -S 127.0.0.1:4321 -t web/ web/front.php
未完待續(xù)