從 0 開始創建一個屬於你自己的 PHP 框架


如何構建一個自己的 PHP 框架

為什么我們要去構建一個自己的 PHP 框架?可能絕大多數的人都會說“市面上已經那么多的框架了,還造什么輪子?”。我的觀點“造輪子不是目的,造輪子的過程中汲取到知識才是目的”。

那怎樣才能構建一個自己的 PHP 框架呢?大致流程如下:

    
入口文件 ----> 注冊自加載函數
        ----> 注冊錯誤(和異常)處理函數
        ----> 加載配置文件
        ----> 請求
        ----> 路由 
        ---->(控制器 <----> 數據模型)
        ----> 響應
        ----> 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 [數據庫配置] │ └── nosql.php [nosql 配置] docs [接口文檔目錄] ├── apib [Api Blueprint] │ └── demo.apib [接口文檔示例文件] ├── swagger [swagger] framework [Easy PHP 核心框架目錄] ├── exceptions [異常目錄] │ ├── CoreHttpException.php[核心 http 異常] ├── handles [框架運行時掛載處理機制類目錄] │ ├── Handle.php [處理機制接口] │ ├── ErrorHandle.php [錯誤處理機制類] │ ├── ExceptionHandle.php [未捕獲異常處理機制類] │ ├── ConfigHandle.php [配置文件處理機制類] │ ├── NosqlHandle.php [nosql 處理機制類] │ ├── LogHandle.php [log 機制類] │ ├── UserDefinedHandle.php[用戶自定義處理機制類] │ └── RouterHandle.php [路由處理機制類] ├── orm [對象關系模型] │ ├── Interpreter.php [sql 解析器] │ ├── DB.php [數據庫操作類] │ ├── Model.php [數據模型基類] │ └── db [數據庫類目錄] │ └── Mysql.php [mysql 實體類] ├── 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 [框架應用啟用腳本] frontend [前端源碼和資源目錄] ├── src [資源目錄] │ ├── components [vue 組件目錄] │ ├── views [vue 視圖目錄] │ ├── images [圖片] │ ├── ... ├── app.js [根 js] ├── app.vue [根組件] ├── index.template.html [前端入口文件模板] ├── store.js [vuex store 文件] public [公共資源目錄,暴露到萬維網] ├── dist [前端 build 之后的資源目錄,build 生成的目錄,不是發布分支忽略該目錄] │ └── ... ├── index.html [前端入口文件,build 生成的文件,不是發布分支忽略該文件] ├── index.php [后端入口文件] 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 [環境變量文件] .gitignore [git 忽略文件配置] 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 文件] webpack.config.js [webpack 配置文件] yarn.lock [yarn   lock 文件] 

框架模塊說明:

入口文件

定義一個統一的入口文件,對外提供統一的訪問文件。對外隱藏了內部的復雜性,類似企業服務總線的思想。

// 載入框架運行文件 require('../framework/run.php'); 

[file: public/index.php]

自加載模塊

使用 spl_autoload_register 函數注冊自加載函數到__autoload 隊列中,配合使用命名空間,當使用一個類的時候可以自動載入(require)類文件。注冊完成自加載邏輯后,我們就可以使用 use 和配合命名空間申明對某個類文件的依賴。

[file: framework/Load.php]

錯誤和異常模塊

腳本運行期間:

  • 錯誤:

通過函數 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 獲取腳本終止執行的最后錯誤,目的是對於不同錯誤級別和致命錯誤進行自定義處理,例如返回友好的提示的錯誤信息。

[file: framework/hanles/ErrorHandle.php]

  • 異常:

通過函數 set_exception_handler 注冊未捕獲異常處理方法,目的捕獲未捕獲的異常,例如返回友好的提示和異常信息。

[file: framework/hanles/ExceptionHandle.php]

配置文件模塊

加載框架自定義和用戶自定義的配置文件。

[file: framework/hanles/ConfigHandle.php]

輸入和輸出

  • 定義請求對象:包含所有的請求信息
  • 定義響應對象:申明響應相關信息

框架中所有的異常輸出和控制器輸出都是 json 格式,因為我認為在前后端完全分離的今天,這是很友善的,目前我們不需要再去考慮別的東西。

[file: framework/Request.php]

[file: framework/Response.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 模式提倡為 MCL 模式

傳統的 MVC 模式包含 model-view-controller 層,絕大多時候我們會把業務邏輯寫到 controller 層或 model 層,但是慢慢的我們會發現代碼難以閱讀、維護、擴展,所以我在這里強制增加了一個 logics 層。至於,邏輯層里怎么寫代碼怎么,完全由你自己定義,你可以在里面實現一個工具類,你也可以在里面再新建子文件夾並在里面構建你的業務邏輯代碼,你甚至可以實現一個基於責任連模式的網關(我會提供具體的示例)。這樣看來,我們的最終結構是這樣的:

  • M: models, 職責只涉及數據模型相關操作
  • C: controllers, 職責對外暴露資源,前后端分離架構下 controllers 其實就相當於 json 格式的視圖
  • L: 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->getSingle('request') ); 

