使用PHP中的组件实现GraphQL服务器

使用PHP中的组件实现GraphQL服务器

GraphQL是一种用于API的查询语言,它使客户能够准确地询问他们需要的数据并准确地接收这些数据,仅此而已。这样,单个查询就可以获取渲染组件所需的所有数据。

(相比之下,一个REST API必须触发多次往返以从不同端点的多个资源中获取数据,这可能会变得非常慢,尤其是在移动设备上。)

尽管GraphQL(意为 “Graph Query Language”)使用图数据模型来表示数据,但GraphQL服务器不一定需要使用图作为数据结构来解析查询,而是可以使用任何需要的数据结构。该图只是一个心理模型,而不是实际实现。

GraphQL项目在其网站graphql.org上声明了这一点:

Graph是对许多现实世界现象进行建模的强大工具,因为它们类似于我们的自然心理模​​型和对潜在过程的口头描述。使用GraphQL,您可以通过定义模式将业务领域建模为图形;在您的架构中,您定义不同类型的节点以及它们如何相互连接/关联。在客户端,这会创建一个类似于面向对象编程的模式:引用其他类型的类型。在服务器上,由于GraphQL只定义了接口,你可以自由地将它与任何后端(新的或旧的!)一起使用

这是个好消息,因为处理图或树(它们是图的子集)并非易事,并且可能导致解决查询的指数或对数时间复杂度(即解决查询所需的时间可能会增加几个订单)查询的每个新输入的数量级)。

在本文中,我们将描述PoP在PHP GraphQL中的GraphQL服务器的架构设计,它使用组件作为数据结构而不是图。该服务器的名字来源于PoP,它是在PHP中构建组件的库,它是基于该库的。

本文分为5个部分,解释:

  1. 什么是组件
  2. PoP的工作原理
  3. PoP中如何定义组件
  4. 组件如何自然地适用于GraphQL
  5. 使用组件解决GraphQL查询的性能

1.什么是组件

每个网页的布局都可以使用组件来表示。组件只是一组代码(例如HTML、JavaScript和CSS)组合在一起以创建一个自治实体,该实体可以包装其他组件以创建更复杂的结构,并且自身也可以被其他组件包装。每个组件都有一个用途,可以是非常基本的东西,例如链接或按钮,也可以是非常复杂的东西,例如轮播或拖放图像上传器。

通过组件构建站点类似于玩乐。例如,在下图中的网页中,简单的组件(链接、按钮、头像)被组合成更复杂的结构(小工具、部分、侧边栏、菜单)一直到顶部,直到我们获得网页:

所有东西都是一个模块

页面是一个wrapping组件的组件,如方框所示

组件可以在客户端(例如JS库Vue和React,或CSS组件库Bootstrap和Material-UI)和服务器端以任何语言实现。

2. PoP的工作原理

PoP描述了一种基于服务器端组件模型的架构,并通过组件模型库在PHP中实现。

在以下部分中,术语“组件”和“模块”可互换使用。

组件层次结构

所有模块相互wrapping的关系,从最顶层的模块一直到最后一层,称为组件层次结构。这种关系可以通过服务器端的关联数组(key=>property)来表示,其中每个模块将其名称声明为关键属性,并将其内部模块声明为属性"modules"

PHP数组中的数据也可以直接在客户端使用,编码为JSON对象。

组件层次结构如下所示:

$componentHierarchy = [
  'module-level0' => [
    "modules" => [
      'module-level1' => [
        "modules" => [
          'module-level11' => [
            "modules" => [...]
          ],
          'module-level12' => [
            "modules" => [
              'module-level121' => [
                "modules" => [...]
              ]
            ]
          ]
        ]
      ],
      'module-level2' => [
        "modules" => [
          'module-level21' => [
            "modules" => [...]
          ]
        ]
      ]
    ]
  ]
]

 

模块之间的关系以严格的自上而下的方式定义:一个模块wrap了其他模块并且知道它们是谁,但它不知道也不关心哪些模块wraps了他。

