那啥,我又造了个轮子

2017-08-06 15:58:06 +08:00
 caoyangmin

做码农十几年,没有正经用过几个别人的轮子,却在一直不停的造轮子,乐此不疲。最初只是因为不知道还有开源这回事,以为天经地义轮子就得自己造;后来是因为害怕自己水平菜,别人的代码驾驭不了;再后来水平依然菜,口味却刁钻了,不和自己口味的代码不要。这么多年来,从最初玩 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 )也可以。但怪我口味太刁钻, 硬是编出了这些理由:

  1. 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。

  2. Laravel 和 symfony 都没有提供面向接口的开发方式,因为 Controller 的输入输出参数隐藏在代码实现里。也因此无法导出结构化数据,不容易生成接口文档。

  3. Laravel 的 ORM 没有实体的概念,导致 Model 和 Controller 间无法共享数据对象。

  4. 没想到第四点就开始写 PhpBoot 了...

PhpBoot 的特色

PhpBoot 有不少主流的特性,不过我想先展示一下它的特色:

  1. 低侵入行

    在基于 PhpBoot 开发时,你所实现的代码里几乎看不到框架的影子。

  2. 参数双向绑定

    很方便的将方法的输入输出映射到 HTTP 的请求和响应上去。让你更自然的去写一个方法或者函数,而不是在代码去处理恼人的 Request 和 Response 对象。

  3. 极简单但强大的 Annotation 能力

    尽量保持和利用 PhpDocment 标准注释的语意,具体再后面示例上展示。

  4. 摆脱在文档、接口、SQL 数、远程调用间枯燥的重复代码

    这是初衷

终于到示例了

我将通过编写一组( YY 的)“图书管理”接口,分步骤,展示 PhpBoot 的这些特性。先来一个最简单的例子:

  1. index.php

    require __DIR__.'/../vendor/autoload.php';
    
    // 加载配置
    $app = \PhpBoot\Application::createByDefault(
        __DIR__.'/../config/config.php'
    );
    // 加载路由
    $app->loadRoutesFromPath( __DIR__.'/../App/Controllers', 'App\\Controllers');
    // 执行请求
    $app->dispatch();
    
  2. 实现接口

    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、高级的参数绑定、自动文档等特性,下面将为你展示这些:

  1. 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=[];
    }
    
  2. 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 中获取的元信息会被缓存。

帮助和文档

写在最后

框架还有很多地方需要完善,比如 ORM 还太简陋、自动文档还想支持 MarkDown 格式、还在实现一个工作流引擎、工作流引擎还会依赖消息队列和定时任务系统、单测覆盖率也不高,等等。我将非常欢迎任何人来使用 PhpBoot,提出问题或者建议,或者一起参与开发,然后成为好基友:D

6418 次点击
所在节点    PHP
19 条回复
linoder
2017-08-06 16:38:33 +08:00
这个项目可以生成 swagger 用的文档么? 多个接口用到相同的 defination 时候,框架生成文档会自动指向同一个
defination 吗?
caoyangmin
2017-08-06 16:47:12 +08:00
@linoder 是的,这是必须的。你可以看下这个示例生成的文档 http://118.190.86.50:8007/index.html?url=http://118.190.86.50:8009/docs/swagger.json
jimisun
2017-08-06 21:03:45 +08:00
我一直以为 php 没有 class 只有 java 有,印象中的 php 就和 jsp 一样……全部在网页中……我错了?
zhuoziyu
2017-08-06 21:51:22 +08:00
@jimisun 大清已经亡了^_^
voocel
2017-08-06 21:54:07 +08:00
@jimisun 活在上古时代吧
littleylv
2017-08-06 21:57:35 +08:00
@jimisun #3 大清还没亡?
lxml
2017-08-06 22:03:10 +08:00
问个问题,PHP 这种 $xxx 标记变量的语法特色是源自其他语言还是自己独创,看起来好带感。
k9982874
2017-08-06 22:07:52 +08:00
看到 require 我就看不下去了……
caoyangmin
2017-08-06 22:13:46 +08:00
simaguo
2017-08-06 22:20:41 +08:00
我后清还在呢
Patrick95
2017-08-06 22:23:21 +08:00
@k9982874 这有啥看不下去的。。require autoload.php 不很正常吗…
jhdxr
2017-08-06 22:31:00 +08:00
@k9982874 完全不用 require 或 include 就意味着你得把所有代码写在一个文件里。。。你确定你更喜欢的是这种方式吗?
HYSS
2017-08-06 22:42:18 +08:00
@k9982874 我感觉你应该是没有了解什么是自动加载 use 不代表引入文件 前提还得是 require
jimisun
2017-08-06 23:01:22 +08:00
@littleylv
@voocel
@zhuoziyu
抱歉,java 新手,没用过 php 只是印象…
vjnjc
2017-08-07 12:13:34 +08:00
@jimisun 我也跟你一样,因为前公司流行用 array。。。根本就是 oo 的概念
KgM4gLtF0shViDH3
2017-08-07 15:40:19 +08:00
php 的语法看着好难受。。
jtcba12
2017-08-07 17:05:55 +08:00
66666
HYSS
2017-08-07 20:25:34 +08:00
@bestkayle 你用下 php7 的没事了
lixueliu
2017-08-09 13:59:01 +08:00
Star

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/380817

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX