做码农十几年,没有正经用过几个别人的轮子,却在一直不停的造轮子,乐此不疲。最初只是因为不知道还有开源这回事,以为天经地义轮子就得自己造;后来是因为害怕自己水平菜,别人的代码驾驭不了;再后来水平依然菜,口味却刁钻了,不和自己口味的代码不要。这么多年来,从最初玩 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 有不少主流的特性,不过我想先展示一下它的特色:
-
低侵入行
在基于 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 做过性能测试,如果有人愿意尝试并提供测试结果,我将非常感谢。PhpBoot 在性能方面不会非常突出,但也不会一塌糊涂。因为设计的初衷并不是解决性能问题,所有并没有特别关注这块,但可以肯定的是使用 Annotation 并不会对对性能造成显著影响,因为从 Annotation 中获取的元信息会被缓存。
帮助和文档
- 在线文档
- QQ 交流群:185193529
- 本人邮箱 [email protected]
写在最后
框架还有很多地方需要完善,比如 ORM 还太简陋、自动文档还想支持 MarkDown 格式、还在实现一个工作流引擎、工作流引擎还会依赖消息队列和定时任务系统、单测覆盖率也不高,等等。我将非常欢迎任何人来使用 PhpBoot,提出问题或者建议,或者一起参与开发,然后成为好基友:D