例如,在上面的组件层次结构中,模块'module-level1'知道它wrap了模块'module-level11''module-level12',并且,它也知道它wrap了'module-level121';但是模块'module-level11'不关心谁在wrap他,因此不知道'module-level1'.

有了基于组件的结构,我们添加了每个模块所需的实际信息,这些信息分为设置(例如配置值和其他属性)和数据(例如查询的数据库对象的ID和其他属性),并且相应地放在条目modulesettingsmoduledata

$componentHierarchyData = [
  "modulesettings" => [
    'module-level0' => [
      "configuration" => [...],
      ...,
      "modules" => [
        'module-level1' => [
          "configuration" => [...],
          ...,
          "modules" => [
            'module-level11' => [
              ...children...
            ],
            'module-level12' => [
              "configuration" => [...],
              ...,
              "modules" => [
                'module-level121' => [
                  ...children...
                ]
              ]
            ]
          ]
        ],
        'module-level2' => [
          "configuration" => [...],
          ...,
          "modules" => [
            'module-level21' => [
              ...children...
            ]
          ]
        ]
      ]
    ]
  ],
  "moduledata" => [
    'module-level0' => [
      "dbobjectids" => [...],
      ...,
      "modules" => [
        'module-level1' => [
          "dbobjectids" => [...],
          ...,
          "modules" => [
            'module-level11' => [
              ...children...
            ],
            'module-level12' => [
              "dbobjectids" => [...],
              ...,
              "modules" => [
                'module-level121' => [
                  ...children...
                ]
              ]
            ]
          ]
        ],
        'module-level2' => [
          "dbobjectids" => [...],
          ...,
          "modules" => [
            'module-level21' => [
              ...children...
            ]
          ]
        ]
      ]
    ]
  ]
]

接下来,将数据库对象数据添加到组件层次结构中。此信息不是放在每个模块下,而是放在名为databases的共享部分下,以避免在2个或更多不同模块从数据库中获取相同对象时重复信息。

此外,该库以关系的方式表示数据库对象数据,以避免当两个或多个不同的数据库对象与一个共同的对象相关时(例如两个具有相同作者的文章),信息重复。

换句话说,数据库对象数据是标准化的。该结构是一个字典,首先组织在每个对象类型下,然后是对象ID,我们可以从中获取对象属性:

$componentHierarchyData = [
  ...
  "databases" => [
    "dbobject_type" => [
      "dbobject_id" => [
        "property" => ...,
        ...
      ],
      ...
    ],
    ...
  ]
]

例如,下面的对象包含一个带有两个模块的组件层次结构"page"=> "post-feed",其中模块"post-feed"获取博客文章。请注意以下事项:

  • 每个模块都知道哪些是其从属性dbobjectids(ID49博客文章)中查询的对象
  • 每个模块从属性中知道其查询对象的对象类型dbkeys(每个文章的数据都在下面找到"posts",文章的作者数据,对应于在文章属性下给出的ID的作者,在下面"author"找到"users"):
  • 因为数据库对象数据是关系型的,所以属性"author"包含作者对象的ID,而不是直接打印作者数据
$componentHierarchyData = [
  "moduledata" => [
    'page' => [
      "modules" => [
        'post-feed' => [
          "dbobjectids": [4, 9]
        ]
      ]
    ]
  ],
  "modulesettings" => [
    'page' => [
      "modules" => [
        'post-feed' => [
          "dbkeys" => [
            'id' => "posts",
            'author' => "users"
          ]
        ]
      ]
    ]
  ],
  "databases" => [
    'posts' => [
      4 => [
        'title' => "Hello World!",
        'author' => 7
      ],
      9 => [
        'title' => "Everything fine?",
        'author' => 7
      ]
    ],
    'users' => [
      7 => [
        'name' => "Leo"
      ]
    ]
  ]
]

数据加载

当模块显示来自数据库对象的属性时,模块可能不知道或不关心它是什么对象;它所关心的只是定义加载对象的哪些属性是必需的。

例如,考虑下图:一个模块从数据库中加载一个对象(在本例中为单个文章),然后其后代模块将显示该对象的某些属性,例如"title""content"

