做码农十几年,没有正经用过几个别人的轮子,却在一直不停的造轮子,乐此不疲。最初只是因为不知道还有开源这回事,以为天经地义轮子就得自己造;后来是因为害怕自己水平菜,别人的代码驾驭不了;再后来水平依然菜,口味却刁钻了,不和自己口味的代码不要。这么多年来,从最初玩 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
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.