tigerb/easy-php

更快的轻量级全栈PHP框架

安装: 275

依赖: 0

建议者: 0

安全: 0

星标: 779

关注者: 43

分支: 133

开放问题: 25

类型:框架

0.8.6 2019-04-21 13:30 UTC

README

Build Status Code Coverage Version PHP Version Docker env License

更快的轻量级全栈PHP框架

中文版 

Docker环境

只需一条命令即可构建easy-php的所有环境

easy-env

如何自己构建PHP框架?

为什么我们需要自己构建PHP框架?或许大多数人会说:“已经提供了这么多PHP框架,我们为什么还要造轮子?”我的观点是:“造轮子不是我们的目的,在造轮子的过程中,我们会获得一些知识,这才是我们的真正目的。”

那么,如何自己构建PHP框架呢?一般流程如下

Entry file ----> Register autoload function
           ----> Register error(and exception) function
           ----> Load config file
           ----> Request
           ----> Router
           ----> (Controller <----> Model)
           ----> Response
           ----> Json
           ----> View

此外,单元测试、NoSQL支持、API文档和一些辅助脚本,例如,我的框架目录如下

项目目录结构

app                             [application backend directory]
├── demo                        [module directory]
│   ├── controllers             [controller directory]
│   │       └── Index.php       [default controller class file]
│   ├── logics                  [logic directory]
│   │   ├── exceptions          [exception directory]
│   │   ├── gateway            [a gateway example]
│   │   ├── tools               [tool class directory]
│   │   └── UserDefinedCase.php [register user defined handle before framework loading router]
│   └── models                  [model directory]
│       └── TestTable.php       [model class file]
├── config                      [config folder]
│    ├── demo                   [module config folder]
│    │   ├── config.php         [module-defined config]
│    │   └── route.php          [module-defined router]
│    ├── common.php             [common config]
│    ├── database.php           [database config]
│    ├── swoole.php             [swoole config]
│    └── nosql.php              [nosql config]
docs                            [api document directory]
├── apib                        [Api Blueprint]
│    └── demo.apib              [api doc example file]
├── swagger                     [swagger]
framework                       [easy-php framework directory]
├── exceptions                  [core exception class]
│      ├── CoreHttpException.php[http exception]
├── handles                     [handle class file be used by app run]
│      ├── Handle.php           [handle interface]
│      ├── ErrorHandle.php      [error handle class]
│      ├── ExceptionHandle.php  [exception handle class]
│      ├── ConfigHandle.php     [config handle class]
│      ├── NosqlHandle.php      [nosql handle class]
│      ├── LogHandle.php        [log handle class]
│      ├── UserDefinedHandle.php[user defined handle class]
│      ├── RouterSwooleHan...   [router handle class for swoole mode]
│      └── RouterHandle.php     [router handle class]
├── orm                         [datebase object relation map class directory]
│      ├── Interpreter.php      [sql Interpreter class]
│      ├── DB.php               [database operation class]
│      ├── Model.php            [data model]
│      └── db                   [db type directory]
│          └── Mysql.php        [mysql class file]
├── router                      [router strategy]
│      ├── RouterInterface.php  [router strategy interface]
│      ├── General.php          [general strategy class]
│      ├── Pathinfo.php         [pathinfo strategy class]
│      ├── Userdefined.php      [userdefined strategy class]
│      ├── Micromonomer.php     [micromonomer strategy class]
│      ├── Job.php              [job strategy class]
│      ├── EasySwooleRouter.php [router strategy entrance class for swoole mode]
│      └── EasyRouter.php       [router strategy entrance class]
├── nosql                       [nosql directory]
│    ├── Memcahed.php           [memcahed class file]
│    ├── MongoDB.php            [mongoDB class file]
│    └── Redis.php              [redis class file]
├── App.php                     [this application class file]
├── Container.php               [container class file]
├── Helper.php                  [helper class file]
├── Load.php                    [autoload class file]
├── Request.php                 [request object class file]
├── Response.php                [response object class file]
├── run.php                     [run this application script file]
├── swoole.php                  [init the framework && swoole server]
frontend                        [application frontend source code directory]
├── src                         [source folder]
│    ├── components             [vue components]
│    ├── views                  [vue views]
│    ├── images                 [images folder]
│    ├── ...
├── app.js                      [vue root js]
├── app.vue                     [vue root component]
├── index.template.html         [frontend entrance template file]
├── store.js                    [vuex store file]
├── .babelrc                    [babel config file]
├── webpack.config.js           [webpack config file]
├── yarn.lock                   [yarn lock file]
jobs                            [Jobs folder, where write you business script]
├── demo                        [Module folder]
│    ├── Demo.php               [Job script example file]
│    ├── ...
public                          [this is a resource directory to expose service resource]
├── dist                        [frontend source file after build]
│    └── ...
├── index.html                  [entrance html file]
├── index.php                   [entrance php script file]
├── server.php                  [init the server with swoole]
runtime                         [temporary file such as log]
├── logs                        [log directory]
├── build                       [phar directory build by build script]
tests                           [unit test directory]
├── demo                        [module name]
│      └── DemoTest.php         [test class file]
├── TestCase.php                [phpunit test case class file]
vendor                          [composer vendor directory]
.git-hooks                      [git hooks directory]
├── pre-commit                  [git pre-commit example file]
├── commit-msg                  [git commit-msg example file]
bin                             [the auto script folder]
├── build                       [build php code to phar file script]
├── cli                         [run this framework with the php cli mode]
├── run                         [quick start script]
.env.example                    [the environment variables example file]
.gitignore                      [git ignore config file]
.travis.yml                     [travis-ci config file]
LICENSE                         [lincese file]
logo.png                        [logo picture]
composer.json                   [composer file]
composer.lock                   [composer lock file]
package.json                    [dependence file for frontend]
phpunit.xml                     [phpunit config file]
README-CN.md                    [readme file chinese]
README.md                       [readme file]

生命周期

框架模块描述

入口文件

定义一个入口文件,为用户提供统一的访问文件,隐藏像企业服务总线这样的复杂逻辑。

// require the application run file
require('../framework/run.php');

[文件:public/index.php]

自动加载模块

通过使用spl_autoload_register在__autoload队列中注册一个自动加载函数,之后,我们可以使用命名空间和'使用'关键字来使用一个类。

[文件:framework/Load.php]

错误和异常处理模块

  • 捕获错误

通过使用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来处理这个set_error_handler无法处理的最终错误。当框架运行时,我们可以自己处理错误,例如,为客户端提供一个友好的错误信息。

[文件:framework/handles/ErrorHandle.php]

  • 捕获异常

通过使用set_exception_handler注册一个函数来处理未被捕获的异常,这可以为客户端提供一个友好的错误信息。

[文件:framework/handles/ExceptionHandle.php]

配置处理模块

加载框架定义的和用户定义的配置文件。

例如,主从数据库配置:

[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

[文件:framework/handles/ConfigHandle.php]

请求和响应模块

  • 请求对象:包含所有请求信息。
  • 响应对象:包含所有响应信息。

框架中所有输出都是JSON,无论是框架核心错误还是业务逻辑的输出,因为我认为这是友好的。

请求参数检查,目前支持require/length/number检查。用法如下
$request = App::$container->get('request');
$request->check('username', 'require');
$request->check('password', 'length', 12);
$request->check('code', 'number');

[文件:framework/Request.php]

[文件:framework/Response.php]

路由处理模块

├── router                      [datebase object relation map class directory]
      ├── RouterInterface.php   [router strategy interface]
      ├── General.php           [general strategy class]
      ├── Pathinfo.php          [pathinfo strategy class]
      ├── Userdefined.php       [userdefined strategy class]
      ├── Micromonomer.php      [micromonomer strategy class]
      ├── Job.php               [job strategy class]
      └── EasyRouter.php        [router strategy entrance class]

通过路由解析URL信息来执行目标控制器的函数。由以下四种类型组成

传统路由

domain/index.php?module=Demo&contoller=Index&action=test&username=test

pathinfo路由

domain/demo/index/modelExample

用户自定义路由

// config/moduleName/route.php, this 'this' point to RouterHandle instance
$this->get('v1/user/info', function (Framework\App $app) {
    return 'Hello Get Router';
});

微单体路由

什么是微单体路由器?许多团队正在向SOA服务结构或微服务结构转型,我认为这对小团队来说很困难。因此,微单体应运而生,这是什么?在我看来,这实际上是对单体应用的SOA流程。例如

app
├── UserService     [user service module]
├── ContentService  [content service module]
├── OrderService    [order service module]
├── CartService     [cart service module]
├── PayService      [pay service module]
├── GoodsService    [goods service module]
└── CustomService   [custom service module]

如上所述,我们实现了一个简单的微单体结构。但是这些模块如何相互通信呢?如下

App::$app->get('demo/index/hello', [
    'user' => 'TIGERB'
]);

因此,我们可以通过解耦来解决此问题。同时,我们可以轻松地将我们的应用程序转换为SOA结构,因为我们只需要更改App类中get实现方式的方法,包括RPC、REST等方式。

[文件:框架/handles/RouterHandle.php]

MVC到MCL

传统的MVC模式包括模型、视图、控制器层。通常,您总是在控制器或模型层编写业务逻辑。但是,经过长时间的使用,您会发现代码难以阅读、维护和扩展。因此,我在框架中强制添加了一个逻辑层,您可以自行实现业务逻辑。您不仅可以实现工具类,还可以在新的子文件夹中实现业务逻辑,更重要的是,您还可以基于责任模式实现网关(我提供了一个示例)。

最终结构如下

  • M:模型,定义数据库表的映射,其中定义了CRUD操作。
  • C:控制器,公开业务资源
  • L:逻辑,灵活实现业务逻辑

逻辑层

网关示例:

我在逻辑文件夹中构建了一个网关,结构如下:

gateway                     [gateway directory in logics]
  ├── Check.php             [interface]
  ├── CheckAppkey.php       [check app key]
  ├── CheckArguments.php    [check require arguments]
  ├── CheckAuthority.php    [check auth]
  ├── CheckFrequent.php     [check call frequent]
  ├── CheckRouter.php       [router]
  ├── CheckSign.php         [check sign]
  └── Entrance.php          [entrance file]

网关入口类代码如下

// init:gateway common arguments must be not empty check
$checkArguments   =  new CheckArguments();
// init:app key check
$checkAppkey      =  new CheckAppkey();
// init:call frequent check
$checkFrequent    =  new CheckFrequent();
// init:sign check
$checkSign        =  new CheckSign();
// init:auth check
$checkAuthority   =  new CheckAuthority();
// init:gateway's router
$checkRouter      =  new CheckRouter();

// build object chain
$checkArguments->setNext($checkAppkey)
               ->setNext($checkFrequent)
               ->setNext($checkSign)
               ->setNext($checkAuthority)
               ->setNext($checkRouter);

// start gateway
$checkArguments->start(
    APP::$container->get('request')
);

网关实现后,如何在框架中使用它?我提供了一个用户自定义的类,我们只需在UserDefinedCase类中注册它。例如

/**
 * register user-defined behavior
 *
 * @var array
 */
private $map = [
    // for example, loading user-defined gateway
    'App\Demo\Logics\Gateway\Entrance'
];

因此,网关正在运行。但是,在RouterHandle之前可以加载哪些UserDefinedCase?

视图层在哪里?我放弃了它,因为我选择了SPA作为前端,详情如下。

[文件:app/*]

使用Vue进行视图

源代码文件夹

分离前端和后端以及双向数据绑定,模块化非常流行。同时,我将我自己构建的项目easy-vue添加到框架中作为视图层。前端源代码文件夹如下

frontend                        [application frontend source code directory]
├── src                         [source folder]
│    ├── components             [vue components]
│    ├── views                  [vue views]
│    ├── images                 [images folder]
│    ├── ...
├── app.js                      [vue root js]
├── app.vue                     [vue root component]
├── index.template.html         [frontend entrance template file]
├── store.js                    [vuex store file]

构建步骤

yarn install

DOMAIN=http://yourdomain npm run dev

构建后

构建成功后,在public中创建了dist文件夹和index.html。当此分支不是发布分支时,此文件将被忽略。

public                          [this is a resource directory to expose service resource]
├── dist                        [frontend source file after build]
│    └── ...
├── index.html                  [entrance html file]

[文件:前端/*]

ORM

ORM(对象关系映射)是什么?在我看来,ORM是一种建立对象和抽象事物之间关系的思想。模型是数据库的表,模型的实例是对表的操作。“你为什么要这样做,直接使用SQL不是更好吗?”我的回答:你可以做你想做的事情,一切都很灵活,但从框架的可重用性、可维护性和可扩展性的角度来看,并不建议这样做。

在市场上,ORM的实现,如:thinkphp和yii中的Active Record、laravel中的Eloquent,然后我们在这里简单地称之为“ORM”。框架中的“ORM”结构如下

├── orm                         
│      ├── Interpreter.php      [sql Interpreter]
│      ├── DB.php               [database operate class]
│      ├── Model.php            [base model class]
│      └── db                   
│          └── Mysql.php        [mysql class]

DB示例

/**
 * DB operation example
 *
 * 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;
}

模型示例

// 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 example
 *
 * 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;
}

[文件:框架/orm/*]

服务容器

服务容器是什么?

服务容器很难理解,我认为它只是一个第三方类,可以注入类和实例。我们可以在容器中非常简单地获取实例。

服务容器的意义?

根据设计模式:我们需要使我们的代码“高度内聚,松散耦合”。高度内聚的结果是“单一原则”,单一原则的结果是类相互依赖。处理依赖的一般方式如下

class Demo
{
    public function __construct ()
    {
        // the demo directly dependent on RelyClassName
        $instance = new RelyClassName ();
    }
}

上述代码没有问题,但不符合“最少知道原则”的设计模式,因为它有直接依赖。我们在框架中引入了一个第三方类,可以创建类或获取实例。因此,第三方类是服务容器,它在系统架构中类似于“中间件”的角色。

实现服务容器后,我将请求、配置等实例注入到单例容器中的服务中,需要使用时可以从容器中获取,非常方便。使用如下

// Inject the single instance
App::$container->setSingle('alias', 'object/closure/class name');

// Such as,Inject Request instance
App::$container->setSingle('request', function () {
    // closure function lazy load
    return new Request();
});
// get Request instance
App::$container->get('request');

[文件:framework/Container]

支持 Nosql

在框架加载时将 nosql 的单例注入到服务容器中,您可以通过配置来决定使用哪种 nosql。目前我们支持 redis/memcached/mongodb。

一些示例

// get redis instance
App::$container->getSingle('redis');
// get memcahed instance
App::$container->getSingle('memcahed');
// get mongodb instance
App::$container->getSingle('mongodb');

[文件:framework/nosql/*]

日志

我将日志类做成一个类似于 composer 的第三方模块,项目链接 https://github.com/easy-framework/easy-log

如何使用?如下

// env config
[log]
path = /runtime/logs/
name = easy-php
size = 512
level= debug


// How to use in your logic
Log::debug('EASY PHP');
Log::notice('EASY PHP');
Log::warning('EASY PHP');
Log::error('EASY PHP');

[文件:framework/handles/LogHandle.php]

支持 Swoole

此框架支持使用 PHP 扩展 swoole 的 swoole 模式,只需

cd public && php server.php

[文件:framework/swoole.php]

支持任务

您可以直接在 jobs 文件夹中执行一些任务,如下

jobs                            [Jobs folder, where write you business script]
├── demo                        [Module folder]
│    ├── Demo.php               [Job script example file]
│    ├── ...

任务示例文件

<?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

[文件:jobs/*]

API 文档

通常,我们在编写 API 后,API 文档是一个问题,我们使用 Api Blueprint 协议编写 API 文档和模拟。同时,我们可以通过使用 Swagger(不可用)实时请求 API。

我选择了 Api Blueprint 的工具 snowboard,详细说明如下:

API 文档生成说明

cd docs/apib

./snowboard html -i demo.apib -o demo.html -s

open the website, https://:8088/

API 模拟说明

cd docs/apib

./snowboard mock -i demo.apib

open the website, https://:8087/demo/index/hello

[文件:docs/*]

PHPUnit

基于 PHPUnit,我认为编写单元测试是一个好习惯。

如何进行测试?

在 tests 文件夹中编写测试文件,参考 DemoTest.php 文件,然后运行

 vendor/bin/phpunit

断言示例

/**
 * test assertion example
 */
public function testDemo()
{
    $this->assertEquals(
        'Hello Easy PHP',
        // assert the result by run hello function in demo/Index controller
        App::$app->get('demo/index/hello')
    );
}

PHPUnit 断言手册

[文件:tests/*]

Git 钩子

  • 编码标准:在提交之前强制使用 php_codesniffer 验证编码
  • 提交消息标准:在提交之前强制使用 Treri 编写的脚本验证提交消息,这可以提高 git 日志的可读性和调试,使日志分析更有用等。

[文件:./git-hooks/*]

脚本

cli 脚本

以 cli 模式运行框架,详细信息见说明。

构建脚本

在 runtime/build 文件夹中构建应用程序,例如

runtime/build/App.20170505085503.phar

<?php
// require the phar file in index.php file
require('runtime/build/App.20170505085503.phar');

命令

php cli --build

[文件:./build]

如何使用?

运行:

composer create-project tigerb/easy-php easy --prefer-dist && cd easy

Web 服务器模式

快速入门

cd bin && php cli --run

示例如下:

命令行模式

php cli --method=<module.controller.action> --<arguments>=<value> ...

For example, php cli --method=demo.index.get --username=easy-php

Swoole 模式

cd public && php server.php

获取帮助

使用 php cli 或 php cli --help

Docker环境

该框架支持 Docker 环境配置,您只需一个命令即可快速构建环境。更多信息请点击 easy-env

使用 php-fpm 的性能

ab -c 100 -n 10000 "http://easy-php.local/Demo/Index/hello"

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)

使用 Swoole 的性能

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。

如何贡献?

cp ./.git-hooks/* ./git/hooks

然后,按常规发起一个 PR。

项目地址: https://github.com/TIGERB/easy-php

TODO

  • 添加数据库 sql 辅助器
  • 集成 swagger
  • 为用户提供更多友好的帮助
  • 模块的配置支持模块定义的 mysql 和 nosql 配置
  • ORM 提供更多 api
  • 解决发布我们的项目时的配置问题
  • 通过使用 phar 实现 自动部署
  • ...

DONE

  • v0.8.6(2019/04/21)

    • 修复核心错误数据结构
    • 修复 phpunit
  • v0.8.5(2019/01/06)

    • 修复 error_report
    • 修复当 __coreError 发生时,响应输出 200 但也输出了 __coreError
  • v0.8.1(2018/06/24)

    • 使用 easy log
    • 添加 bin 文件夹
  • v0.8.0(2017/12/29)

    • 使用 swoole
    • 修复 micromonomer 路由的无限递归问题
  • v0.7.1(2017/08/29)

    • 通过策略设计模式重构路由
  • v0.7.0(2017/06/18)

    • 通过 travis-ci 实现 ci
    • 添加 jobs 脚本文件夹
  • v0.6.9(2017/05/22)

    • 对 API 开发过程更友好
      • 请求参数检查:require/length/number
    • 支持数据库的主从配置
  • v0.6.7(2017/05/14)

    • 修复未设置默认时区
    • 性能测试和优化
    • 使用惰性加载思想优化框架
    • 将 Helper 的方法改为框架的功能

感谢

贡献者

这个项目的存在离不开所有贡献者。[贡献].

赞助者

感谢所有赞助者!🙏 [成为赞助者]

赞助商

通过成为赞助商来支持此项目。您的标志将显示在此处,并带有链接到您的网站。[成为赞助商]

联系方式