前言 大力推薦該教程: "《Create Your own PHP Framework》" Symfony的學習蠻累的,官方文檔雖然很豐富,但是組織方式像參考書而不是指南,一些不錯的指導性文檔常常是是看組件文檔時提到了才偶然發現的,這方面感覺就跟看Laravel和Webpack的官方文檔有差距。同時 ...
前言
大力推薦該教程:《Create Your own PHP Framework》
Symfony的學習蠻累的,官方文檔雖然很豐富,但是組織方式像參考書而不是指南,一些不錯的指導性文檔常常是是看組件文檔時提到了才偶然發現的,這方面感覺就跟看Laravel和Webpack的官方文檔有差距。同時Google中找Symfony的問題也不像Laravel容易找到答案,經常是自己看完整個官方文檔結合源碼才解決,進度趕的時候真是折磨人。總的來講,雖然非常非常強大,但是在掌握上,確實不像Laravel那麼方便學習。如果從Linux的設計哲學上來講,我認為Laravel是策略,Symfony是機制。策略的目標是在易用的前提下,提供足夠的靈活性;而機制相反,在保證靈活性的情況下,足夠易用,比較難學是自然的。策略需要依賴於機制之上,所以Laravel依賴Symfony。
之前在學Laravel時,看了《如何Composer一步一步構建自己的PHP框架》這個系列,對於Laravel的學習大有裨益。於是在學Symfony時,也是希望有個類似的教程,結果在Symfony官方文檔中偶然找到了《Create Your own PHP Framework》,學完後再看Symfony確實清晰了很多。
這裡簡單做下每一節的筆記,主要記了一些設計思想的點,比較零散,看完原文再來看估計會有所共鳴。
筆記
introduction
When creating a framework, following the MVC pattern is not the right goal. The main goal should be the Separation of Concerns.”
看到這句話時,我想起之前跟別人談如何一步步學習Laravel時說:“路由是框架的基石,而在這之上,通過構建MVC的每一層就完成了基本框架;然後再搭配一些現代必備特性比如命令行、測試;以及一些常用服務:隊列、安全認證等。就能理解Laravel。”
一下子就被打臉了,Symfony提出了構建框架的主要目標是關註點分離。從理念層面上是對的,MVC不是唯一的解決,不過太過抽象,MVC只是一種具體的關註點分離的方法,對於普通開發者會比較容易掌握。實際上,如果我只想著關註點分離,也還不知道如何下手。
為什麼要自己寫一個框架?
- 研究Symfony, 這是我的主要目的;
- 根據自己特殊需求做一個自己的框架;
- 純粹出於探索的樂趣;
- 重構舊代碼以便符合現代的最佳實踐;
- 證明你自己。。。
The HttpFoundation Componen
即便是最簡單的事情,使用框架也好於不使用。再簡單的代碼都面臨以下問題:
- 對參數的判斷
- 安全問題,比如XSS攻擊;
- 方便單元測試;
再簡單的問題如果要滿足上面的條件,寫出的代碼都比使用框架還累。
如果你認為安全性與可測試性不足以說服你停止寫舊代碼,趕緊採用新框架的話 ,那麼你可以停止讀本書並繼續你以前的工作方式了。(深深地感受到作者的高冷)
框架存在的目的是讓你更快地寫出更好的代碼,而不是讓你有所犧牲,如果有什麼犧牲的話,我想應該是學習成本的增加吧。
以後就算不使用框架,也應該使用HttpFoundation
組件的Request
和Response
處理請求與響應。
原文:The HttpFoundation Component
The Front Controller
用於分配路由的控制器稱為前端控制器(Front Controller
),它根據$request->getPathInfo()
調用不同目標代碼。這個框架到此最大的問題在於路由於於簡單,所以下一節應該是解決路由問題。
The Routing Component
上面簡單的路由並不太能滿足我們的要求,比如我們想實現路由的通配符匹配就比較麻煩。
因此,使用第三方的路由庫是必要的。symfony/routing
就很方便。這個路由很好,對象卻有點多,剛看時還真是不太好理解。
Routing
組件的基本對象:
RouteCollection
路由集合Route
單個路由RequestContext
請求上下文,通過fromRequest
方法與Request
綁定。(這種分離有利於測試)UrlMatcher
將RouteCollection
與RequestContext
綁定
然後通過
$attributes = $matcher->match($request->getPathInfo());
獲取當前的路由信息,下麵這些實例表明每個路由都會有_route
這個屬性,同時如果定義了通配屬性,也會變成對應的變數。
print_r($matcher->match('/bye'));
/* Gives:
array (
'_route' => 'bye',
);
*/
print_r($matcher->match('/hello/Fabien'));
/* Gives:
array (
'name' => 'Fabien',
'_route' => 'hello',
);
*/
print_r($matcher->match('/hello'));
/* Gives:
array (
'name' => 'World',
'_route' => 'hello',
);
*/
另外,當match不到時,會拋出如下異常:Routing\Exception\ResourceNotFoundException
,
使用Routing有個額外的好處,就是可以從根據路由生成路徑:
echo $generator->generate(
'hello',
array('name' => 'Fabien'),
UrlGeneratorInterface::ABSOLUTE_URL
);
// outputs something like http://example.com/somewhere/hello/Fabien
路由的問題解決了,但是到現在還沒控制器,這個後面應該要解決了。
Templating
直接渲染模板是有問題的,當業務邏輯稍微複雜一點就無法在模板中完成。因此需要將邏輯與渲染模板分開。
這一節為什麼不是直接談控制器
呢,我想跟第一節作者提到的關註點分離
的概念有關,目前為止,框架的問題在於邏輯在模板中做很困難,所以當前事情是要把模板與邏輯抽離出來,本節模板邏輯分離是目的,控制器
只是慣例做法。
按照Symfony
的慣例。通過給Route
的屬性
,增加_controller
這個鍵值,它指明路由對應的方法,框架將直接調用_controller
完成各種不同的工作。
這裡有個註意點,路由的屬性都被保存到$request->attributes
中,該屬性用保存跟HTTP
沒有直接相關的信息。
增加了_controller
屬性之後,再將路由信息
剝離到單獨一個文件src/app.php
,現在模板與業務邏輯區分開了。
The HtppKernel Component:The Controller Resolver]
上一節為止,所有的操作都是過程化的。我們希望將_controller
指向一個類的方法,比如LeapYearController
的indexAction
。改造起來也很簡單。將路由的_controller
改為[new LeapYearController(), ‘indexAction’]
即可。
然而這也帶來了另外一個缺點,不論路由有沒有用到,在它們添加的時候,控制器都被初始化,這對性能是個很大的影響。因此我們希望只有用到的路由才初始化。這個問題可以使用http-kernel
模塊解決。
http-kernel
提供了非常豐富的功能,不過我們現在只關心HttpKernel\Controller\ControllerResolver
和HttpKernel\Controller\ArgumentResolver
。
前者可以用來路由中確定出要調用的方法;後者用來確定要傳遞給方法的參數;參數解析器使用了反射機制,以便實現依賴註入
和將路由的attributes
的同名參數傳遞進去。調用路由方法與傳參,自己做還是要費一定功夫的,所以使用這兩個解析器都是必須的。
原文:The HtppKernel Component:The Controller Resolver
The Separation Of Concerns
我們的目標是構建一個框架,前面的代碼雖然可滿足要求,但是缺少封裝,沒有放到命名空間,這個在規模擴大時並不方便。同時每建一個新站都需要複製整個front.php
。對它們做封裝可提高可用性和可測試性。
本節引入了命名空間
,創建Simplex\Framework
的類和控制器
以及增加psr-4
的自動載入。
本節的分離關註的意義其實是從工程層面體現的:通過對前面實現的功能做一次代碼整理,揭示現代WEB PHP框架
的基本目錄組織方法。
Unit Testing
這一節,對於Framework
這個的類測試了404
, 500
和正常響應
,該類的測試覆蓋率為100%
。這一節對於後續學習單元測試是很有啟發性的:
- 如何配置單元測試文件
phpunit.xml.dist
- 如何創建
Mock Object
,以避免要依賴真實環境; - 如何儘可能的覆蓋測試,通過
404
,500
,正常響應
的示例說明; - 如何生成覆蓋率報告:
$phpunit --coverage-text # 命令行輸出
$phpunit --coverage-html=cov/ # 輸出HTML文檔
這一節的啟發在於:在寫代碼時,傳參應該儘量設計成介面才方便Mock
;而錯誤以throw
的方式拋出;這樣子會方便測試。另外如果你能從單元測試的角度去考慮框架,就會發現很多框架中覺得可能多餘的設計並不是多餘的。比如Laravel
的Facade
。
原文:Unit Testing
event dispatcher
整個框架雖然是完備的,但稱不上是一個好框架。所有的好框架都有很強的可擴展性。那麼什麼是可擴展性呢,作者給了一個蠻不錯的定義:
Being extensible means that the developer should be able to easily hook into the framework life cycle to modify the way the request is handled.
實際上,event dispatcher
這個名字不好理解,我是直接把它當成Laravel
的middleware
來看待。
The HttpKernel Component: HttpKernelInterface
HttpKernelInterface
是HttpKernel
組件最重要的一個方法。許多組件都依賴於該介面,比如HttpCache
。所以自己設計框架的時候,應該實現該介面,以便更好地利用現有組件。(這一節跟下麵一節總結起來呢就是一句話:自己實現的框架核心會有很多問題,還是使用HttpKernel
這個組件好)
原文:The HttpKernel Component: HttpKernelInterface
The HttpKernel Component: The HttpKernel Class
HttpKernel
是HttpKernelInterface
的預設實現。相比於自己實現,它提供了更完備的處理機制,比如我們自己的框架只處理了404
和500
的錯誤,但還有其他的錯誤沒處理;另外,它提供了event dispatcher
的各種預設機制,允許靈活地控制異常時、控制器進入前後、渲染視圖時的顯示;最後,在安全方面和規模增長後的表現也在各個實際的網站中表現得十分優異。
原文:The HttpKernel Component: The HttpKernel Class
The DependencyInjection Comonent
front.php
的代碼基本上在每個應用中都是重覆的,可以考慮將其移到Framework
的構造函數
中,但是你會發現:沒法添加新的listener
, 沒辦法模擬介面做單元測試等等。在實際場景中,我們需要區分開發環境與生產環境;或者想要添加越來越多的dispatcher
;改變response
的輸出字元集等,由於相關的類都只在front.php
中出現,所以這些改動都要在front.php
中增加代碼完成,最終顯然會導致front.php
越來越大。而當我們搞一個新的應用時又需要將front.php
拷貝過去,萬一要改時就顯得更不方便。有沒有一個好的方法,能夠保持依然當前框架的靈活性,但是又要可定製,可以單元測試,同時又沒有重覆代碼嗎?依賴註入(DI)就是解決這個問題的好方法。symfony/dependency-injection
就是一個棒的DI
組件,另外一個輕量級Pimple
也是廣受好評。
通過依賴註入,不同的服務都變成了可配置的。框架本身也通過容器初始化,初始化時的參數也都是容器,可根據需要傳遞不同的實現。而disptacher
也是個容器,配置的時候可以根據實際情況在初始化階段添加儘可能多的listener
。最終,front.php
的代碼就變成獲取framework
的容器即可,其他的事情則在container.php
配置。當程式變複雜時,將listener
單獨獨立出來,將配置單獨獨立出來,都是很簡單的事情。基本上可以說,依賴註入是現代框架的標配了。
原文:The DependencyInjection Comonent