每隔一段时间加载数据 一些模块加载数据库对象,其他模块加载属性

因此,沿着组件层次结构,“数据加载”模块将负责加载查询的对象(在这种情况下是加载单个文章的模块),其后代模块将定义需要来自DB对象的哪些属性("title""content", 在这种情况下)。

可以通过遍历组件层次结构来获取DB对象所需的所有属性:从数据加载模块开始,PoP一直迭代其所有后代模块,直到到达新的数据加载模块,或者直到树的末尾;在每一层,它获取所有需要的属性,然后将所有属性合并在一起并从数据库中查询它们,所有这些都只需要一次。

因为数据库对象数据是以关系方式检索的,那么我们也可以在数据库对象本身之间的关系中应用这种策略。

考虑下图: 从对象类型"post"开始,向下移动组件层次结构,我们需要将数据库对象类型转换为"user""comment",分别对应于文章的作者和每个文章的评论,然后,对于每个评论,它必须再次更改对象类型"user"以对应评论的作者。从数据库对象转移到关系对象就是我所说的“切换域”。

切换到新域后,从组件层次结构的该级别向下,所有需要的属性都将受制于新域:属性"name"取自代表文章作者的"user"对象,"content"取自代表文章每条评论的"comment"对象,然后"name"取自代表每条评论作者的"user"对象:

以关系间隔加载数据 将数据库对象从一个域更改为另一个域

遍历组件层次结构,PoP知道它何时切换域并适当地获取关系对象数据。

3. PoP中如何定义组件

模块属性(配置值、要获取的数据库数据等)和子模块是通过ModuleProcessor对象逐模块定义的,PoP从处理所有相关模块的所有ModuleProcessor创建组件层次结构。

类似于React应用程序(我们必须指出在哪个组件上渲染<div id="root"></div>),PoP中的组件模型必须有一个入口模块。从它开始,PoP将遍历组件层次结构中的所有模块,从相应的ModuleProcessor中获取每个模块的属性,并创建包含所有模块所有属性的嵌套关联数组。

当一个组件定义一个后代组件时,它通过一个包含2个部分的数组来引用它:

  1. PHP类
  2. 组件名称

这是因为组件通常共享属性。例如,组件POST_THUMBNAIL_LARGEPOST_THUMBNAIL_SMALL将共享大多数属性,但缩略图的大小除外。然后,将所有相似的组件分组到同一个PHP类下,并使用switch语句来识别请求的模块并返回相应的属性,这是有意义的。

ModuleProcessor要放置在不同页面上的文章小工具组件如下所示:

class PostWidgetModuleProcessor extends AbstractModuleProcessor {

  const POST_WIDGET_HOMEPAGE = 'post-widget-homepage';
  const POST_WIDGET_AUTHORPAGE = 'post-widget-authorpage';

  function getSubmodulesToProcess() {
  
    return [
      self::POST_WIDGET_HOMEPAGE,
      self::POST_WIDGET_AUTHORPAGE,
    ];
  }

  function getSubmodules($module): array 
  {
    $ret = [];

    switch ($module[1]) {      
      case self::POST_WIDGET_HOMEPAGE:
      case self::POST_WIDGET_AUTHORPAGE:
        $ret[] = [
          UserLayoutModuleProcessor::class,
          UserLayoutModuleProcessor::POST_THUMB
        ];
        $ret[] = [
          UserLayoutModuleProcessor::class,
          UserLayoutModuleProcessor::POST_TITLE
        ];
        break;
    }
    switch ($module[1]) {      
      case self::POST_WIDGET_HOMEPAGE:
        $ret[] = [
          UserLayoutModuleProcessor::class,
          UserLayoutModuleProcessor::POST_DATE
        ];
        break;
    }

    return $ret;
  }

