getpop/field-query

基于组件模型的PoP组件,组件化架构以此为基础

0.8.9 2021-11-30 07:11 UTC

README

通过URL参数查询GraphQL的语法,这使GraphQL API能够在服务器上缓存。

安装

通过Composer

composer require getpop/field-query

开发

源代码托管在PoP单仓库中,位于Engine/packages/field-query

使用方法

初始化组件

\PoP\Root\App::stockAndInitializeModuleClasses([([
    \PoP\FieldQuery\Module::class,
]);

使用它

use PoP\FieldQuery\Facades\Query\FieldQueryInterpreterFacade;

$fieldQueryInterpreter = FieldQueryInterpreterFacade::getInstance();

// To create a field from its elements
$field = /* @todo Re-do this code! Left undone */ new Field($fieldName, $fieldArgs, $fieldAlias, $skipOutputIfNull, $fieldDirectives);

// To retrieve the elements from a field
$fieldName = $fieldQueryInterpreter->getFieldName($field);
$fieldArgs = $fieldQueryInterpreter->getFieldArgs($field);

// All other functions listed in FieldQueryInterpreterInterface
// ...

原因

GraphQL查询通常跨越多行,它通过请求正文提供,而不是通过URL参数提供。因此,在服务器上难以缓存GraphQL查询的结果。为了支持GraphQL的服务器端缓存,我们可以尝试通过URL提供查询,以便使用基于URL作为其唯一ID的页面缓存标准机制。

本项目中描述和实现的语法是对GraphQL语法的重新构想,支持所有相同元素(字段参数、变量、别名、片段、指令等),但设计得易于编写、易于阅读和理解,以便在单行中传递,从而可以作为URL参数传递。

能够将查询作为URL参数传递具有其他几个优点

  • 它消除了客户端库将GraphQL查询转换为所需格式(如Relay)的需求,从而提高了性能并减少了维护的代码量
  • GraphQL API的消耗变得更简单(与REST相同),并且避免了依赖于特殊的客户端(如GraphiQL)来可视化查询结果

谁在使用它

PoP原生使用此语法:在应用程序本身中的每个组件中加载数据(如组件模型所做的那样),以及通过URL参数query从API加载数据(如PoP API所做的那样)。

GraphQL服务器可以实现此语法以支持基于URI的服务器端缓存。为此,必须将查询从此语法转换为相应的GraphQL语法,然后将转换后的查询传递给GraphQL引擎。

语法

类似于GraphQL的查询描述了一组“字段”,其中每个字段可以包含以下元素

  • 字段名称:要检索的数据
  • 字段参数:如何过滤数据或格式化结果
  • 字段别名:在响应中命名字段的方式
  • 字段指令:更改执行操作的行为

与GraphQL不同,字段还可以包含以下元素

  • 字段参数中的属性名称可能是可选的:简化向字段传递参数的过程
  • 书签:保持从已定义的字段加载数据
  • 运算符和助手:标准运算(如andorifisNull等)和环境变量访问助手(以及其他用例)可以作为字段存在
  • 可组合字段:字段的响应可以作为另一个字段的输入,通过其参数或字段指令实现
  • 如果为空则跳过输出:当字段值为空时忽略输出
  • 可组合指令:指令可以修改其他嵌套指令的行为
  • 表达式:在指令间传递值

从组成元素中,只有字段名是必须的;所有其他都是可选的。字段的组成顺序如下

  1. 字段名
  2. 参数:(...)
  3. 书签:[...]
  4. 别名:@...(如果书签也存在,它被放在里面)
  5. 如果为空则跳过输出:?
  6. 指令:指令名称和参数:<directiveName(...)>

字段看起来是这样的

fieldName(fieldArgs)[@alias]?<fieldDirective(directiveArgs)>

为了更清楚地可视化,查询可以分成几行

fieldName(
  fieldArgs
)[@alias]?<
  fieldDirective(
    directiveArgs
  )
>

注意
Firefox已经处理了多行查询:将其复制到地址栏中工作得非常好。要尝试它,复制https://newapi.getpop.org/api/graphql/,然后按每个示例将查询复制到Firefox的地址栏中,voilà,查询应该会执行。

Chrome和Safari表现不佳:它们需要在将查询粘贴到地址栏之前删除所有空格和换行符。

结论:使用Firefox!

要在同一查询中检索多个字段,请使用,将它们连接起来

fieldName1@alias1,
fieldName2(
  fieldArgs2
)[@alias2]?<
  fieldDirective2
>

从节点检索属性

使用|分隔要获取的属性。

In GraphQL:

query {
  id
  fullSchema
}

In PoP (在浏览器中查看查询)

/?query=
  id|
  fullSchema

检索嵌套属性

要获取关系或嵌套数据,请使用.描述到属性的路径。

In GraphQL:

query {
  posts {
    author {
      id
    }
  }
}

In PoP (在浏览器中查看查询)

/?query=
  posts.
    author.
      id

当我们到达节点时,我们可以使用|来获取多个属性

In GraphQL:

query {
  posts {
    author {
      id
      name
      url
    }
  }
}

In PoP (在浏览器中查看查询)

/?query=
  posts.
    author.
      id|
      name|
      url

符号.|可以混合使用,以获取路径上的属性

In GraphQL:

query {
  posts {
    id
    title
    author {
      id
      name
      url
    }
  }
}

In PoP (在浏览器中查看查询)

/?query=
  posts.
    id|
    title|
    author.
      id|
      name|
      url

追加字段

通过使用,将多个字段连接起来以组合它们。

In GraphQL:

query {
  posts {
    author {
      id
      name
      url
    }
    comments {
      id
      content
    }
  }
}

In PoP (在浏览器中查看查询)

/?query=
  posts.
    author.
      id|
      name|
      url,
  posts.
    comments.
      id|
      content

按严格执行顺序追加字段

这是一个语法+功能特性。通过使用;将多个字段连接起来,告诉数据加载引擎在解决左边的所有字段之后再解决;右边的字段。

在GraphQL服务器中,之前的查询解析为这个(使用self来延迟字段解析的时间)

In GraphQL:

query {
  posts {
    author {
      id
      name
      url
    }
    comments {
      id
      content
    }
  }
}

In PoP (在浏览器中查看查询)

/?query=
  posts.
    author.
      id|
      name|
      url;
      
  posts.
    comments.
      id|
      content

在GraphQL服务器中,之前的查询解析为这个(使用self来延迟字段解析的时间)

/?query=
  posts.
    author.
      id|
      name|
      url,
  self.
    self.
      posts.
        comments.
        id|
        content

字段参数

字段参数是一个属性数组,用于过滤结果(当应用于路径上的属性时)或修改输出(当应用于叶节点上的属性时)。这些被括号()包围,使用:定义属性名称和值(成为name:value),并且使用,分隔。

值不需要用引号"..."括起来。

在GraphQL中过滤结果 (重要)

query {
  posts(filter:{ search: "template" }) {
    id
    title
    date
  }
}

在PoP中过滤结果 (重要) (在浏览器中查看查询)

/?query=
  posts(filter: { search: template }).
    id|
    title|
    date

在GraphQL中格式化输出 (重要)

query {
  posts {
    id
    title
    dateStr(format: "d/m/Y")
  }
}

在PoP中格式化输出 (重要) (在浏览器中查看查询)

/?query=
  posts.
    id|
    title|
    dateStr(format:d/m/Y)

字段参数中可选的属性名

如果可以从模式(例如,可以从模式定义中属性的位置推断出名称)中推断出参数名称,则可以忽略定义参数名称。

在PoP中 (在浏览器中查看查询)

/?query=
  posts.
    id|
    title|
    dateStr(d/m/Y)

别名

别名定义了输出字段的名称。别名名称必须以@开头

In GraphQL:

query {
  posts {
    id
    title
    formattedDate: dateStr(format: "d/m/Y")
  }
}

在PoP中 (在浏览器中查看查询)

/?query=
  posts.
    id|
    title|
    dateStr(d/m/Y)@formattedDate

请注意,与GraphQL不同,别名是可选的。在GraphQL中,因为字段参数不是响应中的字段的一部分,所以在查询具有不同参数的相同字段时,必须使用别名来区分它们。在PoP中,然而,字段参数是响应中的字段的一部分,这已经区分了字段。

In GraphQL:

query {
  posts {
    id
    title
    date: date
    formattedDate: dateStr(format: "d/m/Y")
  }
}

在PoP中 (在浏览器中查看查询)

/?query=posts.
  id|
  title|
  dateStr|
  dateStr(d/m/Y)

书签

在GraphQL中遍历字段路径时,从不同的子分支加载数据具有视觉吸引力

In GraphQL:

query {
  users {
    posts {
      author {
        id
        name
      }
      comments {
        id
        content
      }
    }
  }
}

然而,在PoP中,查询可能会变得非常冗长,因为在用,组合字段时,它会从根开始再次遍历路径。

在PoP中 (在浏览器中查看查询)

/?query=
  users.
    posts.
      author.
        id|
        name,
  users.
    posts.
      comments.
        id|
        content

书签通过创建到路径的快捷方式来帮助解决这个问题,这样我们就可以方便地继续从该点加载数据。要定义书签,在遍历路径时,其名称用[...]括起来,要使用它,其名称也用[...]括起来。

在PoP中 (在浏览器中查看查询)

/?query=
  users.
    posts[userposts].
      author.
        id|
        name,
    [userposts].
      comments.
        id|
        content

带别名的书签

当我们需要定义到路径的书签和输出字段的别名时,这两个必须组合在一起:带有@前缀的别名放置在书签分隔符[...]内。

在PoP中 (在浏览器中查看查询)

/?query=
  users.
    posts[@userposts].
      author.
        id|
        name,
    [userposts].
      comments.id|
      content

变量

变量可以用于向字段参数输入值。在GraphQL中,解析到的值在体中定义(在查询的单独字典中),在PoP中,这些值从请求($_GET$_POST)中检索。

变量名称必须以$开头,其值在请求中可以定义在变量名称下,或者在variables条目下,然后是变量名称。

API调用 在GraphQL中

{
  "query":"query ($format: String) {
    posts {
      id
      title
      dateStr(format: $format)
    }
  }",
  "variables":"{
    \"format\":\"d/m/Y\"
  }"
}

