PhpBoot

PhpBoot 是为快速开发 微服务 / RESTful API 设计的PHP框架。它可以帮助开发者更聚焦在业务本身, 而将原来开发中不得不做, 但又重复枯燥的事情丢给框架, 比如编写接口文档、参数校验和远程调用代码等。

特色

PhpBoot 框架提供许多主流的特性, 如IOC、HOOK、ORM等。 这些特性都经过精心设计和选择(有些是第三方开源代码,如 PHP-DI),但和其他框架相比较, PhpBoot 最显著的特色是:

1. 以面向对象的方式编写接口

你肯定看到过这样的代码:

// **不用** PhpBoot 的代码
class BookController
{
    public function findBooks(Request $request)
    {
        $name = $request->get('name');
        $offset = $request->get('offset', 0);
        $limit = $request->get('limit', 10);
        ...
        return new Response(['total'=>$total, 'data'=>$books]);
    }

public function createBook(Request $request)
    ...
}

很多主流框架都需要用类似代码编写接口。但这种代码的一个问题是, 方法的输入输出隐藏在实现里, 这不是通常我们提倡的编码方式。如果你对代码要求更高, 你可能还会实现一层 Service 接口, 而在 Controller 里只是简单的去调用 Service 接口。而使用 PhpBoot, 你可以用更自然的方式去定义和实现接口。上面的例子, 在 PhpBoot 框架中实现是这样的:

/**
 * @path /books/
 */
class Books
{
    /**
     * @route GET /
     * @return Book[]
     */
    public function findBooks($name, &$total=null, $offset=0, $limit=10)
    {
        $total = ...
        ...
        return $books;
    }

    /**
     * @route POST /
     * @param Book $book {@bind request.request} bind $book with http body
     * @return string id of created book
     */
    public function createBook(Book $book)
    {
        $id = ...
        return $id;
    }
}

上面两份代码执行的效果是一样的。可以看到 PhpBoot 编写的代码更符合面向对象编程的原则, 以上代码完整版本请见 phpboot-example

2. 轻松支持 Swagger

Swagger 是目前最流行的接口文档框架。虽然很多框架都可以通过扩展支持Swagger, 但一般不是需要编写很多额外的注释, 就是只能导出基本的路由信息, 而不能导出详细的输入输出参数。而 PhpBoot 可以在不增加额外编码负担的情况下, 轻松去完成上述任务,下图为findBooks对应的文档。更多内容请见 文档在线 Demo

https://github.com/caoym/phpboot/raw/master/docs/_static/WX20170809-184015.png

3. 简单易用的分布式支持

使用 PhpBoot 可以很简单的构建分布式应用。通过如下代码, 即可轻松远程访问上面示例中的 Books 接口:

$books = $app->make(RpcProxy::class, [
        'interface'=>Books::class,
        'prefix'=>'http://x.x.x.x/'
    ]);

$books->findBooks(...);

同时还可以方便的发起并发请求, 如:

$res = MultiRpc::run([
    function()use($service1){
        return $service1->doSomething();
    },
    function()use($service2){
        return $service2->doSomething();
    },
]);

更多内容请查看 文档

4. IDE 友好

IDE 的代码提示功能可以让开发者轻松不少, 但很多框架在这方面做的并不好, 你必须看文档或者代码, 才能知道某个功能的用法。PhpBoot 在一开始就非常注重框架的 IDE 友好性, 尽可能让框架保持准确的代码提示。比如下图是 DB 库在 PhpStorm 下的使用:

https://github.com/caoym/phpboot/raw/master/docs/_static/db.gif

可以看到, IDE 的提示是符合 SQL 语法规则的, 并不是简单罗列所有 SQL 指令。

帮助

  • QQ 交流群:185193529
  • 本人邮箱 caoyangmin@gmail.com

文档

安装

1. 安装 Composer

PhpBoot 框架使用 Composer 来管理其依赖包。所以,在你使用 PhpBoot 之前,你必须确认在你电脑上是否安装了 Composer。

curl -s http://getcomposer.org/installer | php

2. 安装 PhpBoot

完成 Composer 安装后,在你的项目目录下执行 composer,即可添加 PhpBoot 依赖。

composer require "caoym/phpboot"

环境要求

PhpBoot 框架有一些系统上的需求:

  • PHP 版本 >= 5.5.9
  • APC 扩展启用
apc.enable=1
  • 如果启用了OPcache,应同时配置以下选项:
opcache.save_comments=1
opcache.load_comments=1

WebServer 配置

为了使用PhpBoot,你需要配置 WebServer,将所有动态请求指向 index.php

1. Nginx

若使用 Nginx ,修改你的项目对应的配置:

server {
    listen 80;
    server_name example.com;
    index index.php;
    error_log /path/to/example.error.log;
    access_log /path/to/example.access.log;
    root /path/to/public;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ \.php {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param SCRIPT_NAME $fastcgi_script_name;
        fastcgi_index index.php;
        fastcgi_pass 127.0.0.1:9000;
    }
}

2. Apache

Apache 的配置稍微复杂,首先你需要启 mod_rewrite 模块,然后在 index.php 目录下添加 .htaccess 文件:

Options +FollowSymLinks
RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]

另外还需要修改虚拟主机的AllowOverride配置

AllowOverride All

