为何咱们要去构建一个本身的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验证,若是发生错误,请按照提示纠正便可。