做码农十几年,没有正经用过几个别人的轮子,却在一直不停的造轮子,乐此不疲。最初只是因为不知道还有开源这回事,以为天经地义轮子就得自己造;后来是因为害怕自己水平菜,别人的代码驾驭不了;再后来水平依然菜,口味却刁钻了,不和自己口味的代码不要。这么多年来,从最初玩 DirectShow,写了各种视频处理的 Filter ;到后来挣扎在 C++网络编程的泥潭里,ACE 和 boost 我都觉得不如自己写的,光一个引用计数,从跨平台、线程安全,到弱引用、聚合,玩的不亦乐乎。后来实在受不了天天和编译器、操作系统、Crash 做战斗,又觉得 Java 太过啰嗦,就转投了 PHP (不过要害得我找不到工作了)。发现 PHP 的生态确实不如 Java,不过这正合我意,于是这几年,又造了不少轮子。
废话完了,介绍下今天这款轮子。名字是 PhpBoot (请点这里给它加个星吧),因为准备造它时,脑子里想到了 Spring Boot。当时我在开发一些业务层的接口,通常为了实现一个极其简单的接口,我需要写一遍文档、实现一遍接口、编写一些 sql, 如果用了 Gateway 这类东西,还得注册一次接口,如果是个分布式系统,很可能还得写个代理客户端。很自然,我想弄一个框架,让我实现完接口,其他都自动帮我做了。这就是写 PhpBoot 的初衷。
你很可能会说,这些要求很多框架都能实现。确实,比如 swagger-php 加 Laravel,swagger-php 解决文档问题,Laravel 解决后面的,如果需要 RPC,再找个框架组合一下。就算不用 Laravel,用 Symfony + Doctrine (解决 ORM )也可以。但怪我口味太刁钻, 硬是编出了这些理由:
swagger-php 的注释太反人类,请看:
/**
* @SWG\Get(
* path="/pets",
* description="Returns all pets from the system that the user has access to",
* operationId="findPets",
* produces={"application/json", "application/xml", "text/xml", "text/html"},
* @SWG\Parameter(
* name="tags",
* in="query",
* description="tags to filter by",
* required=false,
* type="array",
* @SWG\Items(type="string"),
* collectionFormat="csv"
* ),
* @SWG\Parameter(
* name="limit",
* in="query",
* description="maximum number of results to return",
* required=false,
* type="integer",
* format="int32"
* ),
* @SWG\Response(
* response=200,
* description="pet response",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Pet")
* ),
* ),
* @SWG\Response(
* response="default",
* description="unexpected error",
* @SWG\Schema(
* ref="#/definitions/ErrorModel"
* )
* )
* )
*/
public function findPets()
{
}
有这功夫我情愿写 word。
Laravel 和 symfony 都没有提供面向接口的开发方式,因为 Controller 的输入输出参数隐藏在代码实现里。也因此无法导出结构化数据,不容易生成接口文档。
Laravel 的 ORM 没有实体的概念,导致 Model 和 Controller 间无法共享数据对象。
没想到第四点就开始写 PhpBoot 了...
PhpBoot 有不少主流的特性,不过我想先展示一下它的特色:
低侵入行
在基于 PhpBoot 开发时,你所实现的代码里几乎看不到框架的影子。
参数双向绑定
很方便的将方法的输入输出映射到 HTTP 的请求和响应上去。让你更自然的去写一个方法或者函数,而不是在代码去处理恼人的 Request 和 Response 对象。
极简单但强大的 Annotation 能力
尽量保持和利用 PhpDocment 标准注释的语意,具体再后面示例上展示。
摆脱在文档、接口、SQL 数、远程调用间枯燥的重复代码
这是初衷
我将通过编写一组( YY 的)“图书管理”接口,分步骤,展示 PhpBoot 的这些特性。先来一个最简单的例子:
index.php
require __DIR__.'/../vendor/autoload.php';
// 加载配置
$app = \PhpBoot\Application::createByDefault(
__DIR__.'/../config/config.php'
);
// 加载路由
$app->loadRoutesFromPath( __DIR__.'/../App/Controllers', 'App\\Controllers');
// 执行请求
$app->dispatch();
实现接口
class Books
{
/**
* @route GET /books/
*/
public function getBooks($name, $offset=0, $limit=10)
{
return [];
}
}
上面实现的 Books::getBooks 方法,将被 PhpBoot 加载后,注册为 GET /books/ 接口,并且对应的 query 参数为 name、offset、和 limit,其中 offset 和 limit 参数可选。请求的形式可以是 GET /books/?name=PHP&limit=20
。PhpBoot 通过分析注释中的 @route,获取路由信息。
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 语法。
上面的示例没有展示如依赖注入、ORM、高级的参数绑定、自动文档等特性,下面将为你展示这些:
Book 实体
/**
* @table books
* @pk id
*/
class Book
{
/**
* @var int
* @v optional
*/
public $id;
/**
* @var string
*/
public $name='';
/**
* @var string
*/
public $brief='';
/**
* @var string[]
*/
public $pictures=[];
}
Books 接口
/**
* 图书管理
* @path /books
*/
class Books
{
use EnableDIAnnotations; //启用通过 @inject 标记注入依赖
/**
* @route GET /
*
* @param string $name 查找书名
* @param int $offset 结果集偏移 {@v min:0}
* @param int $limit 返回结果最大条数 {@v max:1000}
* @param int $total 总条数 {@bind response.content.total}
* @throws BadRequestHttpException 参数错误
* @return Book[] 图书列表 {@bind response.content.books}
*/
public function findBooks($name, &$total, $offset=0, $limit=100)
{
$query = \PhpBoot\model($this->db, Book::class)
->where(['name'=>['LIKE'=>"%$name%"]]);
$total = $query->count();
return $query->limit($offset, $limit)->get();
}
/**
* @route GET /{id}
*
* @param string $id 指定图书编号
* @throws NotFoundHttpException 图书不存在
* @return Book 图书信息
*/
public function getBook($id)
{
$book = \PhpBoot\model($this->db, Book::class)
->find($id) or \PhpBoot\abort(new NotFoundHttpException("book $id not found"));
return $book;
}
/**
* @route POST /
*
* @param Book $book {@bind request.request} 这里将 post 的内容绑定到 book 参数上
* @throws BadRequestHttpException
* @return string 返回新建图书的编号
*/
public function createBook(Book $book)
{
!$book->id or \PhpBoot\abort(new BadRequestHttpException("should not specify id while creating books"));
\PhpBoot\model($this->db, $book)->create();
return $book->id;
}
/**
* @inject
* @var DB
*/
private $db;
}
这个例子中,你看到了 @bind 的参数绑定(没有 @bind 时是默认绑定规则);@v 的参数校验;@inject 的依赖注入;以及 ORM 和文档生成(见在线 DEMO)
上面的示例的完整代码,可在此处下载
介绍完成 PhpBoot 的基本用法,以下为你罗列了框架的主要特性:
暂时还没有对 PhpBoot 做过性能测试,如果有人愿意尝试并提供测试结果,我将非常感谢。PhpBoot 在性能方面不会非常突出,但也不会一塌糊涂。因为设计的初衷并不是解决性能问题,所有并没有特别关注这块,但可以肯定的是使用 Annotation 并不会对对性能造成显著影响,因为从 Annotation 中获取的元信息会被缓存。
框架还有很多地方需要完善,比如 ORM 还太简陋、自动文档还想支持 MarkDown 格式、还在实现一个工作流引擎、工作流引擎还会依赖消息队列和定时任务系统、单测覆盖率也不高,等等。我将非常欢迎任何人来使用 PhpBoot,提出问题或者建议,或者一起参与开发,然后成为好基友:D
你肯定看到过这样的代码:
// **不用** 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)
{
...
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。
使用 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();
},
]);
更多内容请查看文档
IDE 的代码提示功能可以让开发者轻松不少, 但很多框架在这方面做的并不好, 你必须看文档或者代码, 才能知道某个功能的用法。PhpBoot 在一开始就非常注重让代码保持IDE友好, 经可能让所有代码都能有正确的代码提示。比如下图是 DB 库在 PhpStorm IDE 下的使用:
1
linoder 2017-08-06 16:38:33 +08:00
这个项目可以生成 swagger 用的文档么? 多个接口用到相同的 defination 时候,框架生成文档会自动指向同一个
defination 吗? |
2
caoyangmin OP @linoder 是的,这是必须的。你可以看下这个示例生成的文档 http://118.190.86.50:8007/index.html?url=http://118.190.86.50:8009/docs/swagger.json
|
3
jimisun 2017-08-06 21:03:45 +08:00 via Android
我一直以为 php 没有 class 只有 java 有,印象中的 php 就和 jsp 一样……全部在网页中……我错了?
|
7
lxml 2017-08-06 22:03:10 +08:00
问个问题,PHP 这种 $xxx 标记变量的语法特色是源自其他语言还是自己独创,看起来好带感。
|
8
k9982874 2017-08-06 22:07:52 +08:00 via iPad
看到 require 我就看不下去了……
|
9
caoyangmin OP |
10
simaguo 2017-08-06 22:20:41 +08:00
我后清还在呢
|
12
jhdxr 2017-08-06 22:31:00 +08:00
@k9982874 完全不用 require 或 include 就意味着你得把所有代码写在一个文件里。。。你确定你更喜欢的是这种方式吗?
|
14
jimisun 2017-08-06 23:01:22 +08:00 via Android
|
16
KgM4gLtF0shViDH3 2017-08-07 15:40:19 +08:00
php 的语法看着好难受。。
|
17
jtcba12 2017-08-07 17:05:55 +08:00
66666
|
19
lixueliu 2017-08-09 13:59:01 +08:00
Star
|