  function getImmutableConfiguration($module, &$props) 
  {
    $ret = [];

    switch ($module[1]) {
      case self::POST_WIDGET_HOMEPAGE:        
        $ret['description'] = __('Latest posts', 'my-domain');
        $ret['showmore'] = $this->getProp($module, $props, 'showmore');
        $ret['class'] = $this->getProp($module, $props, 'class');
        break;

      case self::POST_WIDGET_AUTHORPAGE:        
        $ret['description'] = __('Latest posts by the author', 'my-domain');
        $ret['showmore'] = false;
        $ret['class'] = 'text-center';
        break;
    }

    return $ret;
  }
  
  function initModelProps($module, &$props) 
  {
    switch ($module[1]) {
      case self::POST_WIDGET_HOMEPAGE:
        $this->setProp($module, $props, 'showmore', false);
        $this->appendProp($module, $props, 'class', 'text-center');
        break;
    }

    parent::initModelProps($module, $props);
  }
  // ...
}

创建可重用组件是通过创建抽象的ModuleProcessor类来完成的,这些类定义了必须由一些实例化类实现的占位符函数:

abstract class PostWidgetLayoutAbstractModuleProcessor extends AbstractModuleProcessor
{
  function getSubmodules($module): array
  {  
    $ret = [
      $this->getContentModule($module),
    ];

    if ($thumbnail_module = $this->getThumbnailModule($module)) 
    {
      $ret[] = $thumbnail_module;
    }

    if ($aftercontent_modules = $this->getAfterContentModules($module)) 
    {
      $ret = array_merge(
        $ret,
        $aftercontent_modules
      );
    }

    return $ret;
  }

  abstract protected function getContentModule($module): array;

  protected function getThumbnailModule($module): ?array 
  {
    // Default value (overridable)
    return [self::class, self::THUMBNAIL_LAYOUT];
  }

  protected function getAfterContentModules($module): array 
  {
    return [];
  }

  function getImmutableConfiguration($module, &$props): array 
  {
    return [
      'description' => $this->getDescription(),
    ];
  }

  protected function getDescription($module): string
  {
    return '';
  }
}

然后,自定义ModuleProcessor类可以扩展抽象类,并定义自己的属性:

class PostLayoutModuleProcessor extends AbstractPostLayoutModuleProcessor {

  const POST_CONTENT = 'post-content'
  const POST_EXCERPT = 'post-excerpt'
  const POST_THUMBNAIL_LARGE = 'post-thumbnail-large'
  const POST_THUMBNAIL_MEDIUM = 'post-thumbnail-medium'
  const POST_SHARE = 'post-share'

  function getSubmodulesToProcess() {
  
    return [
      self::POST_CONTENT,
      self::POST_EXCERPT,
      self::POST_THUMBNAIL_LARGE,
      self::POST_THUMBNAIL_MEDIUM,
      self::POST_SHARE,
    ];
  }

}

class PostWidgetLayoutModuleProcessor extends AbstractPostWidgetLayoutModuleProcessor
{
  protected function getContentModule($module): ?array 
  {
    switch ($module[1]) 
    {
      case self::POST_WIDGET_HOMEPAGE_LARGE:
        return [
          PostLayoutModuleProcessor::class,
          PostLayoutModuleProcessor::POST_CONTENT
        ];

      case self::POST_WIDGET_HOMEPAGE_MEDIUM:
      case self::POST_WIDGET_HOMEPAGE_SMALL:
        return [
          PostLayoutModuleProcessor::class,
          PostLayoutModuleProcessor::POST_EXCERPT
        ];
    }

    return parent::getContentModule($module);
  }

  protected function getThumbnailModule($module): ?array 
  {
    switch ($module[1]) 
    {
      case self::POST_WIDGET_HOMEPAGE_LARGE:
        return [
          PostLayoutModuleProcessor::class,
          PostLayoutModuleProcessor::POST_THUMBNAIL_LARGE
        ];

      case self::POST_WIDGET_HOMEPAGE_MEDIUM:
        return [
          PostLayoutModuleProcessor::class,
          PostLayoutModuleProcessor::POST_THUMBNAIL_MEDIUM
        ];
    }

    return parent::getThumbnailModule($module);
  }