在PoP中 (在浏览器中查看:查询1查询2

1. /?
  format=d/m/Y&
  query=
    posts.
      id|
      title|
      dateStr($format)

2. /?
  variables[format]=d/m/Y&
  query=
    posts.
      id|
      title|
      dateStr($format)

片段

片段允许重用查询部分。类似于变量,它们的解析定义在请求中($_GET$_POST)。与 GraphQL 不同,片段不需要指明它操作的是哪种模式类型。

片段名称必须以 -- 开头,它们解析的查询可以定义在片段名称下方,或者在 fragments 条目下方,然后是片段名称。

虽然片段可以包含 |(用于拆分字段),但不能包含 ;(用于拆分操作)或 ,(用于拆分查询),以避免混淆(因为这些是从查询的根而不是片段计算出来的)。

In GraphQL:

query {
  users {
    ...userData
    posts {
      comments {
        author {
          ...userData
        }
      }
    }
  }
}

fragment userData on User {
  id
  name
  url
}

在 PoP 中(在浏览器中查看:查询 1查询 2

1. /?
userData=
  id|
  name|
  url&
query=
  users.
    --userData|
    posts.
      comments.
        author.
          --userData

2. /?
fragments[userData]=
  id|
  name|
  url&
query=
  users.
    --userData|
    posts.
      comments.
        author.
          --userData

指令

指令可以修改执行获取数据操作的方式。每个字段都可以接受多个指令,每个指令都有自己的参数来定制其行为。指令集用 <...> 包围,其中的指令必须用 , 分隔,它们的参数遵循与字段参数相同的语法:它们被 (...) 包围,其 name:value 对用 , 分隔。

In GraphQL:

query {
  posts {
    id
    title
    featuredImage @include(if: $addFeaturedImage) {
      id
      src
    }
  }
}

在 PoP 中(在浏览器中查看:查询 1查询 2查询 3查询 4

1. /?
include=true&
query=
  posts.
    id|
    title|
    featuredImage<
      include(if:$include)
    >.
      id|
      src

2. /?
include=false&
query=
  posts.
    id|
    title|
    featuredImage<
      include(if:$include)
    >.
      id|
      src

3. /?
skip=true&
query=
  posts.
    id|
    title|
    featuredImage<
      skip(if:$skip)
    >.
      id|
      src

4. /?
skip=false&
query=
  posts.
    id|
    title|
    featuredImage<
      skip(if:$skip)
    >.
      id|
      src

操作符和辅助函数

标准操作,如 andorifisNullcontainssprintf 以及许多其他操作,可以作为字段在 API 中提供。然后,操作符名称代表字段名称,它可以接受相同格式的所有其他元素(参数、别名等)。

要作为数组传递参数值,我们将其括在 [] 中,并用 , 分隔其项。格式可以是只是 value(数字数组)或 key:value(索引数组)。

在 PoP 中(在浏览器中查看:查询 1查询 2查询 3查询 4查询 5查询 6查询 7查询 8查询 9

1. /?query=not(true)

2. /?query=or([1, 0])

3. /?query=and([1, 0])

4. /?query=if(true, Show this text, Hide this text)

5. /?query=equals(first text, second text)

6. /?query=isNull(),isNull(something)

7. /?query=sprintf(
  %s is %s, [
    PoP API, 
    cool
  ])

8. /?query=echo([
  name: PoP API,
  status: cool
])

9. /?query=arrayValues([
  name: PoP API,
  status: cool
])

同样,辅助函数可以提供所需的所有信息,也作为字段行为。例如,辅助函数 context 提供系统状态中的值,辅助函数 var 可以从系统状态中检索任何特定变量。

在PoP中(在浏览器中查看:查询1查询2

1. /?query=context

2. /?query=
  var(route)|
  var(target)@target|
  var(datastructure)

可组合字段

当操作符能够接收字段输出作为其输入时,从拥有操作符中获得的真正好处。由于操作符本身就是一个字段,这可以概括为“可组合字段”:将任何字段的输出作为另一个字段的参数值传递。

为了区分字段输入是字符串还是字段名,字段必须具有字段参数括号 (...)(如果没有参数,则简单地 ())。例如,"id" 表示字符串 "id",而 "id()" 表示执行并传递字段 "id" 的结果。

在PoP中(在浏览器中查看:查询1查询2查询3查询4查询5查询6

1. /?query=
  posts.
    hasComments|
    not(hasComments())

2. /?query=
  posts.
    hasComments|
    hasFeaturedImage|
    or([
      hasComments(),
      hasFeaturedImage()
    ])

3. /?query=
  var(fetching-site)|
  posts.
    hasFeaturedImage|
    and([
      hasFeaturedImage(),
      var(fetching-site)
    ])

4. /?query=
  posts.
  if (
    hasComments(),
    sprintf(
      Post with title '%s' has %s comments, [
      title(), 
      commentCount()
    ]),
    sprintf(
      Post with ID %s was created on %s, [
      id(),
      dateStr(d/m/Y)
    ])
  )@postDesc

5. /?query=users.
  name|
  equals(
    name(), 
    leo
  )

6. /?query=
  posts.
    featuredImage|
    isNull(featuredImage())

为了将 () 作为查询字符串的一部分包含,并避免将其视为要执行的字段,我们必须用引号括起来:"..."

在PoP中在浏览器中查看查询

/?query=
  posts.
    sprintf(
      "This post has %s comment(s)", [
      commentCount()
    ])@postDesc

带有指令的可组合字段

可组合字段允许对查询对象本身执行操作。利用这一功能,PoP中的指令变得非常有用,因为它们可以独立地对每个对象进行条件评估。此功能可以引发大量新功能,例如客户端内容操作、精细访问控制、增强验证等。

例如,GraphQL规范 要求 支持指令 includeskip,它们接收一个带有布尔值的参数 if。虽然GraphQL期望此值通过变量提供(如上节指令所示),但在PoP中可以从对象中检索。

在PoP中(在浏览器中查看:查询1查询2

1. /?query=
  posts.
    id|
    title|
    featuredImage<
      include(if:not(isNull(featuredImage())))
    >.
      id|
      src

2. /?query=
  posts.
    id|
    title|
    featuredImage<
      skip(if:isNull(featuredImage()))
    >.
      id|
      src

如果为空则跳过输出

每当字段的值是null时,其嵌套字段将不会被检索。例如,考虑以下情况,其中字段 "featuredImage" 有时是null

在PoP中在浏览器中查看

/?query=
  posts.
    id|
    title|
    featuredImage.
      id|
      src

如上节指令式组合字段中所示,通过结合指令includeskip与组合字段,我们可以决定当字段值为null时不输出该字段。然而,执行此行为的查询在查询路径中间添加了一个指令,使得查询非常冗长且可读性差。由于这是一个非常常见的用例,因此将其通用化并将简化的版本纳入语法中是有意义的。

为此,PoP引入了符号?,将其放置在字段名(及其字段参数、别名和书签)之后,以表示“如果此值是null,则不输出它”。

在PoP中 (在浏览器中查看查询)

/?query=
  posts.
    id|
    title|
    featuredImage?.
      id|
      src

组合指令和表达式

指令可以嵌套:外部指令可以修改其内部嵌套指令的行为。它可以通过“表达式”将值传递给其组合指令,这些表达式是带有%...%的变量,可以在运行时创建(作为查询的一部分编写),也可以在指令本身中定义。

以下示例中,指令<forEach>遍历数组的元素,对每个元素执行组合指令<applyFunction>的操作。它通过预定义表达式%{value}%(在指令内编写)传递数组项。

在PoP中 (在浏览器中查看查询)

/?query=
  echo([
    [banana, apple],
    [strawberry, grape, melon]
  ])@fruitJoin<
    forEach<
      applyFunction(
        function: arrayJoin,
        addArguments: [
          array: %{value}%,
          separator: "---"
        ]
      )
    >
  >

以下示例中,指令<advancePointerInArrayOrObject>通过表达式%{toLang}%(即时定义)将语言传递给指令<translate>,以表示要翻译到的语言。

在PoP中 (在浏览器中查看查询)

/?query=
  echo([
    {
      text: Hello my friends,
      translateTo: fr
    },
    {
      text: How do you like this software so far?,
      translateTo: es
    }
  ])@translated<
    forEach<
      advancePointerInArrayOrObject(
        path: text,
        appendExpressions: {
          toLang:extract(%{value}%,translateTo)
        }
      )<
        translateMultiple(
          from: en,
          to: %{toLang}%,
          oneLanguagePerField: true
        )
      >
    >
  >

组合元素

可以组合不同的元素,以下是一些示例。

一个片段可以包含嵌套路径、变量、指令和其他片段

在PoP中 (在浏览器中查看查询)

/?
postData=
  id|
  title|
  --nestedPostData|
  dateStr(format:$format)&
nestedPostData=
  comments<
    include(if:$include)
  >.
    id|
    content&
format=d/m/Y&
include=true&
limit=3&
order=title&
query=
  posts(
    pagination: {
      limit:$limit
    },
    sort: {
      order:$order
    }
  ).
    --postData|
    author.
      posts(
        pagination: {
          limit:$limit
        }
      ).
        --postData

一个片段可以包含指令,这些指令被传递到片段解析字段中

在PoP中 (在浏览器中查看查询)

/?
fragments[props]=
  title|
  date& 
query=
  posts.
    id|
    --props<
      include(if:hasComments())
    >

如果片段解析字段中的字段已经有自己的指令,则应用这些指令;否则,应用片段定义中的指令

在PoP中 (在浏览器中查看查询)

/?
fragments[props]=
  title|
  url<
    include(if:not(hasComments()))
  >&
query=
  posts.
    id|
    --props<
      include(if:hasComments())
    >

一个片段可以包含别名,然后将其传递到所有片段解析字段作为枚举别名(例如@aliasName1@aliasName2等)

在PoP中 (在浏览器中查看查询)

/?
fragments[props]=
  title|
  url|
  featuredImage&
query=
  posts.
    id|
    --props@prop

一个片段可以包含“如果为null则跳过输出”符号,然后将其传递到所有片段解析字段

在PoP中 (在浏览器中查看查询)

/?
fragments[props]=
  title|
  url|
  featuredImage&
query=
  posts.
    id|
    --props?

将指令和跳过输出如果为null符号与片段结合

在PoP (在浏览器中查看查询)

/?
fragments[props]=
  title|
  url<
    include(if:hasComments())
  >|
  featuredImage&
query=
  posts.
    id|
    hasComments|
    --props?<
      include(if:hasComments())
    >

PHP版本

需求

  • 开发需要PHP 8.1+
  • 生产需要PHP 7.1+

支持的PHP特性

查看leoloso/PoP中支持的PHP特性列表(链接)

预览降级到PHP 7.1

通过Rector(dry-run模式)

composer preview-code-downgrade

标准

PSR-1、PSR-4 和 PSR-12。

要检查编码标准,运行PHP CodeSniffer

composer check-style

要自动修复问题,运行

composer fix-style

变更日志

请参阅CHANGELOG了解最近更改的详细信息。

测试

$ composer test

报告问题

要报告错误或请求新功能,请在PoP monorepo问题跟踪器上操作。

贡献

我们欢迎对这个包的贡献,可以在PoP monorepo(这个包的源代码托管的地方)进行。

请参阅CONTRIBUTINGCODE_OF_CONDUCT以获取详细信息。

安全性

如果您发现任何安全问题,请通过电子邮件leo@getpop.org发送,而不是使用问题跟踪器。

鸣谢

许可证

GNU通用公共许可证v2(或更高版本)。请参阅许可证文件以获取更多信息。