注意:由于 WebServer 版本的差异, 以上配置可能不能按预期工作,但这是使用多数 PHP 框架第一步需要解决的问题, 网上有会有很多解决方案,用好搜索引擎即可

示例

下面将通过编写一个简单的图书管理系统接口,演示 PhpBoot 的使用。完整的示例可在这里下载。

1. 关于 RESTful

当前 RESTful 已经不是新鲜的名词了,抛开抽象的定义,我认为一个通俗的解释可以是:按文件系统的方式去设计接口,即把接口提供的功能,当做是对“目录”的“操作”。比如一个登录接口,按 RESTful 设计,可以是POST /tokens/,即把登录,当做新建一个令牌,这里的/tokens/就是“目录”,POST就是对目录的“操作”。关于 RESTful 比较准确的定义,可以看这里。关于 RESTful 最佳实践,可以看这里

2. 示例接口

下面我将演示如何用 PhpBoot 编写一组“图书管理”接口,这些接口包括:

接口名 METHOD URI 请求示例 响应示例
查询图书 GET /books/ GET /books/?name=php&offset=0&limit=1
{
"total": 0,
"data": [
    {
        "id": 0,
        "name": "string",
        "brief": "string",
        "pictures": [
            "string"
        ]
    }
]
}
获取图书详情 GET /books/{id} GET /books/1
{
    "id": 0,
    "name": "string",
    "brief": "string",
    "pictures": [
        "string"
    ]
}
新建图书 POST /books/ POST /books/

{
"id": 0,
"name": "string",
"brief": "string",
"pictures": [
"string"
]
}
123
删除图书 DELETE /books/{id} DELETE /books/1

3. 项目目录结构

  • app
    • Controllers
      • Books.php 接口实现
    • Entities
      • Book.php 数据实体定义
  • config
    • config.php 配置
  • public
    • index.php 入口
  • vendor 依赖包

4. 入口

index.php 作为项目入口, 通常只需要指定配置文件和 Controllers 目录的路径即可。最终项目对外提供的接口, 由不同的 Controllers 的实现类提供。

<?php
require __DIR__.'/../vendor/autoload.php';

use PhpBoot\Application;

$app = Application::createByDefault(__DIR__.'/../config/config.php');
//扫描 Controllers 目录,自动加载所有路由
$app->loadRoutesFromPath( __DIR__.'/../App/Controllers', 'App\\Controllers');
//执行请求
$app->dispatch();

5. 接口实现

5.1. 定义 Book 实体

为了在不同接口间共享“图书信息”的数据结构,我们定义一个实体如下:

class Book
{
    /**
     * @var int
     */
    public $id;
    /**
     * 书名
     * @var string
     */
    public $name='';

    /**
     * 简介
     * @var string
     */
    public $brief='';

    /**
     * 图片url
     * @var string[]
     */
    public $pictures=[];
}
5.2. 定义 Controller

这里定义了 Books 类作为 Controller,后续接口将实现在此 Books 类中。

/**
 * 提供图书管理接口
 * @path /books/
 */
class Books
{

}

上述代码中的注释@path /books/ 表示 Books 下所有接口,都使用/books/ 作为前缀。

5.3. 查询图书接口
/**
 * 查询图书
 *
 * @route GET /
 *
 * @param string $name  查找书名
 * @param int $offset 结果集偏移 {@v min:0}
 * @param int $limit 返回结果最大条数 {@v max:1000}
 * @param int $total 总条数
 * @throws BadRequestHttpException 参数错误
 * @return Book[] 图书列表
 */
public function findBooks($name, &$total, $offset=0, $limit=100)
{
    $total = 1;
    return [new Book()];
}

为便于理解,这段代码只是返回了一组固定的数据,真实场景下应该还会访问数据库或者缓存。下面将说明这段代码的工作方式:

  1. @route 定义了此接口的路由,即 GET /books/(加上了@path 定义的前缀)。

  2. @param 定义了接口的请求参数和类型,如果不声明@param, 接口参数将从函数定义中提取, 如果函数定义中没有申明参数类型,则参数类型被认为是 mixed。

  3. @v 定义了参数的取值范围,若不指定,框架将只会校验请求中的参数类型, 即如果参数是类型是 int,则请求中参数必须是可以转换成 int 的类型,如 123 或者"123"等,否则会返回 400 错误

  4. 函数的返回值将被 json_encode 后输出到 body。如果函数的参数中没有引用类型(引用类型的参数被认为是输出,而不是输入),return 在 json_encode 后即被当做 body 输出,否则 return 将被赋值给 body 中的 "data"。

  5. &$total 是引用类型的参数,会被最为输出,默认输出到 body 中同名变量中。如这个接口中,最终输出的 body 形式如下:

    {
        "total": 1, //来自 &$total
        "data": [   //来自 return
            {
                "id": 0,
                "name": "string",
                "brief": "string",
                "pictures": [
                    "string"
                ]
            }
        ]
    }
    
  6. 如果希望将 return 输出到其他位置,或者不使用默认的输入参数绑定方式,可以使用@bind, 比如 @return Book[] 图书列表 {@bind response.content.books} 将使 return 结果绑定在 json 的 "books" 上,而不是默认的 "data"。

  7. $offset=0, $limit=100定义了默认值,如果请求中不包含这些参数,将使用默认值。

  8. 注释 @return Book[]@throws BadRequestHttpException 并不会对接口的返回有任何影响, 但是会影响文档输出和远程调用(RPC)。

5.4. 获取图书详情接口
/**
 * 获取图书
 * @route GET /{id}
 *
 * @param string $id 指定图书编号
 *
 * @throws NotFoundHttpException 图书不存在
 *
 * @return Book 图书信息
 */
public function getBook($id)
{
    return new Book();
}

路由 @route GET /{id} 指定了 url 的 path 有一个变量{id},变量的值可以通过函数参数 $id 获取

5.5. 新建图书
/**
 * 新建图书
 *
 * @route POST /
 * @param Book $book {@bind request.request} 这里将post的内容绑定到 book 参数上
 * @throws BadRequestHttpException
 * @return string 返回新建图书的编号
 */
public function createBook(Book $book)
{
    return '123';
}
  1. 请求中的 json 会被框架自动转换成函数中需要的对象参数。
  2. {@bind request.request} 表示用请求的 body 构造 $book 变量,若不指定@bind,默认是提取请求 body 中 "book" 字段构造 $book 变量,也就是说请求会是以下形式:
{
    "book": {
            "id": 0,
            "name": "string",
            "brief": "string",
            "pictures": [
                "string"
            ]
        }
}
5.6. 删除图书
/**
 * 删除图书
 *
 * @route DELETE /{id}
 * @param string $id
 * @throws NotFoundHttpException 指定图书不存在
 * @return void
 */
public function deleteBook($id)
{
    
}

如果函数没有返回值,则响应的 http body 会是 void, 而不是空字符串, 因为 基于PhpBoot 实现的接口,默认情况下,http body 总是 json,而空字符串并不是合法的 json。

6. 更多

更多内容见:

Annotation

PhpBoot 框架较多的使用了 Annotation。当然原生 PHP 语言并不支持此项特性,所以实际是通过Reflection提取注释并解析实现,类似很多主流 PHP 框架的做法(如 symfony、doctrine 等)。但又有所不同的是,主流的Annotation 语法基本沿用了 java 中的形式,如:

/**
 * @Route("/books/{id}", name="book_info")
 * @Method("GET")
 */
 public function getBook($id)...

语法严谨,易于扩展,但稍显啰嗦(PhpBoot 1.x 版本也使用此语法)。特别是PHP 由于先天不足(原生不支持Annotation),通过注释,在没有IDE语法提示和运行时检查机制的情况下。如果写 Annotation 过于复杂,那还不然直接写原生代码。所以 PhpBoot 使用了更简单的 Annotation 语法。如:

/**
 * @route GET /books/{id}
 */
 public function getBook($id)...

1. 语法

@<name> [param0] [param1] [param2] ...

  1. name 只能是连续的字母、数字、斜杠''、中横杠'-' 组成的字符串,建议全小写,单词间用'-'分割,如@myapp\my-ann
  2. name和参数,参数和参数见,用空白符(一个或多个连续的空格、制表符)分割。
  3. 参数中如果包含空格,应将参数用双引号""包围,包围内的双引号用\转义,如 @my-ann "the param \"0\"" param1 第一个参数为the param "0"

分割参数、转义的语法和linux 命令行的语法类似

2. 嵌套

嵌套注释,用{}包围, 比如@param int size {@v min:0|max:10}

路由

PhpBoot 支持两种形式的路由定义, 分别是通过加载 Controller 类,分析 Annotation ,自动加载路由,和通过 Application::addRoute 方法手动添加路由。

1. 自动加载路由

你可以通过 Application::loadRoutesFromClass 或者 Application::loadRoutesFromPath 添加路由。框架扫描每个类的每个方法,如果方法标记了@route,将被自动添加为路由。被加载类的形式如下:

/**
 * @path /books
 */
class Books
{
    /**
     * @route GET /{id}
     */
    public function getBook($id)
}

以上代码表示 http 请求 GET /books/{id} 其实现为 Books::getBook, 其中{id}为url 的可变部分。

1.1. @path

语法: @path <prefix>

标注在类的注释里,用于指定 Controller 类中所定义的全部接口的uri 的前缀。

1.2. @route

语法: @path <method> <uri>

标注在方法的注释里,用于指定接口的路由。method为指定的 http 方法,可以是 GET、HEAD、POST、PUT、PATCH、DELETE、OPTION、DELETE。uri 中可以带变量,用{}包围。

2. 手动加载路由

你可以使用 Application::addRoute 手动加载路由,方法如下:

$app->addRoute('GET', '/books/{id}', function(Request $request){
    $books = new Books();
    return $books->getBook($request->get('id'));
});

需要注意的是,此方法添加的路由,将不能自动生成接口文档。

参数绑定

实现接口时,通常需要从 http 请求中提取数据,作为方法的输入参数,并将方法的返回值转换成 http 的输出。参数绑定功能即可以帮你完成上述工作。

1. 输入绑定

1.1. 根据方法定义绑定

默认情况下,框架会从http请求中提取和方法的参数名同名的变量,作为函数的参数。比如:

/**
 * @route GET /books/
 */
public function getBooks($offsit, $limit)

上述代码,对应的 http 请求形式为 GET /books/?offsit=0&limit=10。在此默认请求下:

  • 如果路由 uri 中定义了变量,参数将优先选取 uri 变量。如:
/**
 * @route GET /books/{id}
 */
public function getBook($id)

其中 $id 取自 uri。

  • 对于没有 BODY 的 http 请求(GET、HEAD、OPTION、DELETE),参数来自 querystring 。
  • 其他请求(POST、PUT、OPTION),参数先取 querystring,如果没有,再取 BODY。
1.2. @param

如果在方法的注释中,标注了 @param,就会有用 @param 的绑定信息覆盖默认来自函数定义的绑定信息。@param 可以指定变量的类型,而原函数定义中只能在参数是数组或者对象时才能指定类型。@param 的语法为标准 PHP Document 的语法。

/**
 * @route GET /books/
 * @param int $offsit
 * @param int $limit
 */
public function getBooks($offsit, $limit)

以上代码,除了绑定变量外,还指定了变量类型,即如果输入值无法转换成 int,将返回 400 BadRequest 错误。未指定@param 时,参数的类型默认为 mixed。

1.3. 输入对象参数

输入参数除了是原生类型外,还可以是对象(这里我们把只有属性和 get、set 方法的对象,称为实体(Entity))。如:

/**
 * @route POST /books/
 * @param Book $book {@bind request.request} 将$_POST 内容转换成Book实例
 */
public function createBook(Book $bok)

其中 Book 的的定义:

/**
 * 图书信息
 */
class Book
{
    /**
     * @var int
     * @v optional
     */
    public $id;
    /**
     * 书名
     * @var string
     */
    public $name='';

    /**
     * 简介
     * @var string
     * @v lengthMax:200
     */
    public $brief='';

    /**
     * 图片url
     * @var string[]
     */
    public $pictures=[];
}

框架对 http 请求到实体的转换,有一套自己的逻辑:

  • @var 指定属性的类型,如果类型不匹配,实例化将抛出 InvalidArgumentException 异常
  • 如果不标注 @var,则默认类型为mixed
  • 如果属性有默认值,表示此属性可选,否则认为此属性必选
  • 支持 @v 定义校验规则
  • 实体可以嵌套
1.4. 参数默认值

如果想指定某个输入参数可选,只需给方法参数设置一个默认值。比如:

/**
 * @route GET /books/
 * @param int $offsit
 * @param int $limit
 */
public function getBooks($offsit=0, $limit=10)

注意:php 方法的默认参数, 必须放在方法的最后

2. 输出绑定

2.1. 绑定return

默认情况下,函数的返回值将 jsonencode 后,作为 body 输出。如

/**
 * @route GET /books/{id}
 */
public function getBook($id)
{
    return ['name'=>'PhpBook', 'desc'=>'PhpBook Document'];
}

curl 请求将得到以下结果

$ curl "http://localhost/books/1"
{
    "name": "PhpBook",
    "desc": "PhpBook Document"
}

注意,这里为便于演示,直接在方法中返回了数组(其实这在其他语言里算对象),但你应该为这种返回定义一个类,首先,有很多改善代码质量的理由鼓励使用对象替代这类数组,其次在自动生成文档时,这类数组无发被结构化描述。

2.2. 绑定引用参数

如果方法的参数是引用类型,则这个参数将不会从请求中获取,而是作为输出。比如:

/**
 * @route GET /books/
 * @param int $offsit
 * @param int $limit
 * @return Books[]
 */
public function getBooks($offsit=0, $limit=10, &$total)
{
    $total = 1;
    return [new Books()];
}

curl 请求将得到以下结果

$ curl "http://localhost/books"
{
    "total": 1,
    "data": [
        {
            "name":null, 
            "desc":null
        }
    ]
}

可以看到,$total 输出到了 http body 中。 注意:当接口存在引用参数时,接口的返回值将会被默认绑定到response.content.data,效果和声明{@bind response.content.data}一致。

3. @bind

通过@bind,可以改变默认的绑定关系,将参数与其他输入项绑定,如:

/**
 * @route GET /books/
 * @return Books[] {@bind response.content.books}
 */
public function getBooks($offsit=0, $limit=10, &$total)

表示将返回绑定到响应 body 的 books 变量(响应默认是 json)。

3.1. 绑定输入
  • 请求Body: request.request
  • Query String: request.query
  • Cookie: request.cookies
  • **请求Header:**request.headers
  • **文件:**request.files
3.2. 绑定输出
  • 响应Body: response.content
  • Cookie: response.cookies
  • **请求Header:**response.headers

参数校验

在"参数绑定"时,起始已经支持了两项基本的校验(类型和是否必选),如果要支持更复杂的校验规则,可以通过 @v 指定,如:

/**
 * @route GET /books/
 * @param int $offsit {@v min:0}
 * @param int $limit {@v min:1|max:100}
 */
public function getBooks($offsit=0, $limit=10)

1. 语法

@v <rule>[:param0[,param1...]][|<rule2>...]

  • 多个规则间用|分割。
  • 规则和其参数间用:分割, 如果有多个参数,参数间用,分割。

2. 支持的规则

  • required - Required field
  • equals - Field must match another field (email/password confirmation)
  • different - Field must be different than another field
  • accepted - Checkbox or Radio must be accepted (yes, on, 1, true)
  • numeric - Must be numeric
  • integer - Must be integer number
  • boolean - Must be boolean
  • array - Must be array
  • length - String must be certain length
  • lengthBetween - String must be between given lengths
  • lengthMin - String must be greater than given length
  • lengthMax - String must be less than given length
  • min - Minimum
  • max - Maximum
  • in - Performs in_array check on given array values
  • notIn - Negation of in rule (not in array of values)
  • ip - Valid IP address
  • email - Valid email address
  • url - Valid URL
  • urlActive - Valid URL with active DNS record
  • alpha - Alphabetic characters only
  • alphaNum - Alphabetic and numeric characters only
  • slug - URL slug characters (a-z, 0-9, -, _)
  • regex - Field matches given regex pattern
  • date - Field is a valid date
  • dateFormat - Field is a valid date in the given format
  • dateBefore - Field is a valid date and is before the given date
  • dateAfter - Field is a valid date and is after the given date
  • contains - Field is a string and contains the given string
  • creditCard - Field is a valid credit card number
  • optional - Value does not need to be included in data array. If it is however, it must pass validation.

依赖注入

PhpBoot 使用开源项目 PHP-DI 作为依赖注入的基础实现。

1. 手动注入

手动注入是只通过配置文件显式的指定注入方式。详见PHP-DI文档

2. 自动注入

2.1. 构造函数注入
class Books
{
    /**
     * @param LoggerInterface $logger 通过依赖注入传入
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger;
    }
    ...
}
2.2. 属性注入
class Books
{
    use EnableDIAnnotations; //启用通过@inject标记注入依赖
    /**
     * @inject 
     * @var DB
     */
    private $db;
}

注意:PhpBoot 禁用了PHP-DI的 Annotation 注入方式,@inject 方式是 PhpBoot 实现的

数据库

1. 配置

可以通过依赖注入的方式,对数据库进行配置。

  1. 在需要数据库的类中加入依赖注入代码:

    use PhpBoot\DB\DB;
    use PhpBoot\DI\Traits\EnableDIAnnotations;
    
    class Books
    {
        use EnableDIAnnotations; //启用通过@inject标记注入依赖
    
        /**
         * @inject
         * @var DB
         */
        private $db;
    
        public function getBooks()...
    }
    

    框架在实例化Books后,根据@inject注释, 自动给属性$db赋值,其逻辑等价于:

    $books->db = $app->get(DB::class);
    
  2. 修改数据库配置

    在 config.php 中加入以下配置(数据库地址等需根据实际情况修改):

    'DB.connection'=> 'mysql:dbname=phpboot-example;host=127.0.0.1',
    'DB.username'=> 'root',
    'DB.password'=> 'root',
    'DB.options' => [],
    

2. 编写 SQL

下面将通过实现 createBook、deleteBook、updateBook、findBooks 方法,演示insert、delete、update、select 的使用。

2.1 INSERT
public function createBook(Book $book)
{
    $newId = $this->db->insertInto('books')
        ->values([
	        'name'=>$book->name,
	        'brief'=>$book->brief,
	        ...
        ])
        ->exec()
        ->lastInsertId(); 
    return $newId;
}
2.2 DELETE
public function deleteBook($id)
{
    $this->db->deleteFrom('books')
        ->where(['id'=>$id])
        ->exec();
}
2.3 UPDATE
public function updateBook(Book $book)
{
    $this->db->update('books')
        ->set([
	        'name'=>$book->name,
	        'brief'=>$book->brief,
	        ...
        ])
        ->where(['id'=>$book->id])
        ->exec(); 
}
2.4 SELECT
public function findBooks($name, $offsit, $limit)
{
    $books = $this->db->select('*')
        ->from('books')
        ->where('name LIKE ?', "%$name%")
        ->orderBy('id')
        ->limit($offsit, $limit)->get();
        
    return $books;
}

3. 高级用法

上述示例展示了PhpBoot\DB的基础用法,PhpBoot\DB同时也支持更复杂的SQL。

3.1 复杂 WHERE

类似 SQL WHERE a=1 OR (b=2 and c=3), 可以以下代码实现:

->where(['a'=>1])
->orWhere(function(ScopedQuery $query){
    $query->where(['b'=>2, 'c'=>3])
})

上面例子中,ScopedQuery 中还能再嵌套 ScopedQuery

3.2 JOIN
$db->select('books.*', DB::raw('authors.name as author'))
    ->from('books')
    ->where(['books.id'=>1])
    ->leftJoin('authors')->on('books.authorId = authors.id')
    ->get()
3.3 WHERE ... IN ...

使用PDO时,WHERE IN的预处理方式很不方便,需要为IN的元素预留数量相等的?, 比如:

$pdo->prepare(
    'SELECT * FROM table WHERE a IN (?,?,?)'
)->execute([1,2,3])

而使用PhpBoot\DB可以解决这个问题:

$db->select()->from('table')->where('a IN (?)', [1,2,3]);
3.4 使用 SQL 函数

默认情况下,框架会对输入做转换, 如会在表名和列名外加上``,会把变量作为绑定处理,比如下面的语句

$db->select('count(*) AS count')
    ->from('table')
    ->where(['time'=>['>'=>'now()']]);

等价 的 SQL:

SELECT `count(*) AS count` FROM `table` where `time` > 'now()'

如果希望框架不做转换,需要使用DB::raw(),比如:

$db->select(DB::raw('count(*) AS count'))
    ->from('table')
    ->where(['time'=>['>'=>DB::raw('now()')]]);

与下面 SQL 等价

SELECT count(*) AS count FROM `table` where `time` > now()
3.5 子查询

下面代码演示子查询用法:

$parent = $db->select()->from('table1')->where('a=1');
$child = $db->select()->from($parent);

与下面 SQL 等价

SELECT * FROM (SELECT * FROM `table1` WHERE a=1)
3.6 事务
$db->transaction(
    function(DB $db){
        $db->update('table1')->...
        $db->update('table1')->...
    }
)

事务允许嵌套,但只有最外层的事务起作用,内部嵌套的事务与最外层事务将被当做同一个事务。

4. 使用多个数据库

PhpBoot 为DB 类定义了默认的构造方式,形式如下:

DB::class => \DI\factory([DB::class, 'connect'])
    ->parameter('dsn', \DI\get('DB.connection'))
    ->parameter('username', \DI\get('DB.username'))
    ->parameter('password', \DI\get('DB.password'))
    ->parameter('options', \DI\get('DB.options')),

所以如果你的业务只使用连接一个数据库,只需要对DB.connection, DB.username ,DB.password, DB.options进行配置即可。但有的时候可能需要对在应用中连接不同的数据库,这时可以通过依赖注入配置多个库,如:

  1. 先配置另一个数据库连接

    'another_db' => \DI\factory([DB::class, 'connect'])
        ->parameter('dsn', 'mysql:dbname=phpboot-example;host=127.0.0.1')
        ->parameter('username', 'root')
        ->parameter('password', 'root')
        ->parameter('options', [])
    
  2. 在需要的地方注入此连接

    use PhpBoot\DB;
    
    class Books
    {
        use EnableDIAnnotations; //启用通过@inject标记注入依赖
        /**
         * @inject another_db
         * @var DB
        */
        private $db2;
    }
    

缓存

PhpBoot 使用doctrine/cache作为底层缓存实现。doctrine/cache 支持的缓存类型有: APC、APCu、Memcache、Xcache、Redis。

业务缓存

如果需要在业务代码中使用缓存, 此处以Redis为例, 演示基本用法。

  1. 在修改配置文件 config.php, 增加以下代码:
'redis' => \DI\object(\Doctrine\Common\Cache\RedisCache::class)
    ->method('setRedis', \DI\factory(function(){
        $redis = new \Redis();
        $redis->connect('127.0.0.1', 6379);
        return $redis;
    })),
  1. 在控制器中需要 redis 的地方, 注入 redis 实例
/**
 * @inject redis
 * @var \Doctrine\Common\Cache\RedisCache
 */
private $redis;

系统缓存

PhpBoot 框架为提高性能, 会将路由及Annotation 分析后的其他元信息进行缓存。生产环境建议使用 APC 扩展, 开发环境可以用文件缓存代替 apc, 方法是在 config.php 里加一个配置。

Cache::class => \DI\object(FilesystemCache::class)
    ->constructorParameter('directory', sys_get_temp_dir())

文档输出

1. Swagger 文档

Swagger 是流行的 HTTP API 描述规范,同时 Swagger 官方还提供了丰富的工具,比如用于文档展示和接口测试的 Swagger UI, 相关资料请阅读官方文档

以 phpboot-example 为例,生成的文档如下。文档中除了描述了接口的路由、参数定义、参数校验,还提供了接口测试工具。点击这里查看在线 Demo

_images/WX20170809-184015.png

PhpBoot 项目可以很方便的生成 Swagger 文档,无需添加额外的 Annotation(很多框架为支持 Swagger,通常需要增加很多额外的注释,而这些注释只用于 Swagger。PhpBoot 生成 Swagger 的信息来自路由的标准注释,包括@route, @param, @return,@throws 等)

如需开启 Swagger 文档,只需在在 Application 初始化时 添加以下代码:

PhpBoot\Docgen\Swagger\SwaggerProvider::register($app , function(Swagger $swagger){
    $swagger->host = 'example.com';
    $swagger->info->description = 'this is the description of the apis';
    ...
});

然后访问你的项目 url+/docs/swagger.json如( http://localhost/docs/swagger.json),即可获取 json 格式的 Swagger 文档。

2. MarkDown 文档

开发中...

ORM

目前 PhpBoot 提供基本的 ORM 支持,包括:

1. 定义实体

实体对应数据库的表, 实体的属性名和数据库的列名一致。下面是一个典型的实体定义:

/**
 * 图书信息
 * @table books
 * @pk id
 */
class Book
{
    /**
     * @var int
     */
    public $id;
    
    /**
     * 书名
     * @var string
     */
    public $name='';
    
    /**
     * 图片url
     * @var string[]
     */
    public $pictures=[];
}

其中:

  • @table 指定表名
  • @pk 指定表的主键
  • @var 定义列的类型,如果类型为对象或者数组,则保存到数据库是将被序列化为 json

可以看到,ORM 中的实体和接口中的实体很类似,事实上,我们鼓励在 ORM 和接口中复用实体类

2. 操作数据库

PhpBoot 提供量个组方法,modelmodels, 分别用于操作实体“实例”和实体“类”。

2.1. model 方法

model() 方法用于操作实体“实例”,或者说操作单个实体对象。

2.1.1 create

存储指定实体实例(对应 SQL 的 insert)

$book = new Book();
$book->name = ...
...

\PhpBoot\model($this->db, $book)->create();
echo $book->id; //获取自增主键的值
2.1.2 update

更新实体对应的数据库记录(对应 SQL 的 update)

$book = new Book();
$book->id = ...
...

\PhpBoot\model($this->db, $book)->update();
2.1.3 delete

删除实体对应的数据库记录(对应 SQL 的 delete )

$book = new Book();
$book->id = ...

\PhpBoot\model($this->db, book)->delete();
2.2 models 方法

models() 方法用于操作实体“类”,或者说操作一组实体。

2.2.1. find

根据主键查找(对应 SQL 的 select )

$book = \PhpBoot\models($this->db, Book::class)->find($id);
2.2.2. findWhere

根据组合查询条件查找(对应 SQL 的 select )

$books = \PhpBoot\models($this->db, Book::class)
    ->findWhere(['name'=>'abc'])
    ->get();
2.2.3. update

根据主键更新(对应 SQL 的 update )

\PhpBoot\models($this->db, Book::class)
    ->update(1 ['name'=>'abc']);
2.2.4. updateWhere

根据组合查询条件更新(对应 SQL 的 update )

\PhpBoot\models($this->db, Book::class)
    ->updateWhere(['name'=>'abc'], ['id'=>1])
    ->exec();
2.2.6. delete

根据主键删除(对应 SQL 的 delete )

\PhpBoot\models($this->db, Book::class)
    ->delete(1);
2.2.7. deleteWhere

根据组合查询条件删除(对应 SQL 的 delete )

\PhpBoot\models($this->db, Book::class)
    ->deleteWhere(['id'=>1])
    ->exec();

Hook

1. 定义 Hook

演示如何通过 Hook 实现 Basic Authorization 登录校验

/**
 * 简单登录校验
 *
 * 实现了 Basic Authorization
 * @package App\Hooks
 */
class BasicAuth implements HookInterface
{
    /**
     * @param Request $request
     * @param callable $next
     * @return Response
     */
    public function handle(Request $request, callable $next)
    {
        $auth = $request->headers->get('Authorization');
        $auth or \PhpBoot\abort(new UnauthorizedHttpException('Basic realm="PhpBoot Example"', 'Please login...'));
        $auth = explode(' ', $auth);
        $auth[1] == md5("{$this->username}:{$this->password}") or fail(new UnauthorizedHttpException('Basic realm="PhpBoot Example", "Invalid username or password!"'));
        return $next($request);
    }

    /**
     * @var string
     */
    public $username;
    /**
     * @var string
     */
    public $password;
}

可以看到,Hook 只需要继承HookInterface,实现 handle 方法。

2. 使用 Hook

为需要的接口添加此 Hook

2.1. 通过 @hook 添加 Hook
/**
 * @route POST /books/
 * @param Book $book {@bind request.request}
 * @hook \App\Hooks\BasicAuth 指定此接口需要BasicAuth校验
 */
public function createBook(Book $bok)

一个接口可以指定多个 Hook,执行的顺序依照@hook 定义的顺序。

2.2. 添加路由时指定 Hook

Application::addRoute()、Application::loadRoutes*() 方法添加路由时,可以指定 Hook ,如:

$app->loadRoutesFromPath($path, [BaseAuth::class]);
2.3. 设置全局 Hook

Application::setGlobalHooks 用于设置全局 Hook, 如:

Application::setGlobalHooks([BaseAuth::class])

全局 Hook 不依赖于是否存在路由,即就算没有请求对应的路由,全局 Hook 还是会被执行。

关于 Hook 的更多细节, 可以参考\PhpBoot\Controller\Hooks\Cors的实现。

RPC

RPC即远程过程调用,是一种常用的分布式系统间访问接口的方式。PhpBoot 提供强大又简单易用的 RPC 支持,可以让你像使用本地接口一样,方便的使用远程接口。

1. 示例

下面将通过实现一个订单服务的示例,演示 PhpBoot RPC 的使用。

1.1. 定义接口

为保持示例尽量简单,这里我们只实现“创建订单”这一个接口。

/**
 * @path /orders
 */
interface OrderServiceInterface
{
    /**
     * @route POST /
     * @param ProductInfo $product 商品快照
     * @return string 返回订单号
     */
    public function createOrder(ProductInfo $product);
}
1.2. 实现接口

接口定义好以后, 我们需要在服务端,实现该服务接口,以便可以对外提供访问。

/**
 * @path /orders
 */
class OrderService implements OrderServiceInterface 
{
    /**
     * @route POST /
     * @param ProductInfo $product 商品快照
     * @return string 返回订单号
     */
    public function createOrder(ProductInfo $product)
    {
        // create the order
        return $orderId;
    }
}
1.3. 远程调用接口

在客户端,可以通过下面方法调用远程的接口。

$orderService =  $app->make(
    RpcProxy::class, 
    [
        'interface'=>OrderServiceInterface::class, 
        'prefix'=>'http://10.x.x.1/'
    ]
);
/**@var OrderServiceInterface $orderService*/

$orderId = $orderService->createOrder($product);

另一种推荐的方法是通过依赖注入创建代理类。如

//配置依赖

return [
    OrderServiceInterface::class 
        => \DI\objet(RpcProxy::class)
            ->constructorParameter('interface', OrderServiceInterface::class)
            ->constructorParameter('prefix', 'http://10.x.x.1/')
    
]
// 注入依赖

class AnotherService
{
    ...
    
    /**
     * @inject 
     * @var OrderServiceInterface
     */
    private $orderService;
    
    public function doSomething()
    {
        $orderId = $this->orderService->createOrder($product)
    }
}

2. 注意

由于 RpcProxy 默认通过 __call 实现远程方法的调用,所以无法传递引用参数。当接口参数中存在引用参数时,应该针对接口实现一个RpcProxy的子类,并重写包含引用参数的方法。以下是示例

// 这是个典型的例子,接口的方法中有引用类型参数
/**
 * @path /orders
 */
interface OrderServiceInterface
{
    /**
     * @route GET /
     * @param int $offset
     * @param int $limit
     * @param int $total 此为引用类型参数, 用于返回查询的总条数
     * @return Order[] 返回订单列表
     */
    public function getOrders($offset, $limit, &$total);
}
// 这是个典型的例子,接口的方法中有引用类型参数
/**
 * @path /orders
 */
class OrderServiceProxy extends RpcProxy implements OrderServiceInterface 
//如果不想实现OrderServiceInterface的所有方法,也可以不继承OrderServiceInterface
{
    /**
     * @route GET /
     * @param int $offset
     * @param int $limit
     * @param int $total 此为引用类型参数, 用于返回查询的总条数
     * @return Order[] 返回订单列表
     */
    public function getOrders($offset, $limit, &$total)
    {
        return $this->__call(__FUNCTION__, [$offset, $limit, &$total]);
    }
}
//接下来可以通过OrderServiceProxy 访问远程接口了

$orderService =  $app->make(
    OrderServiceProxy::class, 
    [
        'interface'=>OrderServiceInterface::class, 
        'prefix'=>'http://10.x.x.1/'
    ]
);

$orderService->getOrders...

3. 并发访问

在使用远程服务时,有时可能需要同时访问多个远程接口。如果能并行执行,在一些情况下可以大大减少接口执行时间。PhpBoot RPC 提供了并发执行的功能。使用方法如下:

$orderService = $app->make ...
$bookService = $app->make ...


$rpcRes = MultiRpc::run([
    function()use(orderService){
        return orderService->getOrders(...);
    },
    function(){
        return bookService->getBooks(...);

    }
])

$res = []
foreach($rpcRes as $i){
    list($success, $error) = $i
    if($error){
        //执行失败的原因
    }else{
        //执行成功, 处理$success
    }
}
return $res;

注意,MultiRpc 内部是将需并发执行的操作,调用转换为递归调用,并在递归的最后,等待所有异步操作完成。 所以实际上,真正并发执行的只是网络请求,所有网络请求结束后,后续代码执行还是串行的

命令行

使用PHP开发非http服务时,如定时任务等,常需要通过命令行模式启动php脚本,PhpBoot的CLI支持可以让你快速完成这方面工作。

1. 实现命令行

/**
 * @command test    //可选 @command指定命令的前缀
 */
namespace App\Commands

class TestCommand
{
    /**
     * run test
     *
     * the run test
     * @command run    // 命令唯一标识
     *
     * @param int $arg0 arg 0
     * @param string $arg1 arg 1
     * @param string[] $arg2 arg 2
     */
    public function runTest($arg0, $arg1, $arg2){
        print_r([$arg0, $arg1, $arg2]);
        return 0; // 返回进程的exit code
    }
}

2. 编写入口文件 cli.php

use \PhpBoot\Application;
use \PhpBoot\Console;

ini_set('date.timezone','Asia/Shanghai');
require __DIR__.'/../vendor/autoload.php';

// 加载配置
$app = Application::createByDefault(__DIR__ . '/../config/config.php');
// 加载命令行
$console = Console::create($app);
$console->loadCommandsFromPath(__DIR__.'/../App/Commands', 'App\\Commands');
// 执行命令行
$console->run();

3. 执行命令

执行 php ./cli.php
$ php ./cli.php 
Console Tool

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  help     Displays help for a command
  list     Lists commands
  my.test   : run test
执行 php ./cli.php my.test 1 2 a b
$ php ./cli.php my.test 1 2 a b 
array(3) {
  [0]=>
  int(1)
  [1]=>
  string(1) "2"
  [2]=>
  array(2) {
    [0]=>
    string(1) "a"
    [1]=>
    string(1) "b"
  }
}

FAQ

是否必须使用 APC 扩展

PhpBoot 框架为提高性能, 会将路由及Annotation 分析后的其他元信息进行缓存。生产环境建议使用 APC 扩展, 开发环境可以用文件缓存代替 apc, 方法是在 config.php 里加一个配置

Cache::class => \DI\object(FilesystemCache::class)
    ->constructorParameter('directory', sys_get_temp_dir())

composer 更新失败怎么办

packagist.org 国内访问不稳定,可以翻墙试试,或者用国内的镜像phpcomposer, 执行下面命令

composer config repo.packagist composer https://packagist.phpcomposer.com