實現完成這個 gateway 之后,我們如何在框架中去使用呢?在 logic 層目錄中我提供了一個 user-defined 的實體類,我們把 gateway 的入口類注冊到 UserDefinedCase 這個類中,示例如下:

/** * 注冊用戶自定義執行的類 * * @var array */ private $map = [ // 演示 加載自定義網關 'App\Demo\Logics\Gateway\Entrance' ]; 

這樣這個 gateway 就可以工作了。接着說說這個 UserDefinedCase 類,UserDefinedCase 會在框架加載到路由機制之前被執行,這樣我們就可以靈活的實現一些自定義的處理了。這個 gateway 只是個演示,你完全可以天馬行空的組織你的邏輯~

視圖 View 去哪了?由於選擇了完全的前后端分離和 SPA(單頁應用), 所以傳統的視圖層也因此去掉了,詳細的介紹看下面。

[file: app/*]

使用 Vue 作為視圖

源碼目錄

完全的前后端分離,數據雙向綁定,模塊化等等的大勢所趨。這里我把我自己開源的 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 生成的文件,不是發布分支忽略該文件] 

[file: frontend/*]

數據庫對象關系映射

數據庫對象關系映射 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; } 

[file: framework/orm/*]

服務容器模塊

什么是服務容器?

服務容器聽起來很浮,按我的理解簡單來說就是提供一個第三方的實體,我們把業務邏輯需要使用的類或實例注入到這個第三方實體類中,當需要獲取類的實例時我們直接通過這個第三方實體類獲取。

服務容器的意義?

用設計模式來講:其實不管設計模式還是實際編程的經驗中,我們都是強調“高內聚,松耦合”,我們做到高內聚的結果就是每個實體的作用都是極度專一,所以就產生了各個作用不同的實體類。在組織一個邏輯功能時,這些細化的實體之間就會不同程度的產生依賴關系,對於這些依賴我們通常的做法如下:

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->getSingle('request'); 

[file: framework/Container]

Nosql 模塊

提供對 nosql 的支持,提供全局單例對象,借助我們的服務容器我們在框架啟動的時候,通過配置文件的配置把需要的 nosql 實例注入到服務容器中。目前我們支持 redis/memcahed/mongodb。

如何使用?如下,

// 獲取 redis 對象 App::$container->getSingle('redis'); // 獲取 memcahed 對象 App::$container->getSingle('memcahed'); // 獲取 mongodb 對象 App::$container->getSingle('mongodb'); 

[file: framework/nosql/*]

接口文檔生成和接口模擬模塊

通常我們寫完一個接口后,接口文檔是一個問題,我們這里使用 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 

[file: docs/*]

單元測試模塊

基於 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') ); } 

phpunit 斷言文檔語法參考

[file: tests/*]

Git 鈎子配置

目的規范化我們的項目代碼和 commit 記錄。

  • 代碼規范:配合使用 php_codesniffer,在代碼提交前對代碼的編碼格式進行強制驗證。
  • commit-msg 規范:采用 ruanyifeng 的 commit msg 規范,對 commit msg 進行格式驗證,增強 git log 可讀性和便於后期查錯和統計 log 等, 這里使用了Treri的 commit-msg 腳本,Thx~。

[file: ./git-hooks/*]

輔助腳本

cli 腳本

以命令行的方式運行框架,具體見使用說明。

build 腳本

打包 PHP 項目腳本,打包整個項目到 runtime/build 目錄,例如:

runtime/build/App.20170505085503.phar

<?php // 入口文件引入包文件即可 require('runtime/build/App.20170505085503.phar'); 

[file: ./build]

如何使用?

執行:

  • composer install
  • chmod -R 777 runtime

網站服務模式:

步驟 1: yarn install 步驟 2: DOMAIN=http://localhost:666 npm run demo 步驟 3: cd public 步驟 4: php -S localhost:666 訪問網站: http://localhost:666/index.html 訪問接口: http://localhost:666/Demo/Index/hello demo 如下: 

https://raw.githubusercontent.com/TIGERB/easy-php/master/demo.gif

客戶端腳本模式:

php cli --method=<module.controller.action> --<arguments>=<value> ... 例如, php cli --method=demo.index.get --username=easy-php 

獲取幫助:

使用命令 php cli 或者 php cli --help

TODO

  • 懶加載優化框架加載流程
  • 變更 Helper 助手類的成員方法為框架函數,簡化使用提高生產效率
  • 提供更友善的開發 api 幫助
  • 模塊支持數據庫 nosql 自定義配置
  • 支持 mysql 主從配置
  • ORM 提供更多鏈式操作 api
  • 框架 log 行為進行級別分類
  • 想辦法解決上線部署是配置文件問題
  • 性能測試和優化
  • ...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM