爲何咱們要去構建一個本身的PHP框架?可能絕大多數的人都會說「市面上已經那麼多的框架了,還造什麼輪子?」。個人觀點「造輪子不是目的,造輪子的過程當中汲取到知識才是目的」。php
那怎樣才能構建一個本身的PHP框架呢?大體流程以下:html
入口文件 ----> 註冊自加載函數
----> 註冊錯誤(和異常)處理函數
----> 加載配置文件
----> 請求
----> 路由
---->(控制器 <----> 數據模型)
----> 響應
----> json
----> 視圖渲染數據
複製代碼
除此以外咱們還須要單元測試、nosql支持、接口文檔支持、一些輔助腳本等。最終個人框架目錄以下:前端
app [PHP應用目錄]
├── demo [模塊目錄]
│ ├── controllers [控制器目錄]
│ │ └── Index.php [默認控制器文件,輸出json數據]
│ ├── logics [邏輯層,主要寫業務邏輯的地方]
│ │ ├── exceptions [異常目錄]
│ │ ├── gateway [一個邏輯層實現的gateway演示]
│ │ ├── tools [工具類目錄]
│ │ └── UserDefinedCase.php [註冊框架加載到路由前的處理用例]
│ └── models [數據模型目錄]
│ └── TestTable.php [演示模型文件,定義一一對應的數據模型]
├── config [配置目錄]
│ ├── demo [模塊配置目錄]
│ │ ├── config.php [模塊自定義配置]
│ │ └── route.php [模塊自定義路由]
│ ├── common.php [公共配置]
│ ├── database.php [數據庫配置]
│ ├── swoole.php [swoole配置]
│ └── nosql.php [nosql配置]
docs [接口文檔目錄]
├── apib [Api Blueprint]
│ └── demo.apib [接口文檔示例文件]
├── swagger [swagger]
framework [Easy PHP核心框架目錄]
├── exceptions [異常目錄]
│ ├── CoreHttpException.php[核心http異常]
├── handles [框架運行時掛載處理機制類目錄]
│ ├── Handle.php [處理機制接口]
│ ├── EnvHandle.php [環境變量處理機制類]
│ ├── ErrorHandle.php [錯誤處理機制類]
│ ├── ExceptionHandle.php [未捕獲異常處理機制類]
│ ├── ConfigHandle.php [配置文件處理機制類]
│ ├── NosqlHandle.php [nosql處理機制類]
│ ├── LogHandle.php [log機制類]
│ ├── UserDefinedHandle.php[用戶自定義處理機制類]
│ ├── RouterSwooleHan... [swoole模式路由處理機制類]
│ └── RouterHandle.php [路由處理機制類]
├── orm [對象關係模型]
│ ├── Interpreter.php [sql解析器]
│ ├── DB.php [數據庫操做類]
│ ├── Model.php [數據模型基類]
│ └── db [數據庫類目錄]
│ └── Mysql.php [mysql實體類]
├── router [路由策略]
│ ├── RouterInterface.php [路由策略接口]
│ ├── General.php [普通路由]
│ ├── Pathinfo.php [pathinfo路由]
│ ├── Userdefined.php [自定義路由]
│ ├── Micromonomer.php [微單體路由]
│ ├── Job.php [腳本任務路由]
│ ├── EasySwooleRouter.php [swoole模式路由策略入口類]
│ └── EasyRouter.php [路由策略入口類]
├── nosql [nosql類目錄]
│ ├── Memcahed.php [Memcahed類文件]
│ ├── MongoDB.php [MongoDB類文件]
│ └── Redis.php [Redis類文件]
├── App.php [框架類]
├── Container.php [服務容器]
├── Helper.php [框架助手類]
├── Load.php [自加載類]
├── Request.php [請求類]
├── Response.php [響應類]
├── run.php [框架應用啓用腳本]
├── swoole.php [swoole模式框架應用啓用腳本]
frontend [前端源碼和資源目錄]
├── src [資源目錄]
│ ├── components [vue組件目錄]
│ ├── views [vue視圖目錄]
│ ├── images [圖片]
│ ├── ...
├── app.js [根js]
├── app.vue [根組件]
├── index.template.html [前端入口文件模板]
├── store.js [vuex store文件]
jobs [腳本目錄,寫業務腳本的地方]
├── demo [模塊目錄]
│ ├── Demo.php [腳本演示文件]
│ ├── ...
public [公共資源目錄,暴露到萬維網]
├── dist [前端build以後的資源目錄,build生成的目錄,不是發佈分支忽略該目錄]
│ └── ...
├── index.html [前端入口文件,build生成的文件,不是發佈分支忽略該文件]
├── index.php [後端入口文件]
├── server.php [swoole模式後端入口文件]
runtime [臨時目錄]
├── logs [日誌目錄]
├── build [php打包生成phar文件目錄]
tests [單元測試目錄]
├── demo [模塊名稱]
│ └── DemoTest.php [測試演示]
├── TestCase.php [測試用例]
vendor [composer目錄]
.git-hooks [git鉤子目錄]
├── pre-commit [git pre-commit預commit鉤子示例文件]
├── commit-msg [git commit-msg示例文件]
.babelrc [babel配置文件]
.env.example [環境變量示例文件]
.gitignore [git忽略文件配置]
.travis.yml [持續集成工具travis-ci配置文件]
build [php打包腳本]
cli [框架cli模式運行腳本]
LICENSE [lincese文件]
logo.png [框架logo圖片]
composer.json [composer配置文件]
composer.lock [composer lock文件]
package.json [前端依賴配置文件]
phpunit.xml [phpunit配置文件]
README-CN.md [中文版readme文件]
README.md [readme文件]
run [快速開始腳本]
webpack.config.js [webpack配置文件]
yarn.lock [yarn lock文件]
複製代碼
定義一個統一的入口文件,對外提供統一的訪問文件。對外隱藏了內部的複雜性,相似企業服務總線的思想。vue
// 載入框架運行文件
require('../framework/run.php');
複製代碼
[file: public/index.php]mysql
使用spl_autoload_register函數註冊自加載函數到__autoload隊列中,配合使用命名空間,當使用一個類的時候能夠自動載入(require)類文件。註冊完成自加載邏輯後,咱們就可使用use和配合命名空間申明對某個類文件的依賴。webpack
[file: framework/Load.php]laravel
腳本運行期間:git
經過函數set_error_handler註冊用戶自定義錯誤處理方法,可是set_error_handler不能處理如下級別錯誤,E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING,和在 調用 set_error_handler() 函數所在文件中產生的大多數 E_STRICT。因此咱們須要使用register_shutdown_function配合error_get_last獲取腳本終止執行的最後錯誤,目的是對於不一樣錯誤級別和致命錯誤進行自定義處理,例如返回友好的提示的錯誤信息。github
[file: framework/hanles/ErrorHandle.php]web
經過函數set_exception_handler註冊未捕獲異常處理方法,目的捕獲未捕獲的異常,例如返回友好的提示和異常信息。
[file: framework/hanles/ExceptionHandle.php]
加載框架自定義和用戶自定義的配置文件。
例如,數據庫主從配置.env文件參數示例:
[database]
dbtype = mysqldb
dbprefix = easy
dbname = easyphp
dbhost = localhost
username = easyphp
password = easyphp
slave = 0,1
[database-slave-0]
dbname = easyphp
dbhost = localhost
username = easyphp
password = easyphp
[database-slave-1]
dbname = easyphp
dbhost = localhost
username = easyphp
password = easyphp
複製代碼
[file: framework/hanles/ConfigHandle.php]
框架中全部的異常輸出和控制器輸出都是json格式,由於我認爲在先後端徹底分離的今天,這是很友善的,目前咱們不須要再去考慮別的東西。
$request = App::$container->get('request');
$request->check('username', 'require');
$request->check('password', 'length', 12);
$request->check('code', 'number');
複製代碼
[file: framework/Response.php]
├── router [路由策略]
│ ├── RouterInterface.php [路由策略接口]
│ ├── General.php [普通路由]
│ ├── Pathinfo.php [pathinfo路由]
│ ├── Userdefined.php [自定義路由]
│ ├── Micromonomer.php [微單體路由]
│ ├── Job.php [腳本任務路由]
│ └── EasyRouter.php [路由策略入口類]
複製代碼
經過用戶訪問的url信息,經過路由規則執行目標控制器類的的成員方法。我在這裏把路由大體分紅了四類:
傳統路由
domain/index.php?module=Demo&contoller=Index&action=test&username=test
複製代碼
pathinfo路由
domain/demo/index/modelExample
複製代碼
用戶自定義路由
// 定義在config/moduleName/route.php文件中,這個的this指向RouterHandle實例
$this->get('v1/user/info', function (Framework\App $app) {
return 'Hello Get Router';
});
複製代碼
微單體路由
我在這裏詳細說下這裏所謂的微單體路由,面向SOA和微服務架構大行其道的今天,有不少的團隊都在向服務化邁進,可是服務化過程當中不少問題的複雜度都是指數級的增加,例如分佈式的事務,服務部署,跨服務問題追蹤等等。這致使對於小的團隊從單體架構走向服務架構不免困難重重,因此有人提出來了微單體架構,按照個人理解就是在一個單體架構的SOA過程,咱們把微服務中的的各個服務仍是以模塊的方式放在同一個單體中,好比:
app
├── UserService [用戶服務模塊]
├── ContentService [內容服務模塊]
├── OrderService [訂單服務模塊]
├── CartService [購物車服務模塊]
├── PayService [支付服務模塊]
├── GoodsService [商品服務模塊]
└── CustomService [客服服務模塊]
複製代碼
如上,咱們簡單的在一個單體裏構建了各個服務模塊,可是這些模塊怎麼通訊呢?以下:
App::$app->get('demo/index/hello', [
'user' => 'TIGERB'
]);
複製代碼
經過上面的方式咱們就能夠鬆耦合的方式進行單體下各個模塊的通訊和依賴了。與此同時,業務的發展是難以預估的,將來當咱們向SOA的架構遷移時,很簡單,咱們只須要把以往的模塊獨立成各個項目,而後把App實例get方法的實現轉變爲RPC或者REST的策略便可,咱們能夠經過配置文件去調整對應的策略或者把本身的,第三方的實現註冊進去便可。
[file: framework/hanles/RouterHandle.php]
傳統的MVC模式包含model-view-controller層,絕大多時候咱們會把業務邏輯寫到controller層或model層,可是慢慢的咱們會發現代碼難以閱讀、維護、擴展,因此我在這裏強制增長了一個logics層。至於,邏輯層裏怎麼寫代碼怎麼,徹底由你本身定義,你能夠在裏面實現一個工具類,你也能夠在裏面再新建子文件夾並在裏面構建你的業務邏輯代碼,你甚至能夠實現一個基於責任連模式的網關(我會提供具體的示例)。這樣看來,咱們的最終結構是這樣的:
logics邏輯層
邏輯層實現網關示例:
咱們在logics層目錄下增長了一個gateway目錄,而後咱們就能夠靈活的在這個目錄下編寫邏輯了。gateway的結構以下:
gateway [Logics層目錄下gateway邏輯目錄]
├── Check.php [接口]
├── CheckAppkey.php [檢驗app key]
├── CheckArguments.php [校驗必傳參數]
├── CheckAuthority.php [校驗訪問權限]
├── CheckFrequent.php [校驗訪問頻率]
├── CheckRouter.php [網關路由]
├── CheckSign.php [校驗簽名]
└── Entrance.php [網關入口文件]
複製代碼
網關入口類主要負責網關的初始化,代碼以下:
// 初始化一個:必傳參數校驗的check
$checkArguments = new CheckArguments();
// 初始化一個:app key check
$checkAppkey = new CheckAppkey();
// 初始化一個:訪問頻次校驗的check
$checkFrequent = new CheckFrequent();
// 初始化一個:簽名校驗的check
$checkSign = new CheckSign();
// 初始化一個:訪問權限校驗的check
$checkAuthority = new CheckAuthority();
// 初始化一個:網關路由規則
$checkRouter = new CheckRouter();
// 構成對象鏈
$checkArguments->setNext($checkAppkey)
->setNext($checkFrequent)
->setNext($checkSign)
->setNext($checkAuthority)
->setNext($checkRouter);
// 啓動網關
$checkArguments->start(
APP::$container->get('request')
);
複製代碼
實現完成這個gateway以後,咱們如何在框架中去使用呢?在logic層目錄中我提供了一個user-defined的實體類,咱們把gateway的入口類註冊到UserDefinedCase這個類中,示例以下:
/**
* 註冊用戶自定義執行的類
*
* @var array
*/
private $map = [
// 演示 加載自定義網關
'App\Demo\Logics\Gateway\Entrance'
];
複製代碼
這樣這個gateway就能夠工做了。接着說說這個UserDefinedCase類,UserDefinedCase會在框架加載到路由機制以前被執行,這樣咱們就能夠靈活的實現一些自定義的處理了。這個gateway只是個演示,你徹底能夠天馬行空的組織你的邏輯~
視圖View去哪了?因爲選擇了徹底的先後端分離和SPA(單頁應用), 因此傳統的視圖層也所以去掉了,詳細的介紹看下面。
源碼目錄
徹底的先後端分離,數據雙向綁定,模塊化等等的大勢所趨。這裏我把我本身開源的vue前端項目結構easy-vue移植到了這個項目裏,做爲視圖層。咱們把前端的源碼文件都放在frontend目錄裏,詳細以下,你也能夠本身定義:
frontend [前端源碼和資源目錄,這裏存放咱們整個前端的源碼文件]
├── src [資源目錄]
│ ├── components [編寫咱們的前端組件]
│ ├── views [組裝咱們的視圖]
│ ├── images [圖片]
│ ├── ...
├── app.js [根js]
├── app.vue [根組件]
├── index.template.html [前端入口文件模板]
└── store.js [狀態管理,這裏只是個演示,你能夠很靈活的編寫文件和目錄]
複製代碼
build步驟
yarn install
DOMAIN=http://你的域名 npm run dev
複製代碼
編譯後
build成功以後會生成dist目錄和入口文件index.html在public目錄中。非發佈分支.gitignore文件會忽略這些文件,發佈分支去除忽略便可。
public [公共資源目錄,暴露到萬維網]
├── dist [前端build以後的資源目錄,build生成的目錄,不是發佈分支忽略該目錄]
│ └── ...
├── index.html [前端入口文件,build生成的文件,不是發佈分支忽略該文件]
複製代碼
數據庫對象關係映射ORM(Object Relation Map)是什麼?按照我目前的理解:顧名思義是創建對象和抽象事物的關聯關係,在數據庫建模中model實體類其實就是具體的表,對錶的操做其實就是對model實例的操做。可能絕大多數的人都要問「爲何要這樣作,直接sql語句操做很差嗎?搞得這麼麻煩!」,個人答案:直接sql語句固然能夠,一切都是靈活的,可是從一個項目的可複用,可維護, 可擴展出發,採用ORM思想處理數據操做是理所固然的,想一想若是若干一段時間你看見代碼裏大段的難以閱讀且無從複用的sql語句,你是什麼樣的心情。
市面上對於ORM的具體實現有thinkphp系列框架的Active Record,yii系列框架的Active Record,laravel系列框架的Eloquent(聽說是最優雅的),那咱們這裏言簡意賅就叫ORM了。接着爲ORM建模,首先是ORM客戶端實體DB:經過配置文件初始化不一樣的db策略,並封裝了操做數據庫的全部行爲,最終咱們經過DB實體就能夠直接操做數據庫了,這裏的db策略目前我只實現了mysql(負責創建鏈接和db的底層操做)。接着咱們把DB實體的sql解析功能獨立成一個可複用的sql解析器的trait,具體做用:把對象的鏈式操做解析成具體的sql語句。最後,創建咱們的模型基類model,model直接繼承DB便可。最後的結構以下:
├── orm [對象關係模型]
│ ├── Interpreter.php [sql解析器]
│ ├── DB.php [數據庫操做類]
│ ├── Model.php [數據模型基類]
│ └── db [數據庫類目錄]
│ └── Mysql.php [mysql實體類]
複製代碼
DB類使用示例
/**
* DB操做示例
*
* findAll
*
* @return void
*/
public function dbFindAllDemo()
{
$where = [
'id' => ['>=', 2],
];
$instance = DB::table('user');
$res = $instance->where($where)
->orderBy('id asc')
->limit(5)
->findAll(['id','create_at']);
$sql = $instance->sql;
return $res;
}
複製代碼
Model類使用示例
// controller 代碼
/**
* model example
*
* @return mixed
*/
public function modelExample()
{
try {
DB::beginTransaction();
$testTableModel = new TestTable();
// find one data
$testTableModel->modelFindOneDemo();
// find all data
$testTableModel->modelFindAllDemo();
// save data
$testTableModel->modelSaveDemo();
// delete data
$testTableModel->modelDeleteDemo();
// update data
$testTableModel->modelUpdateDemo([
'nickname' => 'easy-php'
]);
// count data
$testTableModel->modelCountDemo();
DB::commit();
return 'success';
} catch (Exception $e) {
DB::rollBack();
return 'fail';
}
}
//TestTable model
/**
* Model操做示例
*
* findAll
*
* @return void
*/
public function modelFindAllDemo()
{
$where = [
'id' => ['>=', 2],
];
$res = $this->where($where)
->orderBy('id asc')
->limit(5)
->findAll(['id','create_at']);
$sql = $this->sql;
return $res;
}
複製代碼
什麼是服務容器?
服務容器聽起來很浮,按個人理解簡單來講就是提供一個第三方的實體,咱們把業務邏輯須要使用的類或實例注入到這個第三方實體類中,當須要獲取類的實例時咱們直接經過這個第三方實體類獲取。
服務容器的意義?
用設計模式來說:其實無論設計模式仍是實際編程的經驗中,咱們都是強調「高內聚,鬆耦合」,咱們作到高內聚的結果就是每一個實體的做用都是極度專注,因此就產生了各個做用不一樣的實體類。在組織一個邏輯功能時,這些細化的實體之間就會不一樣程度的產生依賴關係,對於這些依賴咱們一般的作法以下:
class Demo
{
public function __construct()
{
// 類demo直接依賴RelyClassName
$instance = new RelyClassName();
}
}
複製代碼
這樣的寫法沒有什麼邏輯上的問題,可是不符合設計模式的「最少知道原則」,由於之間產生了直接依賴,整個代碼結構不夠靈活是緊耦合的。因此咱們就提供了一個第三方的實體,把直接依賴轉變爲依賴於第三方,咱們獲取依賴的實例直接經過第三方去完成以達到鬆耦合的目的,這裏這個第三方充當的角色就相似系統架構中的「中間件」,都是協調依賴關係和去耦合的角色。最後,這裏的第三方就是所謂的服務容器。
在實現了一個服務容器以後,我把Request,Config等實例都以單例的方式注入到了服務容器中,當咱們須要使用的時候從容器中獲取便可,十分方便。使用以下:
// 注入單例
App::$container->setSingle('別名,方便獲取', '對象/閉包/類名');
// 例,注入Request實例
App::$container->setSingle('request', function () {
// 匿名函數懶加載
return new Request();
});
// 獲取Request對象
App::$container->get('request');
複製代碼
提供對nosql的支持,提供全局單例對象,藉助咱們的服務容器咱們在框架啓動的時候,經過配置文件的配置把須要的nosql實例注入到服務容器中。目前咱們支持redis/memcahed/mongodb。
如何使用?以下,
// 獲取redis對象
App::$container->getSingle('redis');
// 獲取memcahed對象
App::$container->getSingle('memcahed');
// 獲取mongodb對象
App::$container->getSingle('mongodb');
複製代碼
支持swoole擴展下運行
cd public && php server.php
複製代碼
咱們能夠在jobs目錄下直接編寫咱們的任務腳本,以下
jobs [腳本目錄,寫業務腳本的地方]
├── demo [模塊目錄]
│ ├── Demo.php [腳本演示文件]
│ ├── ...
複製代碼
任務腳本示例:
<?php
namespace Jobs\Demo;
/**
* Demo Jobs
*
* @author TIERGB <https://github.com/TIGERB>
*/
class Demo
{
/**
* job
*
* @example php cli --jobs=demo.demo.test
*/
public function test()
{
echo 'Hello Easy PHP Jobs';
}
}
複製代碼
最後直接運行下面的命令便可:
php cli --job=demo.demo.test
複製代碼
一般咱們寫完一個接口後,接口文檔是一個問題,咱們這裏使用Api Blueprint協議完成對接口文檔的書寫和mock(可用),同時咱們配合使用Swagger經過接口文檔實現對接口的實時訪問(目前未實現)。
Api Blueprint接口描述協議選取的工具是snowboard,具體使用說明以下:
接口文檔生成說明
cd docs/apib
./snowboard html -i demo.apib -o demo.html -s
open the website, http://localhost:8088/
複製代碼
接口mock使用說明
cd docs/apib
./snowboard mock -i demo.apib
open the website, http://localhost:8087/demo/index/hello
複製代碼
基於phpunit的單元測試,寫單元測試是個好的習慣。
如何使用?
tests目錄下編寫測試文件,具體參考tests/demo目錄下的DemoTest文件,而後運行:
vendor/bin/phpunit
複製代碼
測試斷言示例:
/**
* 演示測試
*/
public function testDemo()
{
$this->assertEquals(
'Hello Easy PHP',
// 執行demo模塊index控制器hello操做,斷言結果是否是等於'Hello Easy PHP' 
App::$app->get('demo/index/hello')
);
}
複製代碼
目的規範化咱們的項目代碼和commit記錄。
cli腳本
以命令行的方式運行框架,具體見使用說明。
build腳本
打包PHP項目腳本,打包整個項目到runtime/build目錄,例如:
runtime/build/App.20170505085503.phar
<?php
// 入口文件引入包文件便可
require('runtime/build/App.20170505085503.phar');
複製代碼
Command:
php cli --build
執行:
composer create-project tigerb/easy-php easy --prefer-dist && cd easy
網站服務模式:
快速開始一個demo:
php cli --run
複製代碼
demo以下:
客戶端腳本模式:
php cli --method=<module.controller.action> --<arguments>=<value> ...
例如, php cli --method=demo.index.get --username=easy-php
複製代碼
Swoole模式:
cd public && php server.php
複製代碼
獲取幫助:
使用命令 php cli 或者 php cli --help
ab -c 100 -n 10000 "http://easy-php.local/Demo/Index/hello"
Document Path: /
Document Length: 53 bytes
Concurrency Level: 100
Time taken for tests: 3.259 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 1970000 bytes
HTML transferred: 530000 bytes
Requests per second: 3068.87 [#/sec] (mean)
Time per request: 32.585 [ms] (mean)
Time per request: 0.326 [ms] (mean, across all concurrent requests)
Transfer rate: 590.40 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 4
Processing: 6 32 4.0 31 68
Waiting: 6 32 4.0 31 68
Total: 8 32 4.0 31 68
Percentage of the requests served within a certain time (ms)
50% 31
66% 32
75% 33
80% 34
90% 39
95% 41
98% 43
99% 46
100% 68 (longest request)
複製代碼
ab -c 100 -n 10000 "http://easy-php.local/Demo/Index/hello"
Concurrency Level: 100
Time taken for tests: 1.319 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 1870000 bytes
HTML transferred: 160000 bytes
Requests per second: 7580.84 [#/sec] (mean)
Time per request: 13.191 [ms] (mean)
Time per request: 0.132 [ms] (mean, across all concurrent requests)
Transfer rate: 1384.39 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 5 10.6 3 172
Processing: 1 9 13.4 7 177
Waiting: 0 7 11.7 6 173
Total: 3 13 16.9 11 179
Percentage of the requests served within a certain time (ms)
50% 11
66% 12
75% 13
80% 14
90% 15
95% 17
98% 28
99% 39
100% 179 (longest request)
複製代碼
不足的地方還有不少,若是你們發現了什麼問題,能夠給我提issue或者PR。
或者你覺着在這個框架實現的細節你想了解的,同樣能夠給我提issue,後面我會總結成相應的文章分享給你們。
如何貢獻?
cp ./.git-hooks/* ./git/hooks
複製代碼
而後正常發起PR便可, 全部的commit我都會進行代碼格式(psr)驗證和commit-msg驗證,若是發生錯誤,請按照提示糾正便可。