  protected function getAfterContentModules($module): array 
  {
    $ret = [];

    switch ($module[1]) 
    {
      case self::POST_WIDGET_HOMEPAGE_LARGE:
        $ret[] = [
          PostLayoutModuleProcessor::class,
          PostLayoutModuleProcessor::POST_SHARE
        ];
        break
    }

    return $ret;
  }

  protected function getDescription($module): string
  {
    return __('These are my blog posts', 'my-domain');
  }
}

4. 组件如何天生适合GraphQL

组件模型可以自然地映射树形GraphQL查询,使其成为实现GraphQL服务器的理想架构。

PoP的GraphQL实现了将GraphQL查询转换为其相应组件层次结构所需的ModuleProcessor类,并使用PoP数据加载引擎进行解析。

这就是该解决方案起作用的原因和方式。

将客户端组件映射到GraphQL查询

GraphQL查询可以使用PoP的组件层次结构来表示,其中每个对象类型代表一个组件,从一个对象类型到另一个对象类型的每个关系字段都代表一个包装另一个组件的组件。

让我们通过一个例子来看看是怎么回事。假设我们要构建以下“热门导演”小工具:

热门导演小组件

精选导演小工具

使用Vue或React(或任何其他基于组件的库),我们将首先识别组件。在这种情况下,我们将有一个外部组件<FeaturedDirector>(红色),它wrap一个组件<Film>(蓝色),它本身wrap一个组件<Actor>(绿色):

热门控制器小部件组件 识别小工具中的组件

伪代码如下所示:

<!-- Component: <FeaturedDirector> -->
<div>
  Country: {country}
  {foreach films as film}
    <Film film={film} />
  {/foreach}
</div>

<!-- Component: <Film> -->
<div>
  Title: {title}
  Pic: {thumbnail}
  {foreach actors as actor}
    <Actor actor={actor} />
  {/foreach}
</div>

<!-- Component: <Actor> -->
<div>
  Name: {name}
  Photo: {avatar}
</div>

然后我们确定每个组件需要哪些数据。对于<FeaturedDirector>,我们需要name,avatarcountry。对于<Film>我们需要thumbnailtitle。对于<Actor>我们需要nameavatar

热门导演小部件数据 识别每个组件的数据属性

我们构建了GraphQL查询来获取所需的数据:

query {
  featuredDirector {
    name
    country
    avatar
    films {
      title
      thumbnail
      actors {
        name
        avatar
      }
    }
  }
}

可以理解,组件层次结构的形状和GraphQL查询之间存在直接关系。事实上,一个GraphQL查询甚至可以被认为是一个组件层次结构的表示。

使用服务器端组件解析GraphQL查询

由于GraphQL查询具有相同的组件层次结构,PoP 将查询转换为其等效的组件层次结构,使用其为组件获取数据的方法对其进行解析,最后重新创建查询的形状以在响应中发送数据.

让我们看看这是如何工作的。

为了处理数据,PoP将GraphQL类型转换为组件:<FeaturedDirector>=> Director<Film>=> Film<Actor>=> Actor,并使用使用它们在查询中出现的顺序,PoP创建了一个具有相同元素的虚拟组件层次结构:根组件Director,wrap组件Film,wrap组件Actor

从现在开始,谈论GraphQL类型或PoP组件不再重要。

为了加载它们的数据,PoP在“迭代”中处理它们,在自己的迭代中检索每种类型的对象数据,如下所示:

处理迭代中的类型

处理迭代中的类型

PoP的数据加载引擎实现了以下伪算法来加载数据:

准备:

  1. 有一个空队列存储必须从数据库中获取的对象的ID列表,按类型组织(每个条目将是[type => list of IDs]:)
  2. 检索特色导演对象的ID,并将其放在队列中的Director类型下

循环直到队列中没有更多条目:

  1. 从队列中获取第一个条目:ID的类型和列表(例如:Director[2]),并将此条目从队列中移除
  2. 对数据库执行单个查询以检索具有这些ID的该类型的所有对象
  3. 如果该类型具有关系字段(例如:Director类型具有films类型的关系字段Film),则从当前迭代中检索到的所有对象中收集这些字段的所有ID(例如:来自Director类型的所有对象的films中的所有ID,并将队列中对应类型下的这些ID(例如: Film类型下的ID [3, 8])。

在迭代结束时,我们将加载所有类型的所有对象数据,如下所示:

处理迭代中的类型

处理迭代中的类型

请注意,在队列中处理该类型之前,如何收集该类型的所有ID。例如,如果我们向类型Director添加一个关系字段preferredActors,这些ID将添加到类型Actor下的队列中,并将与类型Film中的字段actors的ID一起处理:

 

热门导演加载数据扩展

处理迭代中的类型

但是,如果一个类型已被处理,然后我们需要从该类型加载更多数据,那么它就是该类型的新迭代。例如,将关系字段preferredDirector添加到Author类型中,将使类型Director再次添加到队列中:

热门导演加载数据重复

迭代重复的类型

还要注意,这里我们可以使用缓存机制:在类型Director的第二次迭代中,不会再次检索ID为2的对象,因为它在第一次迭代中已经检索到,因此可以从缓存中获取。

现在我们已经获取了所有对象数据,我们需要将其塑造成预期的响应,镜像GraphQL查询。目前,数据被组织为关系数据库:

Director类型表:

ID NAME COUNTRY AVATAR FILMS
2 George Lucas USA george-lucas.jpg [3, 8]

Film类型表:

ID TITLE THUMBNAIL ACTORS
3 The Phantom Menace episode-1.jpg [4, 6]
8 Attack of the Clones episode-2.jpg [6, 7]

Actor类型表:

ID NAME AVATAR
4 Ewan McGregor mcgregor.jpg
6 Nathalie Portman portman.jpg
7 Hayden Christensen christensen.jpg

在这个阶段,PoP将所有数据组织为表格,以及每种类型如何相互关联(即Director 通过films字段引用FilmFilm 通过actors引用Actor))。然后,通过从根迭代组件层次结构、导航关系并从关系表中检索相应的对象,PoP将从GraphQL查询中生成树形:

树形响应

树形响应

最后,将数据打印到输出中会产生与GraphQ查询形状相同的响应:

{
  data: {
    featuredDirector: {
      name: "George Lucas",
      country: "USA",
      avatar: "george-lucas.jpg",
      films: [
        { 
          title: "Star Wars: Episode I",
          thumbnail: "episode-1.jpg",
          actors: [
            {
              name: "Ewan McGregor",
              avatar: "mcgregor.jpg",
            },
            {
              name: "Natalie Portman",
              avatar: "portman.jpg",
            }
          ]
        },
        { 
          title: "Star Wars: Episode II",
          thumbnail: "episode-2.jpg",
          actors: [
            {
              name: "Natalie Portman",
              avatar: "portman.jpg",
            },
            {
              name: "Hayden Christensen",
              avatar: "christensen.jpg",
            }
          ]
        }
      ]
    }
  }
}

5.使用组件解析GraphQL查询的性能分析

让我们分析数据加载算法的大O表示法,以了解对数据库执行的查询数量如何随着输入数量的增长而增长,以确保该解决方案具有高性能。

PoP的数据加载引擎在与每种类型对应的迭代中加载数据。当它开始迭代时,它已经拥有所有要获取的对象的所有 ID 的列表,因此它可以执行 1 个单一查询来获取相应对象的所有数据。然后,对数据库的查询数量将随着查询中涉及的类型数量线性增长。换句话说,时间复杂度是O(n),其中n是查询中的类型数量(但是,如果一个类型被多次迭代,那么必须被多次添加到n)。

这个解决方案的性能非常好,肯定超过了处理图所期望的指数复杂度,或者处理树所期望的对数复杂度。

结论

GraphQL服务器不需要使用图形来表示数据。在本文中,我们探索了PoP描述的架构,并通过PoP由GraphQL实现,它基于组件并根据类型在迭代中加载数据。

通过这种方法,服务器可以解决具有线性时间复杂度的GraphQL查询,这比使用图或树所期望的指数或对数时间复杂度要好。

评论留言