V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
caoyangmin
V2EX  ›  PHP

那啥,我又造了个轮子

  •  3
     
  •   caoyangmin · 2017-08-06 15:58:06 +08:00 · 6385 次点击
    这是一个创建于 2668 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    第 1 条附言  ·  2017-08-06 20:32:48 +08:00
    第 2 条附言  ·  2017-08-10 00:37:32 +08:00

    补充 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)
        {
            ...
            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. 简单易用的分布式支持

    使用 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();
        },
    ]);
    

    更多内容请查看文档

    第 3 条附言  ·  2017-08-10 00:38:06 +08:00

    补充 PhpBoot 特色

    3. IDE 友好

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

    19 条回复    2017-08-09 13:59:01 +08:00
    linoder
        1
    linoder  
       2017-08-06 16:38:33 +08:00
    这个项目可以生成 swagger 用的文档么? 多个接口用到相同的 defination 时候,框架生成文档会自动指向同一个
    defination 吗?
    caoyangmin
        2
    caoyangmin  
    OP
       2017-08-06 16:47:12 +08:00 via iPhone
    @linoder 是的,这是必须的。你可以看下这个示例生成的文档 http://118.190.86.50:8007/index.html?url=http://118.190.86.50:8009/docs/swagger.json
    jimisun
        3
    jimisun  
       2017-08-06 21:03:45 +08:00 via Android
    我一直以为 php 没有 class 只有 java 有,印象中的 php 就和 jsp 一样……全部在网页中……我错了?
    zhuoziyu
        4
    zhuoziyu  
       2017-08-06 21:51:22 +08:00
    @jimisun 大清已经亡了^_^
    voocel
        5
    voocel  
       2017-08-06 21:54:07 +08:00
    @jimisun 活在上古时代吧
    littleylv
        6
    littleylv  
       2017-08-06 21:57:35 +08:00
    @jimisun #3 大清还没亡?
    lxml
        7
    lxml  
       2017-08-06 22:03:10 +08:00
    问个问题,PHP 这种 $xxx 标记变量的语法特色是源自其他语言还是自己独创,看起来好带感。
    k9982874
        8
    k9982874  
       2017-08-06 22:07:52 +08:00 via iPad
    看到 require 我就看不下去了……
    caoyangmin
        9
    caoyangmin  
    OP
       2017-08-06 22:13:46 +08:00 via iPhone
    simaguo
        10
    simaguo  
       2017-08-06 22:20:41 +08:00
    我后清还在呢
    Patrick95
        11
    Patrick95  
       2017-08-06 22:23:21 +08:00
    @k9982874 这有啥看不下去的。。require autoload.php 不很正常吗…
    jhdxr
        12
    jhdxr  
       2017-08-06 22:31:00 +08:00
    @k9982874 完全不用 require 或 include 就意味着你得把所有代码写在一个文件里。。。你确定你更喜欢的是这种方式吗?
    HYSS
        13
    HYSS  
       2017-08-06 22:42:18 +08:00
    @k9982874 我感觉你应该是没有了解什么是自动加载 use 不代表引入文件 前提还得是 require
    jimisun
        14
    jimisun  
       2017-08-06 23:01:22 +08:00 via Android
    @littleylv
    @voocel
    @zhuoziyu
    抱歉,java 新手,没用过 php 只是印象…
    vjnjc
        15
    vjnjc  
       2017-08-07 12:13:34 +08:00
    @jimisun 我也跟你一样,因为前公司流行用 array。。。根本就是 oo 的概念
    KgM4gLtF0shViDH3
        16
    KgM4gLtF0shViDH3  
       2017-08-07 15:40:19 +08:00
    php 的语法看着好难受。。
    jtcba12
        17
    jtcba12  
       2017-08-07 17:05:55 +08:00
    66666
    HYSS
        18
    HYSS  
       2017-08-07 20:25:34 +08:00
    @bestkayle 你用下 php7 的没事了
    lixueliu
        19
    lixueliu  
       2017-08-09 13:59:01 +08:00
    Star
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3621 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 11:04 · PVG 19:04 · LAX 03:04 · JFK 06:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.