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

自学咖网努力为各位打造免费分享知识与教程网站

GraphQL是一种针对API的查询语言,能够让客户准确的提出所需的数据并准确的接收,仅此而已。这样,一个查询就可以获得呈现组件所需的所有数据。

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

虽然GraphQL(意为“图查询语言”)使用图数据模型来表示数据,但是GraphQL server并不一定需要使用图作为数据结构来解析查询,而是可以使用任何需要的数据结构。该图只是一个心理模型,而不是实际的实现。

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

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

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

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

本文分为五个部分,分别阐述:

什么是组件?

PoP的工作原理

如何在PoP中定义组件

组件如何自然地适用于GraphQL?

使用组件解决GraphQL查询的性能

1.什么是组件?

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

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

自学咖网努力为各位打造免费分享知识与教程网站

该页面是包装组件的一个组件,如框中所示。

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

2.POP的工作原理

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

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

组件层次结构

所有模块之间的关系,从顶层模块到最后一层模块,称为组件层次结构。这种关系可以用服务器端关联数组(key=>property)来表示,其中每个模块都将其名称声明为key属性,将其内部模块声明为属性“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” => […] ] ] ] ] ]]

模块之间的关系是从上到下严格定义的:一个模块包装了其他模块,知道它们是谁,但不知道也不关心是哪些模块包装了它。

例如,在上面的组件层次结构中,模块’ module-level1 ‘知道它包装了模块’ module-level11 ‘和’ module-level12 ‘,它也知道它包装了’ module-level 121 ‘;;但是模块’ module-level11 ‘不在乎谁包装他,所以它不知道’ module-level1 ‘。

使用基于组件的结构,我们添加每个模块所需的实际信息,这些信息分为设置(如配置值和其他属性)和数据(如被查询数据库对象的ID和其他属性),并相应地将它们放入条目modulesettings和moduledata中:

$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的共享部分下,以避免当两个或多个不同的模块从数据库中获取相同的对象时出现信息重复。

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

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

$componentHierarchyData = [ … “databases” => [ “dbobject_type” => [ “dbobject_id” => [ “property” => …, … ], … ], … ]]

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

每个模块都知道它从属性db objectid(ID4和9 blog posts)中查询哪些对象。

每个模块从属性中知道其查询对象的对象类型dbkeys(每篇文章的数据在下面找到为“posts”,文章的作者数据,对应于在文章的属性下给出的ID的作者,而“users”在下面找到为“author”):

因为数据库对象数据是关系型的,所以属性“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” ] ] ]]

数据加载

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

例如,考虑下图:一个模块从数据库中加载一个对象(在本例中是一篇文章),然后它的后代模块会显示该对象的一些属性,比如“标题”和“内容”:

自学咖网努力为各位打造免费分享知识与教程网站 一些模块加载数据库对象,其他模块加载属性一些模块加载数据库对象,另一些模块加载属性。

因此,沿着组件层次结构,“数据加载”模块将负责加载被查询的对象(在本例中是加载一篇文章的模块),它的后代将定义需要DB对象中的哪些属性(在本例中是“标题”和“内容”)。

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

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

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

在切换到新域之后,从组件层次结构中的这一级别开始,所有必需的属性都将服从于新域:属性“name”取自代表文章作者的“user”对象,“content”取自代表文章的每个评论的“comment”对象,然后“name”取自代表每个评论作者的“user”对象:

自学咖网努力为各位打造免费分享知识与教程网站 将数据库对象从一个域更改为另一个域将数据库对象从一个域更改到另一个域。

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

3.如何在POP中定义组件

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

与React应用程序类似(我们必须指明在哪个组件上呈现),PoP中的组件模型必须有一个入口模块。从它开始,PoP将遍历组件层次结构中的所有模块,从相应的ModuleProcessor中获取每个模块的属性,并创建一个包含所有模块所有属性的嵌套关联数组。

当一个组件定义一个后代组件时,它通过一个由两部分组成的数组来引用它:

php类

组件名称

这是因为组件通常共享属性。例如,组件POST_THUMBNAIL_LARGE和POST_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(或任何其他基于组件的库),我们将首先识别组件。在这种情况下,我们将有一个外部组件(红色),它包装一个组件(蓝色)和包装一个组件本身(绿色):

自学咖网努力为各位打造免费分享知识与教程网站 识别小工具中的组件识别小工具中的组件。

伪代码如下:

Country: {country} {foreach films as film} {/foreach} Title: {title} Pic: {thumbnail} {foreach actors as actor} {/foreach} Name: {name} Photo: {avatar}

然后我们确定每个组件需要什么数据。是的,我们需要名字,头像和国家。对我们来说,我们需要缩略图和标题。我们需要名字和头像:

自学咖网努力为各位打造免费分享知识与教程网站 识别每个组件的数据属性确定每个组件的数据属性。

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

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

可以理解,组件层次结构的形状和GraphQL查询有直接的关系。事实上,GraphQL查询甚至可以被视为组件层次结构的一种表示。

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

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

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

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

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

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

自学咖网努力为各位打造免费分享知识与教程网站

在迭代中处理类型

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

准备:

有一个空队列存储必须从数据库获得的对象的ID列表,按类型组织(每个条目将是[type = > list of ID]:)

检索特色控制器对象的ID,并将其放入控制器类型下的队列中。

直到循环队列中不再有条目:

从队列中获取第一个条目ID的类型和列表(例如:Director和[2]),并从队列中删除该条目。

对数据库执行一个查询,检索具有这些id的这种类型的所有对象。

如果该类型具有关系字段(例如,导演类型具有电影类型的关系字段),这些字段的所有ID将从当前迭代中检索的所有对象中收集(例如,来自导演类型的所有对象的电影中的所有ID),并且相应类型下的这些ID将被排队(例如,电影类型[3,8]下的ID)。

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

自学咖网努力为各位打造免费分享知识与教程网站

在迭代中处理类型

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

自学咖网努力为各位打造免费分享知识与教程网站

在迭代中处理类型

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

自学咖网努力为各位打造免费分享知识与教程网站

迭代类型

还要注意,这里我们可以使用缓存机制:在Type Director的第二次迭代中,不会再次检索ID为2的对象,因为在第一次迭代中已经检索过了,所以可以从缓存中检索。

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

控制器类型表:

IDNAMECOUNTRYAVATARFILMS2George LucasUSAgeorge-lucas.jpg[3, 8]

电影类型表:

IDTITLETHUMBNAILACTORS3The Phantom Menaceepisode-1.jpg[4, 6]8Attack of the Clonesepisode-2.jpg[6, 7]

执行元类型表:

IDNAMEAVATAR4Ewan McGregormcgregor.jpg6Nathalie Portmanportman.jpg7Hayden Christensenchristensen.jpg

在这个阶段,PoP将所有的数据组织成表格,以及每种类型之间是如何相互关联的(即导演通过电影场指电影,电影通过演员指演员)。然后,通过从根开始迭代组件层次结构,导航关系并从关系表中检索相应的对象,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的列表,因此它可以执行一个查询来获取相应对象的所有数据。然后,对数据库的查询数量将随着查询中涉及的类型数量而线性增加。换句话说,时间复杂度为O(n),其中n是查询中类型的数量(但是,如果一个类型迭代多次,则必须多次加到n)。

这个方案的性能非常好,绝对超过了处理图预期的指数复杂度或者处理树预期的对数复杂度。

结论

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

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

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » 用PHP中的组件实现GraphQL服务器