getpop/component-model

用于PoP的组件模型,组件化架构基于此模型

5.0.0 2024-09-07 03:33 UTC

README

用于PoP的组件模型,组件化架构基于此模型。

安装

通过Composer

composer require getpop/component-model

开发

源代码托管在GatoGraphQL monorepo上,在Engine/packages/component-model下。

使用方法

初始化组件

\PoP\Root\App::stockAndInitializeModuleClasses([([
    \PoP\ComponentModel\Component::class,
]);

主要概念

一切都是组件

"组件"这个术语通常用来描述封装一组相关函数以构建模块化应用的概念。[请参考软件组件] 在PoP中,组件被称为"组件",所以从现在起,"组件"和"组件"这两个名称可以互换使用。

组件只是一组HTML、JavaScript和CSS代码的组合,用来创建一个自主实体。每个组件可以是一个原子功能,其他组件的组合,或者两者的组合。每个组件都有其目的,从非常基础的东西,如链接或按钮,到非常复杂的东西,如轮播图或拖放图片上传器。

组件之间的关系是以严格自上而下的方式定义的:一个组件包装其他组件并知道它们是谁,但它不知道也不关心哪些组件在包装它。更复杂的组件是通过迭代包装更简单的组件来创建的,直到达到代表网页的最顶层组件。

Sequence of components wrapping components wrapping components, from an avatar all the way up to the webpage

在PoP中,一切都是组件

In PoP, everything is a component

所有组件之间相互包装的关系,从最顶层组件一直到最后一层,称为组件层次结构。PoP API的核心是组件层次结构,在服务器端以关联数组的形式实现,其中每个组件以其名称作为键属性,并以其所需的所有属性作为值,然后在其"components"属性下嵌套其子组件,迭代地添加它们自己的数据和它们子组件的数据。最后,这个关联数组以JSON对象的形式返回,通过API进行消费。

{
  "topmost-component": {
    someprop: {...},
    components: {
      "component-level1": {
        someprop: {...},
        someprop: {...},
        components: {
          "component-level11": {
            ...
          }
        },
        components: {
          "component-level12": {
            someprop: {...},
            components: {
              "component-level121": {
                ...
              }
            }
          }
        }
      },
      "component-level2": {
        someprop: {...},
        components: {
          "component-level21": {
            ...
          }
        }
      }
    }
  }
}

关系型数据库数据

PoP以关系方式表示数据库数据,按每个对象类型、对象ID和对象属性组织,反映了数据库中数据的结构。这样,所有数据都是归一化的,只从数据库中获取一次,只在输出中打印一次。它在API响应中的databases条目下添加。

{
  databases: {
    primary: {
      db_key_1: {
        dbobject_1_id: {
          property_1: ...,
          ...
          property_n: ...,
        },
        ...
        dbobject_n_id: {
          property_1: ...,
          ...
          property_n: ...,
        },
      },
      ...
      db_key_n: {...},
    }
  }
}

例如,如果获取标题为"Hello World!"和"Everything fine?"且作者为"Leo"的博客文章的数据,那么PoP将返回以下响应;请注意,属性"author"包含指向作者对象的ID,而不是直接打印作者数据。

{
  databases: {
    primary: {
      posts: {
        4: {
          title: "Hello World!",
          author: 7
        },
        9: {
          title: "Everything fine?",
          author: 7
        }
      },
      users: {
        7: {
          name: "Leo"
        }
      }
    }
  }
}

每个组件都知道从 datasetcomponentdata 部分查询的对象,该部分提供了在 objectIDs 属性下查询对象的 ID(例如博客文章的 ID 4 和 9),并且知道从 databases 部分通过 componentsettings 部分检索数据库对象数据,其中 outputKeys 属性指示每个对象属于哪种类型(因此,它知道文章的作者数据,对应于 "author" 属性下的给定 ID 的作者,位于 "users" 对象类型中)

{
  componentsettings: {
    "page": {
      components: {
        "post-feed": {
          outputKeys: {
            id: "posts",
            author: "users"
          }
        }
      }
    }
  },
  datasetcomponentdata: {
    "page": {
      components: {
        "post-feed": {
          objectIDs: [4, 9]
        }
      }
    }
  }
}

该引擎从组件层次结构中推断如何检索数据库数据

当一个组件从数据库对象显示属性时,该组件可能不知道或关心它是哪个对象;它只关心定义从加载的对象中需要哪些属性。例如,考虑下面的图像:一个组件从数据库中加载一个对象(在这种情况下,单个帖子),然后其子组件将显示对象的某些属性,例如 "title" 和 "content"。

While some components load the database object, others load properties

因此,在组件层次结构中,一些组件将负责加载查询对象(在这种情况下,加载单个帖子的组件),其子组件将定义从数据库对象中需要哪些属性(在这种情况下为 "title" 和 "content")。

可以通过遍历组件层次结构自动检索 DB 对象的所有必需属性:从数据加载组件开始,迭代所有子组件直到达到新的数据加载组件或树尾;在每一级获得所有必需的属性,然后将所有属性合并并从数据库中查询,所有这些属性只查询一次。在下面的结构中,组件 "single-post" 从 DB 获取结果,子组件 "post-title" 和 "post-content" 定义查询 DB 对象时需要加载的属性(分别为 "title" 和 "content");子组件 "post-layout" 不需要任何数据字段。请注意,执行查询(从组件层次结构和它们所需的数据字段自动计算)将包含所有组件及其子组件所需的属性。

"single-post"
  => Load objects from domain "post" where ID = 37
  components
    "post-layout"
      components
        "post-title"
          => Load property "title"
        "post-content"
          => Load property "content"

这会导致以下(伪)查询

SELECT 
  title, content 
FROM 
  posts 
WHERE 
  id = 37 

当组件层次结构更改时,从数据库中检索数据的查询将自动更新。如果我们向 "single-post" 下添加子组件 "post-thumbnail",它需要数据字段 "thumbnail"。

"single-post"
  => Load objects from domain "post" where ID = 37
  components
    "post-layout"
      components
        "post-title"
          => Load property "title"
        "post-content"
          => Load property "content"
        "post-thumbnail"
          => Load property "thumbnail"

然后查询将自动更新以包含新数据

SELECT 
  title, content, thumbnail 
FROM 
  posts 
WHERE 
  id = 37 

此策略也适用于关系对象。考虑下面的图像:从对象域 "post" 开始,我们需要将 DB 对象域更改为实体 "user" 和 "comment",分别对应于帖子的作者和帖子的每个评论,然后,对于每个评论,它必须再次将域更改为 "user",以表示评论的作者。从一域更改为另一域后,从该组件层次结构级别向下,所有必需的属性都将受到新域的影响:属性 "name" 从表示帖子作者的 "user" 对象中获取,"content" 从表示帖子每个评论的 "comment" 对象中获取,然后从表示每个评论作者的 "user" 对象中获取 "name"。

Changing the DB object from one domain to another

回到我们之前的例子,如果我们需要显示帖子的作者数据,堆叠子组件 "post-author" 将在该级别将域从 "post" 更改为相应的 "user",然后从该级别向下传递给组件的上下文中的 DB 对象是用户。然后,"post-author" 下的子组件 "user-name" 和 "user-avatar" 将在用户对象中加载属性 "name" 和 "avatar"。

"single-post"
  => Load objects from domain "post" where ID = 37
  components
    "post-layout"
      components
        "post-title"
          => Load property "title"
        "post-content"
          => Load property "content"
        "post-author"
          => Change object domain from "post" to "user", based on property "author"
          components
            "user-layout"
              components
                "user-name"
                  => Load property "name"
                "user-avatar"
                  => Load property "avatar"

导致以下查询

SELECT 
  p.title, p.content, p.author, u.name, u.avatar 
FROM 
  posts p 
INNER JOIN 
  users u 
WHERE 
  p.id = 37 AND p.author = u.id 

配置值包含在内,可以通过 props 覆盖

注意:配置层必须通过另一个包添加

在JavaScript文件中为客户端渲染硬编码类名或其他属性(如标题的HTML标签或头像最大宽度)而不是,我们可以通过API传递配置值,这样就可以在服务器上直接更新这些值,而不需要重新部署JavaScript文件

{
  componentsettings: {
    "component1": {
      components: {
        "component2": {
          configuration: {
            class: "whoweare text-center",
            title: "Who we are",
            titletag: "h3"
          },
          components: {
            "component3": {
              configuration: {
                classes: {
                  wrapper: "media",
                  avatar: "mr-3",
                  body: "media-body",
                  items: "row",
                  item: "col-sm-6"
                },
                avatarmaxsize: "100px"
              },
              components: {
                "component4": {
                  configuration: {
                    classes: {
                      wrapper: "card",
                      image: "card-img-top",
                      body: "card-body",
                      title: "card-title",
                      avatar: "img-thumbnail"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

配置值可以通过props设置,这些props在组件层级中定义,以便组件可以修改其子组件的行为,并且高级组件在设置prop时有优先权。设置props是单向的:父组件可以设置任何子组件的prop,但没有组件可以设置任何祖先组件的prop,或者设置属于不同分支的组件的prop。在下面的例子中,“component1”可以设置“component2”、“component3”和“component4”的props,“component2”可以设置“component3”的props,而“component3”和“component4”则不能设置任何props

"component1"
  components
    "component2"
      components
        "component3"
    "component4"

假设我们有一个名为“component3”的组件,其属性“color”默认设置为“red”。如果不指定prop的目标组件,下面的伪代码中的组件正在设置自身的prop

Component("component3")->setProp({
  prop: "color",
  value: "red"
});

通过“componentpath”属性,一个组件可以将其自身及其任何子组件的任何级别的属性目标化。在下面的伪代码中,父组件“component2”添加了指向子组件“component3”的“componentpath”属性,将其“color”属性的值覆盖为“green”

Component("component2")->setProp({
  componentpath: ["component3"],
  prop: "color",
  value: "green"
});

这可以无限地进行。例如,在下面的伪代码中,“component1”组件,它是“component2”的父组件,可以进一步覆盖应用于“component3”的“color”属性的值

Component("component1")->setProp({
  componentpath: ["component2", "component3"],
  prop: "color",
  value: "blue"
});

组件“component1”可以直接设置组件“component3”的属性,该属性位于2级以下(“component1”=>“component2”=>“component3”),而不必通过中间组件(在这种情况下通过“component2”)重新传输信息。因此,API允许通过跳过中间组件来设置props,即路径上的每个组件不必须传递prop值,直到它到达目的地,prop值将在目标组件的上下文中设置,而不会污染中间组件的上下文。

凭借这些功能,API允许对组件进行强大的自定义,从而可以针对不同的用例生成各种布局和功能。例如,在下面的图片中,组件<ShareButtons>被嵌入两次,根据是否设置属性“show-description”为truefalse来打印描述(“Facebook”、“Twitter”等)

A component can be customized through props

在下面的图片中,只需覆盖组件中打印的类,就可以以三种不同的方式渲染组件

// Layout on the left uses default configuration of thumbnail on top of the text
Component("post-layout")->setProp({
  prop: "classes",
  value: {
    wrapper: "",
    thumb: "",
    contentbody: ""
  }
});

// Layout on the center display a big thumbnail to the left of the text
Component("central-section")->setProp({
  componentpath: ["post-layout"],
  prop: "classes",
  value: {
    wrapper: "row",
    thumb: "col-sm-4",
    contentbody: "col-sm-8"
  }
});

// Layout on the floating window display a small thumbnail to the left of the text
Component("floating-window")->setProp({
  componentpath: ["post-layout"],
  prop: "classes",
  value: {
    wrapper: "media",
    thumb: "media-left",
    contentbody: "media-body"
  }
});

A component can be rendered in multiple fashions

网页是其自己的API端点

PoP将只发出一个请求来获取页面上所有组件的所有数据,对所有结果进行数据库数据标准化。要调用的API端点简单地说与我们要获取数据的网页URL相同,只需添加一个额外的参数output=json,表示以JSON格式提供数据,而不是将其作为HTML打印出来

GET - /url-of-the-page/?output=json

组件是其自己的API

每个组件都可以通过将其组件路径添加到包含它的网页URL中,从客户端与服务器交互。这样,在创建组件时,我们不需要创建与它一起使用的API(如REST或GraphQL),因为组件已经能够与服务器上的自身进行通信并加载其数据:它是完全自治和自助的。

这是通过允许选择要包含在响应中的组件路径(即从顶级组件开始的特定组件的路径)来实现的,以便只从该级别开始加载数据,并忽略该级别以上的所有内容。这是通过在URL中添加参数componentFilter=componentpathscomponentpaths[]=path-to-the-component来完成的(我们使用componentpaths[]而不是componentpaths以提高灵活性,以便我们可以在单个请求中包含多个组件路径)。componentpaths[]参数的值是由点分隔的组件列表。因此,为位于component1 => component2 => component5下的组件“component5”获取数据,是通过将参数componentpaths[]=component1.component2.component5添加到URL来完成的。

例如,在以下组件层次结构中,每个组件都在加载数据,因此每个级别都有一个objectIDs条目。

"component1"
  objectIDs: [...]
  components
    "component2"
      objectIDs: [...]
      components
        "component3"
          objectIDs: [...]
        "component4"
          objectIDs: [...]
        "component5"
          objectIDs: [...]
          components
            "component6"
              objectIDs: [...]

然后,通过在网页URL中添加参数componentFilter=componentpathscomponentpaths[]=component1.component2.component5,将产生以下响应。

"component1"
  components
    "component2"
      components
        "component5"
          objectIDs: [...]
          components
            "component6"
              objectIDs: [...]

本质上,API从component1 => component2 => component5开始加载数据,这就是为什么“component6”(它位于“component5”下)也带来了它的数据,但“component3”和“component4”则没有。

每个加载数据的组件都会在datasetcomponentmeta部分下的dataloadsource条目中导出与其交互的URL。

{
  datasetcomponentmeta: {
    "component1": {
      components: {
        "component2": {
          components: {
            "component5":  {
              meta: {
                dataloadsource: "https://page-url/?componentFilter=componentpaths&componentpaths[]=component1.component2.component5"
              },
              components: {
                "component6": {
                  meta: {
                    dataloadsource: "https://page-url/?componentFilter=componentpaths&componentpaths[]=component1.component2.component5.component6"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

架构设计和实现

在PoP中,组件被称作“component”,因此从现在开始,“component”和“component”这两个术语将互换使用。

组件层次结构和JSON输出

所有组件相互嵌套的关系,从顶级组件一直到底层,被称为组件层次结构。这种关系可以通过服务器端上的关联数组(键=>属性数组的数组)来表示,其中每个组件以它的名称作为键属性,以“components”属性下的内部组件表示。

// Component hierarchy on server-side, eg: through PHP:
[
  "top-component" => [
    "components" => [
      "component-level1" => [
        "components" => [
          "component-level11" => [
            "components" => [...]
          ],
          "component-level12" => [
            "components" => [
              "component-level121" => [
                "components" => [...]
              ]
            ]
          ]
        ]
      ],
      "component-level2" => [
        "components" => [
          "component-level21" => [
            "components" => [...]
          ]
        ]
      ]
    ]
  ]
]

请注意组件是如何嵌套的。这样,如果组件具有相同的名称,组件属性将永远不会相互冲突,从而避免了为组件添加命名空间的需求。例如,一个组件配置中的"class"属性不会覆盖另一个组件配置中的"class"属性。

然后API简单地将其编码为JSON对象以供消费。它的格式是一个完整的规范:只要服务器以所需格式返回JSON响应,客户端就可以独立于其实现方式来消费API。

// Component hierarchy encoded as JSON:
{
  "top-component": {
    components: {
      "component-level1": {
        components: {
          "component-level11": {
            ...
          },
          "component-level12": {
            components: {
              "component-level121": {
                ...
              }
            }
          }
        }
      },
      "component-level2": {
        components: {
          "component-level21": {
            ...
          }
        }
      }
    }
  }
}

组件之间的关系是以严格的自顶向下方式定义的:一个组件封装其他组件并知道它们是谁,但它不知道,也不关心哪些组件在封装它。例如,在上面的JSON代码中,组件"component-level1"知道它封装了组件"component-level11"``和"component-level12",并且通过传递性,它还知道它封装了"component-level121";但组件"component-level11"并不关心谁在封装它,因此它对"component-level1"一无所知。

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

{
  componentsettings: {
    "top-component": {
      configuration: {...},
      ...,
      components: {
        "component-level1": {
          configuration: {...},
          ...,
          components: {
            "component-level11": {
              repeat...
            },
            "component-level12": {
              configuration: {...},
              ...,
              components: {
                "component-level121": {
                  repeat...
                }
              }
            }
          }
        },
        "component-level2": {
          configuration: {...},
          ...,
          components: {
            "component-level21": {
              repeat...
            }
          }
        }
      }
    }
  },
  datasetcomponentdata: {
    "top-component": {
      objectIDs: [...],
      ...,
      components: {
        "component-level1": {
          objectIDs: [...],
          ...,
          components: {
            "component-level11": {
              repeat...
            },
            "component-level12": {
              objectIDs: [...],
              ...,
              components: {
                "component-level121": {
                  repeat...
                }
              }
            }
          }
        },
        "component-level2": {
          objectIDs: [...],
          ...,
          components: {
            "component-level21": {
              repeat...
            }
          }
        }
      }
    }
  }
}

组件属性(配置值、要获取的数据库数据等)和子组件不会手动添加到关联数组中。相反,它们通过在每个组件上定义的名为 ComponentProcessor 的对象来定义。PoP 引擎将遍历组件层次结构中的所有组件,从入口组件开始,从相应的 ComponentProcessor 中获取每个组件的属性,并创建包含所有组件属性的嵌套关联数组。名为 COMPONENT_SOMENAME 的组件的 ComponentProcessor 看起来像这样

class SomeComponentProcessor extends AbstractComponentProcessor {

  const COMPONENT_SOMENAME = 'somename';

  function getSubcomponentsToProcess() {
  
    return array(
      COMPONENT_SOMENAME,
    );
  }

  function getSubcomponents($component) 
  {
    $ret = parent::getSubcomponents($component);

    switch ($component->name) {
      
      case self::COMPONENT_SOMENAME:
        
        $ret[] = self::COMPONENT_SOMELAYOUT1;
        $ret[] = self::COMPONENT_SOMELAYOUT2;
        break;
    }

    return $ret;
  }

  function getImmutableConfiguration($component, &$props) 
  {
    $ret = parent::getImmutableConfiguration($component, $props);

    // Print the components properties ...
    switch ($component->name) {
      case self::COMPONENT_SOMENAME:        
        $ret['description'] = __('Some description');
        $ret['showmore'] = $this->getProp($component, $props, 'showmore');
        $ret['class'] = $this->getProp($component, $props, 'class');
        break;
    }

    return $ret;
  }
  
  function initModelProps($component, &$props) 
  {
    // Implement the components properties ...
    switch ($component->name) {
      case self::COMPONENT_SOMENAME:
        $this->setProp($component, $props, 'showmore', false);
        $this->appendProp($component, $props, 'class', 'text-center');
        break;
    }

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

数据库对象数据检索并放置在名为 databases 的共享部分中,以避免当两个或更多不同的组件从数据库中检索相同对象时重复信息。此外,它以关系的方式添加到关联数组中,并在 JSON 响应中打印,以避免当两个或更多不同的数据库对象与一个共同对象(如两个具有相同作者的帖子)相关联时重复信息。换句话说,数据库对象数据是规范化的。其结构是一个字典,首先按对象类型组织,然后按对象 ID 组织,我们可以从中获取对象属性

{
  databases: {
    primary: {
      dbobject_type: {
        dbobject_id: {
          property: ...,
          ...
        },
        ...
      },
      ...
    }
  }
}

API 响应示例

例如,下面的 API 响应包含一个包含两个组件的组件层次结构,"page" => "post-feed",其中组件 "post-feed" 检索博客帖子。请注意以下内容

  • 每个组件都知道其查询的对象,从属性 objectIDs(博客帖子的 ID 4 和 9)中得知
  • 每个组件都知道其查询对象的类型,从属性 outputKeys 中得知(每个帖子的数据都位于 "posts" 下,帖子的作者数据,对应于帖子属性 "author" 下给出的作者 ID,位于 "users" 下)
  • 由于数据库对象数据是关联的,属性 "author" 包含指向作者对象的 ID 而不是直接打印作者数据
{
  datasetcomponentdata: {
    "page": {
      components: {
        "post-feed": {
          objectIDs: [4, 9]
        }
      }
    }
  },
  componentsettings: {
    "page": {
      components: {
        "post-feed": {
          outputKeys: {
            id: "posts",
            author: "users"
          }
        }
      }
    }
  },
  databases: {
    primary: {
      posts: {
        4: {
          title: "Hello World!",
          author: 7
        },
        9: {
          title: "Everything fine?",
          author: 7
        }
      },
      users: {
        7: {
          name: "Leo"
        }
      }
    }
  }
}

组件的定义

每个组件都有一个唯一名称来标识它,定义为常量

const COMPONENT_SOMENAME = 'somename';

所有组件属性都通过名为 ComponentProcessor 的对象实现。

虚拟组件

虚拟组件是“动态生成”的组件:具有基础个性和动态行为的组件。例如,API 的 自定义查询功能 根据 URL 参数 query 的值创建组件层次结构,为每个嵌套关系沿路径创建一个虚拟组件。

虚拟组件不能依赖于 props 来定义其行为,因为在创建组件层次结构时我们没有 $props 可用(否则就是鸡生蛋的问题)。因此,给虚拟组件提供的特定属性编码到组件名称本身中。然后,组件的个性由名为 "componentname" 的组件给出,虚拟组件属性是运行时元素,它定义了其动态行为。

ComponentProcessor

ComponentProcessor 是一个对象类,用于定义组件的所有属性。ComponentProcessor 依据 SOLID 方法实现,建立一个对象继承方案,逐步向组件添加属性。所有 ComponentProcessor 的基类是 AbstractComponentProcessor

namespace PoP\Engine;
abstract class AbstractComponentProcessor {

  // ...
}

在实践中,由于组件是通过 ComponentProcessor 对象实现的,描述组件等于描述 ComponentProcessor 如何实现所有功能来定义组件的属性。

每个ComponentProcessor可以处理多个组件:因为不同的组件自然会共享许多属性,所以让单个ComponentProcessor实现多个组件比每个组件一个ComponentProcessor更易于阅读,并且可以减少代码量。通过函数getSubcomponentsToProcess定义了哪些组件由ComponentProcessor处理。

class SomeComponentProcessor extends \PoP\Engine\AbstractComponentProcessor {

  const COMPONENT_SOMENAME1 = 'somename1';
  const COMPONENT_SOMENAME2 = 'somename2';
  const COMPONENT_SOMENAME3 = 'somename3';

  function getSubcomponentsToProcess() {
  
    return array(
      COMPONENT_SOMENAME1,
      COMPONENT_SOMENAME2,
      COMPONENT_SOMENAME3,
    );
  }

  // Implement the components properties ...
  // ...
}

一旦实例化了ComponentProcessor类,其定义的所有组件都可以添加到组件层次结构中。

要访问组件的属性,我们必须通过类ComponentProcessor_Manager中的函数getComponentProcessor引用其对应的ComponentProcessor。

// Retrieve the PoP_ComponentProcessor_Manager object from the factory
$componentprocessor_manager = \PoP\Engine\ComponentProcessor_Manager_Factory::getInstance();

// Obtain the ComponentProcessor for component COMPONENT_SOMENAME
$processor = $componentprocessor_manager->getComponentProcessor(new \PoP\ComponentModel\Component\Component(SomeComponentProcessor::class, SomeComponentProcessor::COMPONENT_SOMENAME));

// Do something...
// $processor->...

组件的解剖结构

由于ComponentProcessor可以处理多个组件,因此每个函数都将接收一个参数$component,指示正在处理的组件是哪一个。请注意,在函数内部,我们可以方便地使用switch语句来相应地操作(具有共享属性的组件可以轻松共享逻辑),并且根据SOLID原则,我们首先获取父类的结果,然后ComponentProcessor添加自己的属性。

class SomeComponentProcessor extends \PoP\Engine\AbstractComponentProcessor {

  function foo($component) 
  {
    // First obtain the value from the parent class
    $ret = parent::foo($component);

    // Add properties to the component
    switch ($component->name) 
    {
      case self::COMPONENT_SOMENAME1:
        
        // Do something with $ret
        // ...
        break;

      // These components share the same properties
      case self::COMPONENT_SOMENAME2:
      case self::COMPONENT_SOMENAME3:
        
        // Do something with $ret
        // ...
        break;
    }

    return $ret;
  }
}

除了参数$component之外,大多数函数还会接收一个$props参数,其值为组件上设置的"props"(有关更多信息,请参阅Props部分)。

class SomeComponentProcessor extends \PoP\Engine\AbstractComponentProcessor {

  function foo($component, &$atts) 
  {
    $ret = parent::foo($component, &$atts);

    // ...

    return $ret;
  }
}

组合

组件通过函数getSubcomponents由其他组件组成。

class SomeComponentProcessor extends \PoP\Engine\AbstractComponentProcessor {

  function getSubcomponents($component) 
  {
    $ret = parent::getSubcomponents($component);

    switch ($component->name) 
    {
      case self::COMPONENT_SOMENAME1:
        
        $ret[] = self::COMPONENT_SOMENAME2;
        break;

      case self::COMPONENT_SOMENAME2:
      case self::COMPONENT_SOMENAME3:
        
        $ret[] = new \PoP\ComponentModel\Component\Component(LayoutComponentProcessor::class, LayoutComponentProcessor::COMPONENT_LAYOUT1);
        $ret[] = new \PoP\ComponentModel\Component\Component(LayoutComponentProcessor::class, LayoutComponentProcessor::COMPONENT_LAYOUT2);
        $ret[] = new \PoP\ComponentModel\Component\Component(LayoutComponentProcessor::class, LayoutComponentProcessor::COMPONENT_LAYOUT3);
        break;
    }

    return $ret;
  }
}

注意:组件层次结构是通过在入口组件上调用getSubcomponents创建的,然后迭代地为子组件重复此过程。

抽象ComponentProcessors可以通过占位符函数定义所需的子组件,这些函数将由继承的ComponentProcessor实现。

abstract class PostLayoutAbstractComponentProcessor extends \PoP\Engine\AbstractComponentProcessor {

  function getSubcomponents($component) {
  
    $ret = parent::getSubcomponents($component);

    if ($thumbnail_component = $this->getThumbnailComponent($component)) 
    {
      $ret[] = $thumbnail_component;
    }
    if ($content_component = $this->getContentComponent($component)) 
    {
      $ret[] = $content_component;
    }
    if ($aftercontent_components = $this->getAftercontentComponents($component)) 
    {
      $ret = array_merge(
        $ret,
        $aftercontent_components
      );
    }

    return $ret;
  }

  protected function getContentComponent($component) 
  {
    // Must implement
    return null;
  }
  protected function getThumbnailComponent($component) 
  {
    // Default value
    return self::COMPONENT_LAYOUT_THUMBNAILSMALL;
  }
  protected function getAftercontentComponents($component) 
  {
    return array();
  }
}

class PostLayoutComponentProcessor extends PostLayoutAbstractComponentProcessor {

  protected function getContentComponent($component) 
  {
    switch ($component->name) 
    {
      case self::COMPONENT_SOMENAME1:
        
        return self::COMPONENT_LAYOUT_POSTCONTENT;

      case self::COMPONENT_SOMENAME2:
      case self::COMPONENT_SOMENAME3:
        
        return self::COMPONENT_LAYOUT_POSTEXCERPT;
    }

    return parent::getContentComponent($component);
  }
  protected function getThumbnailComponent($component) 
  {
    switch ($component->name) 
    {
      case self::COMPONENT_SOMENAME1:
        
        return self::COMPONENT_LAYOUT_THUMBNAILBIG;

      case self::COMPONENT_SOMENAME3:
        
        return self::COMPONENT_LAYOUT_THUMBNAILMEDIUM;
    }

    return parent::getThumbnailComponent($component);
  }
  protected function getAftercontentComponents($component) 
  {
    $ret = parent::getAftercontentComponents($component);

    switch ($component->name) 
    {
      case self::COMPONENT_SOMENAME2:
        
        $ret[] = self::COMPONENT_LAYOUT_POSTLIKES;
        break
    }

    return $ret;
  }
}

// Initialize
new PostLayoutComponentProcessor();

函数名称和缓存

组件层次结构取决于该URL中需要哪些组件,而不是取决于URL本身。因此,包含在不同URL中的组件层次结构可以被缓存并跨它们重用。例如,请求/events/1//events/2/很可能具有相同的组件层次结构。然后,第二个请求可以重用第一个请求缓存的组件层次结构,从而避免再次计算所有必需的属性,从而优化性能。

大多数属性都可以缓存,但是有一些属性不能缓存。例如,添加配置属性"classname",其值为post-{id}(其中ID是请求帖子的ID),不能缓存,因为ID直接取决于URL。为了解决这个问题并最大化缓存量,PoP已将组件属性分成不重叠的部分:"可缓存的"和"不可缓存的",并通过适当的函数实现。

缓存在两个不同的区域执行:服务器端和客户端。所有功能都将在服务器上需要,但并非所有内容都会发送到客户端。例如,"props"用于修改配置值;虽然配置值会发送到客户端,但"props"本身不会。这两种情况下的缓存不同,因此定义它们的函数也将不同,如下所述。

服务器端仅属性:模型和请求

仅在应用程序服务器端需要且永远不会到达客户端的属性可以分为"模型"和"请求"。

  • 模型:它是"组件层次结构"的同义词。它表示所有固定到组件层次结构的组件属性。因此,当在服务器上缓存组件层次结构时,这些属性可以包含在缓存中。
  • 请求:这些属性可以根据请求的URL而改变,因此它们不能在缓存组件层次结构时包含在内。

例如,一个名为 "description" 的属性,其值为 "Welcome to my site!",在组件层次结构中是不可变的,因此可以在 model 函数中设置。属性 "classname",其值为 post-{id},其中 ID 是请求帖子的 ID,它直接取决于 URL,必须在 request 函数下设置。

客户端属性:不可变、在模型中可变和在请求中可变

发送到客户端的属性(例如:配置值)也可以在客户端应用程序中进行缓存,这样一旦组件层次结构被加载,我们就可以避免再次获取此信息,如果我们已经拥有满足请求的所有数据,我们可以在不与服务器通信的情况下渲染布局。

在服务器端进行缓存很简单:每个组件层次结构都在其自己的文件中进行缓存,因此可以独立处理。然而,在客户端,我们将缓存来自不同请求的 JSON 响应,这些请求涉及具有共享属性的不同的组件层次结构,我们希望只在总共一次而不是每次请求一次的情况下缓存这些共享属性。例如,请求 /posts/1 可能具有组件层次结构 "singlepost" => "postlayout",而 /events/1 可能具有组件层次结构 "singlepost" => "eventlayout"。在这种情况下,第一级 "singlepost" 在不同的请求之间是共享的。

为了处理这种情况,我们需要使函数的命名更精细:上述术语 model 已进一步分为两个,immutablemutable on model,术语 request 已重命名为 mutable on request。如何以及为什么这样做在“架构设计与实现”文档的 客户端缓存 部分有详细说明。

immutablemutable on model 之间的区别在于,mutable on model 上的属性可以根据组件的后代组件改变其值。

  • 不可变:固定的属性
  • mutableonmodel:可以根据后代组件更改的属性
  • mutableonrequest:可以根据请求的 URL 更改的属性

例如,我们可以有一个配置属性 "descendants" 明确声明其后代组件的名称。在下面的示例中,组件 "singlepost" 有后代组件 "postlayout" 和配置属性 "descendants",其值为 ["postlayout"]

{
  "singlepost": {
    configuration: {
      class: "text-center",
      descendants: ["postlayout"]
    },
    components: {
      "postlayout": {
        configuration: {
          class: "post-37",
        }
      }
    }
  }
}

然后,属性填充如下:class: "text-center"不可变descendants: ["component2"]在模型中可变,而 class: "post-37",对应于 post-{id},是 在请求中可变

属性

组件最有用之处在于它们是通用的,并且可以通过属性或“属性”进行定制。例如,一个组件可以定义一个属性来更改背景颜色配置值,定义从数据库中获取多少对象,或它可能需要的任何内容。

设置属性只有一个方向:组件可以在任何后代组件上设置属性,但没有任何组件可以在任何祖先组件或属于不同分支的组件上设置属性。在下面的结构中,“component1”可以设置属性在“component2”、“component3”和“component4”上,“component2”在“component3”上,“component3”和“component4”在没有人上。

"component1"
  components
    "component2"
      components
        "component3"
    "component4"

组件可以在组件层次结构中的任何层数下设置后代组件的属性,并且这是直接进行的,即不涉及中间的组件或影响它们的属性。在上面的结构中,“component1”可以直接在“component3”上设置属性,而无需通过“component2”。

属性设置通过函数 initModelProps($component, &$props)initRequestProps($component, &$props) 完成。一个属性必须在其中一个函数中实现,但不能同时在两个函数中实现。initRequestProps 用于定义直接依赖于请求URL的属性,例如,将类名 post-{id} 添加到属性 "class" 中,其中 {id} 是该URL上请求的帖子的ID。initModelProps 用于其他所有内容。

属性设置是在一开始就完成的:在获得组件层次结构之后,PoP Engine 将在 任何事情之前 调用这两个函数(即,在获取配置、获取数据库数据等之前)。因此,除了创建组件层次结构的函数(即 getSubcomponents 和由 getSubcomponents 调用的内部函数)之外,ComponentProcessor 中的每个函数都可以接收 $props

initModelPropsinitRequestProps 在参数 $props 下存储属性,因此它是通过引用传递的。在其他所有函数中,$props 也可以通过引用传递,但这只是为了性能问题,以避免在内存中重复对象。

在这两个函数内部,我们可以通过以下三个函数来设置属性

  • function setProp($component_or_componentpath, &$props, $field, $value, $starting_from_componentpath = array())
  • function appendProp($component_or_componentpath, &$props, $field, $value, $starting_from_componentpath = array())
  • function mergeProp($component_or_componentpath, &$props, $field, $value, $starting_from_componentpath = array())

这三个函数彼此相似,但有以下区别

appendProp 用于向现有属性追加值,因此它是一个累积属性。它通常用于添加类名。例如,如果组件 "component1" 和 "component2" 都在 "component3" 上执行 appendProp 操作,属性 "class" 的值分别为 "big""center",则 "component3" 的 "class" 属性将被设置为 "big center"

mergeProp 类似,但针对数组。它通常用于向DOM元素上打印的参数添加。例如,如果组件 "component1" 和 "component2" 都在 "component3" 上执行 mergeProp 操作,属性 "params" 的值分别为 ["data-target" => "#main"]["data-mode" => "static"],则 "component3" 的 "params" 属性将被设置为 ["data-target" => "#main", "data-mode" => "static"]

setProp 不是累积的,但它只接受一个值:第一个设置的值。因此,在组件层次结构中,较高级别的组件在设置属性值方面比较低级别的组件具有优先级。例如,如果组件 "component1" 和 "component2" 都在 "component3" 上执行 setProp 操作,属性 "title" 的值分别为 "First title""Second title",则 "component3" 的 "title" 属性将被设置为 "First title"

所有三种方法接收相同的参数

  • $component_or_componentpath:此值可以是字符串,包含要设置属性的组件名称,或数组。如果是组件名称,有两种可能性:如果目标组件是设置属性的组件本身,则组件正在对自己设置属性(例如,为属性设置默认值);如果不是,则将在子组件层次结构中找到具有该名称的子组件的属性。如果是数组,则这是要设置属性的已针对目标子组件的子路径。
  • &$props:这是存储所有属性的对象,一个在组件层次结构中传递的独立对象,用于初始化所有属性
  • $field:属性的名称
  • $value:要设置在属性上的值
  • $starting_from_componentpath:包含从组件子路径开始的数组,用于查找要设置属性的靶组件

每个组件首先初始化自己的属性,然后才继续流向父类,这样在对象继承方案中,继承类比它们的祖先有优先级。

function initModelProps($component, &$props) 
{
  // Set prop...
  // Set prop...
  // Set prop...

  parent::initModelProps($component, $props);
}

通过函数 function getProp($component, &$props, $field, $starting_from_componentpath = array()) 访问属性的值。函数的签名与上面的类似,但是没有参数 $value

让我们看一个例子:一个用于渲染地图的组件有2个方向:"horizontal""vertical"。它由组件 "map" => "map-inner" 组成,这两个组件都需要这个属性。组件 "map" 将默认值设置为 "vertical",然后获取此属性的值,以防祖先组件已经设置了该属性,然后将此值设置在组件 "map-inner" 上。下面是针对组件 "map" 实现的函数。

function initModelProps($component, &$props) 
{
  switch ($component->name) {
    case self::COMPONENT_MAP:
      // Component "map" is setting the default value
      $this->setProp($component, $props, 'orientation', 'vertical');

      // Obtain the value from the prop
      $orientation = $this->getProp($component, $props, 'orientation');

      // Set the value on "map-inner"
      $this->setProp([[SomeComponent::class, SomeComponent::COMPONENT_MAPINNER]], $props, 'orientation', $orientation);
      break;
  }

  parent::initModelProps($component, $props);
}

默认情况下,组件地图将具有属性 "orientation",其值为 "vertical"。然而,父组件 "map-wrapper" 可以预先设置此属性为 "horizontal"

function initModelProps($component, &$props) 
{
  switch ($component->name) {
    case self::COMPONENT_MAPWRAPPER:
      $this->setProp([[SomeComponent::class, SomeComponent::COMPONENT_MAP]], $props, 'orientation', 'horizontal');      
      break;
  }

  parent::initModelProps($component, $props);
}

数据加载

在组件层次结构中,某些组件将定义需要从数据库中获取哪些对象,其子组件将指示数据库对象必须具有哪些属性。考虑下面的图像,其中组件 "singlepost" 定义了要加载哪个数据库对象,其子组件 "post-title""post-content" 指示该对象必须加载属性 "title""content"

While some components load the database object, others load properties

数据加载组件

指示需要加载哪些数据库对象的组件被称为“数据加载”组件。为了做到这一点,数据加载组件必须定义以下函数和属性。

定义数据源

通过函数 getDatasource 指示结果是否是 不可变(例如:永远不会改变且可缓存的)或 按需可变。默认情况下,结果设置为 按需可变(通过常量 \PoP\ComponentModel\Constants\DataSources::MUTABLEONREQUEST),因此只有在结果为 不可变 时,此函数必须实现。

function getDatasource($component, &$props) 
{
  switch ($component->name) {
    case self::COMPONENT_WHOWEARE:
      return \PoP\ComponentModel\Constants\DataSources::IMMUTABLE;
  }

  return parent::getDatasource($component, $props);
}
定义数据库对象 ID

通过函数 getDbobjectIds 定义要从数据库中检索的对象的 ID。如果组件已经知道需要哪些数据库对象,它可以简单地返回它们。

function getDbobjectIds($component, &$props, $data_properties) 
{
  switch ($component->name) {
    case self::COMPONENT_WHOWEARE:
      return [13, 54, 998];
  }

  return parent::getDbobjectIds($component, $props, $data_properties);
}

然而,很可能是事先不知道对象,必须通过查询来找到它们。在这种情况下,组件处理器必须从类 QueryDataAbstractComponentProcessor 继承,该类实现了 getDbobjectIds,将找到数据库对象 ID 的责任转移到从相应的 DataloadergetDbobjectIds 函数。

定义 Dataloader

通过函数 getDataloader 定义要使用的 Dataloader,这是负责从数据库获取数据的对象。

function getDataloader($component) 
{
  switch ($component->name) {
    case self::COMPONENT_AUTHORARTICLES:
      return [Dataloader::class, Dataloader::DATALOADER_POSTLIST];
  }
    
  return parent::getDataloader($component);
}
定义查询参数

通过函数 getImmutableDataloadQueryArgsgetMutableonrequestDataloadQueryArgs 定制一个查询来过滤数据,并将其传递给 Dataloader。

protected function getImmutableDataloadQueryArgs($component, $props) 
{
  $ret = parent::getImmutableDataloadQueryArgs($component, $props);
  
  switch ($component->name) {
    case self::COMPONENT_AUTHORARTICLES:
      // 55: id of "Articles" category
      $ret['cat'] = 55;
      break;
  }

  return $ret;
}

protected function getMutableonrequestDataloadQueryArgs($component, $props) 
{
  $ret = parent::getMutableonrequestDataloadQueryArgs($component, $props);
  
  switch ($component->name) {
    case self::COMPONENT_AUTHORARTICLES:
    
      // Set the logged-in user id
      $cmsapi = \PoP\CMS\FunctionAPI_Factory::getInstance();
      $ret['author'] = $cmsapi->getCurrentUserID();
      break;
  }

  return $ret;
}
定义查询输入输出处理器

在获取数据后,我们可以通过 QueryInputOutputHandler 对象来传达状态(例如:是否有更多结果?下一个分页编号是什么?等等),这些对象通过函数 getQueryhandler 定义。默认情况下,它返回具有名称 GD_DATALOAD_QUERYHANDLER_ACTIONEXECUTION 的对象,这在执行操作时需要(参见章节 数据发布和操作)。

function getQueryhandler($component) 
{
  switch ($component->name) {
    case self::COMPONENT_AUTHORARTICLES:
      return GD_DATALOAD_QUERYHANDLER_LIST;
  }
  
  return parent::getQueryhandler($component);
}
定义数据属性

如果组件需要将变量传递给在获取/处理数据中涉及的其他任何对象(DataloaderQueryInputOutputHandlerActionExecuter 等),它可以通过“数据属性”来实现,通过函数 getImmutableHeaddatasetcomponentDataPropertiesgetMutableonrequestHeaddatasetcomponentDataProperties 设置。

function getImmutableHeaddatasetcomponentDataProperties($component, &$props) 
{
  $ret = parent::getImmutableHeaddatasetcomponentDataProperties($component, $props);

  switch ($component->name) {
    case self::COMPONENT_AUTHORARTICLES:
      // Make it not fetch more results
      $ret[GD_DATALOAD_QUERYHANDLERPROPERTY_LIST_STOPFETCHING] = true;
      break;
  }
  
  return $ret;
}
有条件加载数据库数据

我们可以通过将数据加载组件的属性 "skip-data-load" 设置为 true 来指示它不加载其数据

function initModelProps($component, &$props) 
{
  switch ($component->name) {
    case self::COMPONENT_AUTHORARTICLES:

      // Set the content lazy
      $this->setProp($component, $props, 'skip-data-load', true);
      break;
  }

  parent::initModelProps($component, $props);
}

注意:作为一个属性,这个值可以由数据加载组件本身或其任何祖先组件设置。

以下是不为组件加载数据的几个用例

  • 在不提供任何搜索参数的情况下加载搜索页面
  • 验证已登录用户是否有所需的权限
  • 在加载网站时不在加载数据,而仅在加载单页应用(SPA)中的页面时加载数据

数据加载 + 子组件

从一个数据加载组件开始,包括其自身,任何子组件都可以执行以下功能:在数据库对象上加载属性或“数据字段”,以及从当前数据库对象“切换领域”到另一个对象。

定义数据字段

“数据字段”,即从加载的数据库对象中需要的属性,是通过函数 getLeafComponentFieldNodes 定义的

function getLeafComponentFieldNodes($component, $props) 
{
  $ret = parent::getLeafComponentFieldNodes($component, $props);

  switch ($component->name) {
    case self::COMPONENT_AUTHORARTICLES:
      $ret[] = 'title';
      $ret[] = 'content';
      break;
  }

  return $ret;
}

“数据字段”的值通过一个称为 ObjectTypeFieldResolver 的对象解析,如下所述。

将领域切换到关系对象

在定义要检索的数据时,我们还可以“切换领域”,即从当前数据库对象切换到另一个关系定义的对象。

考虑以下图像:从对象类型“文章”开始,并向下移动到组件层次结构,我们需要将数据库对象类型更改为“用户”和“评论”,分别对应文章的作者和每篇文章的评论,然后,对于每个评论,它必须再次将对象类型更改为“用户”以对应评论的作者。切换到新领域后,从组件层次结构中的该级别向下,所有必需的属性或数据字段都将受新领域的影响:属性“名称”从表示文章作者的“用户”对象中获取,“内容”从表示每篇文章评论的“评论”对象中获取,然后从表示每个评论作者的“用户”对象中获取“名称”

Changing the DB object from one domain to another

切换领域是通过函数 getRelationalComponentFieldNodes 完成的。它必须返回一个数组,其中每个键是属性或“数据字段”,包含要切换到的对象的ID,其值是另一个数组,其中键是 Dataloader,用于加载此对象,其值是用于的组件

function getRelationalComponentFieldNodes($component) 
{
  $ret = parent::getRelationalComponentFieldNodes($component);

  switch ($component->name) {
    case self::COMPONENT_AUTHORARTICLES:
    
      $ret['author'] = [
        GD_DATALOADER_USERLIST => [
          COMPONENT_AUTHORNAME,
        ]
      ];
      $ret['comments'] = [
        GD_DATALOADER_COMMENTLIST => [
          COMPONENT_COMMENTLAYOUT,
        ]
      ];
      break;
  }

  return $ret;
}

注意:类似于 getComponents,此方法也将组件加载到组件层次结构中,因此它不能接收参数 $props

或者,我们也可以通过选择通过常量 POP_CONSTANT_SUBCOMPONENTDATALOADER_DEFAULTFROMFIELD 定义的默认数据加载器来代替显式定义数据加载器的名称,这些默认数据加载器是通过 ObjectTypeFieldResolver 定义的。在下面的示例中,将自动选择字段 "author""comments" 的默认数据加载器

function getRelationalComponentFieldNodes($component) 
{
  $ret = parent::getRelationalComponentFieldNodes($component);

  switch ($component->name) {
    case self::COMPONENT_AUTHORARTICLES:
    
      $ret['author'] = [
        POP_CONSTANT_SUBCOMPONENTDATALOADER_DEFAULTFROMFIELD => [
          new \PoP\ComponentModel\Component\Component(SomeComponentProcessor::class, SomeComponentProcessor::COMPONENT_AUTHORNAME),
        ]
      ];
      $ret['comments'] = [
        POP_CONSTANT_SUBCOMPONENTDATALOADER_DEFAULTFROMFIELD => [
          new \PoP\ComponentModel\Component\Component(SomeComponentProcessor::class, SomeComponentProcessor::COMPONENT_COMMENTLAYOUT),
        ]
      ];
      break;
  }

  return $ret;
}

数据加载器

数据加载器对象负责获取数据库数据。它知道它必须获取哪种类型的数据以及如何获取。所有数据加载器都继承自类 Dataloader。给定一个ID数组,它必须通过函数 executeGetData 从数据库中获取相应的对象。

function executeGetData($ids) {
  
  $objects = array();

  // Fetch all objects with IDs $ids
  // ...
  
  return $objects;
}

例如,一个获取文章的数据加载器将实现如下功能

function executeGetData($ids) {
  
  $cmsapi = \PoP\CMS\FunctionAPI_Factory::getInstance();
  $query = array(
    'include' => $ids,
  );
  return $cmsapi->getPosts($query);
}

数据加载器还必须实现以下函数

  • getFieldprocessor:返回将处理数据字段的所有对象返回的 ObjectTypeFieldResolver 的名称
  • getDatabaseKey:返回在JSON响应中对象将被存储在 databases 下的对象类型

例如,一个获取帖子的dataloader将像这样实现这些函数

function getDatabaseKey() 
{
  return GD_DATABASE_KEY_POSTS;
}  

function getFieldprocessor() 
{
  return GD_DATALOAD_FIELDPROCESSOR_POSTS;
}

此外,dataloader很可能也会负责从数据库中获取要获取的$ids。在这种情况下,它必须继承自类QueryDataDataloader,并实现函数getDbobjectIds

function getDbobjectIds($data_properties) {
  
  $ids = array();

  // Find the IDs of the objects to be fetched
  // ...
  
  return $ids;
}

例如,一个获取单个帖子的dataloader将简单地返回帖子的对象ID,在上下文变量

function getDbobjectIds($data_properties) {
  
  // Simply return the global $post ID. 
  $vars = \PoP\ComponentModel\Engine_Vars::getVars();
  return array($vars['global-state']['queried-object-id']);
}

获取结果列表的dataloader(例如:帖子列表、用户列表等)需要执行查询并过滤结果。这种逻辑已在特性Dataloader_ListTrait中实现,该特性要求实现函数getQuery,从通过数据属性提供的$query_args生成查询,以及实现函数executeQueryIDs,给定生成的$query,返回对象ID列表

function getQuery($query_args) 
{
  $query = array();

  // Add all the conditions in $query, taking values from $query_args
  // ...

  return $query;
}

function executeQueryIDs($query) {
    
  $ids = array();

  // Find the IDs of the objects to be fetched
  // ...
  
  return $ids;
}

例如,一个获取帖子列表的dataloader将像这样实现

function getQuery($query_args) {
    
  return $query_args;
}

function executeQueryIDs($query) {
    
  $cmsapi = \PoP\CMS\FunctionAPI_Factory::getInstance();
  return $cmsapi->getPosts($query, [QueryOptions::RETURN_TYPE => ReturnTypes::IDS]);
}  

ObjectTypeFieldResolver

ObjectTypeFieldResolver是解析“数据字段”到其对应值的对象。它必须继承自类AbstractObjectTypeFieldResolver,并实现函数getValue,该函数接收两个参数,$resultitem是数据库对象,$field是要解析的数据字段,必须返回应用于数据库对象的该属性值。

注意:字段名称不能包含以下特殊字符:"," (\PoP\ComponentModel\Tokens\Param::VALUE_SEPARATOR),"." (POP_CONSTANT_DOTSYNTAX_DOT)或"|" (POP_CONSTANT_PARAMFIELD_SEPARATOR)

例如,一个针对帖子的ObjectTypeFieldResolver看起来像这样

class ObjectTypeFieldResolver_Posts extends \PoP\Engine\AbstractObjectTypeFieldResolver {

  function getValue($resultitem, $field) {
  
    // First Check if there's a ObjectTypeFieldResolverExtension to implement this field
    $hook_value = $this->getHookValue(GD_DATALOAD_FIELDPROCESSOR_POSTS, $resultitem, $field);
    if (!\PoP\ComponentModel\GeneralUtils::isError($hook_value)) {
      return $hook_value;
    }    

    $cmsresolver = \PoP\CMS\ObjectPropertyResolver_Factory::getInstance();
    $cmsapi = \PoP\CMS\FunctionAPI_Factory::getInstance();
    $post = $resultitem;
    switch ($field) 
    {
      case 'tags' :
        $value = $cmsapi->getCustomPostTags($this->getId($post), [], array('fields' => 'ids'));
        break;

      case 'title' :
        $value = \PoP\CMS\HooksAPI_Factory::getInstance()->applyFilters('the_title', $cmsresolver->getPostTitle($post), $this->getId($post));
        break;
      
      case 'content' :
        $value = $cmsresolver->getPostContent($post);
        $value = \PoP\CMS\HooksAPI_Factory::getInstance()->applyFilters('pop_content_pre', $value, $this->getId($post));
        $value = \PoP\CMS\HooksAPI_Factory::getInstance()->applyFilters('the_content', $value);
        $value = \PoP\CMS\HooksAPI_Factory::getInstance()->applyFilters('pop_content', $value, $this->getId($post));
        break;
    
      case 'url' :

        $value = $cmsapi->getPermalink($this->getId($post));
        break;

      case 'excerpt' :
        $value = $cmsapi->getTheExcerpt($this->getId($post));
        break;

      case 'comments' :
        $query = array(
          'status' => 'approve',
          'type' => 'comment', 
          'post_id' => $this->getId($post),
          'order' =>  'ASC',
          'orderby' => 'comment_date_gmt',
        );
        $comments = $cmsapi->getComments($query);
        $value = array();
        foreach ($comments as $comment) {
          $value[] = $cmsresolver->getCommentID($comment);
        }
        break;
  
      case 'author' :
        $value = $cmsresolver->getPostAuthor($post);
        break;  
      
      default:
        $value = parent::getValue($resultitem, $field);
        break;
    }

    return $value;
  }
}

ObjectTypeFieldResolver还允许通过函数getFieldDefaultDataloader选择用于处理特定字段的默认dataloader。此功能对于通过函数getRelationalComponentFieldNodes切换域是必需的,并决定不明确指定用于加载关系的dataloader,而是使用该字段的默认dataloader。例如,对于帖子字段处理器,它将实现如下

function getFieldDefaultDataloader($field) 
{
  switch ($field) 
  {
    case 'tags' :
      return GD_DATALOADER_TAGLIST;

    case 'comments' :
      return GD_DATALOADER_COMMENTLIST;

    case 'author' :
      return GD_DATALOADER_CONVERTIBLEUSERLIST;																													
  }

  return parent::getFieldDefaultDataloader($field);
}

ObjectTypeFieldResolverExtension

ObjectTypeFieldResolverExtension是一个对象,允许为特定的ObjectTypeFieldResolver解析数据字段,要么覆盖其值,要么扩展它们。例如,它可以在应用级别实现,解析那些应用特定的数据字段。它必须继承自类ObjectTypeFieldResolver_HookBase并实现函数getValue,该函数接收三个参数,$resultitem是数据库对象,$field是要解析的数据字段,$fieldprocessor是钩入的ObjectTypeFieldResolver对象,必须返回应用于数据库对象的该属性值。

例如,一个针对帖子的ObjectTypeFieldResolverExtension可能添加一个自定义的“免责声明”消息,它看起来像这样

class ObjectTypeFieldResolver_Posts_Hook extends \PoP\Engine\ObjectTypeFieldResolver_HookBase {

  function getClassesToAttachTo() {
    
    return array(
      [ObjectTypeFieldResolver::class, ObjectTypeFieldResolver::FIELDPROCESSOR_POSTS],
    );
  }

  function getValue($resultitem, $field, $fieldprocessor) 
  {
    $post = $resultitem;
    switch ($field) 
    {
      case 'disclaimer':
        return \PoP\Engine\MetaManager::getCustomPostMeta($fieldprocessor->getId($post), "disclaimer", true);
    }

    return parent::getValue($resultitem, $field, $fieldprocessor);
  }  
}

过滤数据

通过实现接口DataloadQueryArgsFilter,组件也可以过滤由祖先dataloader组件获取的数据。为此,它们必须实现函数filterDataloadQueryArgs,以过滤某些属性的查询,并实现函数getValue,以提供相应的值。例如,一个执行内容搜索的组件看起来像这样(注意,由于它扩展了TextFormInputsBase,其getValue函数已由FormInputsBase类实现)

class TextFilterInputs extends TextFormInputsBase implements \PoP\ComponentModel\DataloadQueryArgsFilter
{
  public function filterDataloadQueryArgs(array &$query, $component, $value)
  {
    switch ($component->name) 
    {
      case self::COMPONENT_FILTERINPUT_SEARCH:
        $query['search'] = $value;
        break;
    }
  }
}

QueryInputOutputHandler

QueryInputOutputHandler是一个对象,它同步客户端和服务器之间的查询状态。它必须继承自类QueryInputOutputHandlerBase并实现以下函数

在从数据库获取数据之前,函数 prepareQueryArgs 会填充用于传递给数据加载器的 $query_args 对象。它可以从请求中获取值(例如:通过客户端中的应用程序设置)或定义默认值。

从数据库获取数据后,函数 getQueryStategetQueryParamsgetQueryResult,所有这些函数都接收参数 $data_properties、$checkpoint_validation、$executed、$objectIDs,将执行查询的信息发送回客户端:状态值(例如:是否有更多结果?)、参数值(例如:每次应检索多少结果)和结果值(例如:执行是否成功)。

执行器

除了加载数据外,“数据加载”组件还可以发布数据或执行底层CMS支持的操作(登录/注销用户、发送电子邮件、日志记录等)。

为此,组件处理器必须通过函数 getActionExecuterClass 定义组件的 ActionExecuter 对象。

function getActionExecuterClass($component) {
  
  switch ($component->name) {
    case self::COMPONENT_SOMENAME:
  
      return SomeActionExecuter::class;
  }

  return parent::getActionExecuterClass($component);
}

ActionExecuter 是一个执行操作或操作的对象。它必须继承自类 AbstractActionExecuter,并实现函数 execute

function execute(&$data_properties) {
  
  // Execute some operation and return the results
  // ...
  return $results;
}

例如,一个用于注销用户的 ActionExecuter 可能如下所示

class ActionExecuter_Logout extends \PoP\Engine\AbstractActionExecuter {

  function execute(&$data_properties) 
  {
    if ('POST' == \PoP\Root\App::server('REQUEST_METHOD')) { 

      // If the user is not logged in, then return the error
      $vars = \PoP\ComponentModel\Engine_Vars::getVars();
      if (!$vars['global-userstate']['is-user-logged-in']) 
      {
        $error = __('You are not logged in.');
      
        // Return error string
        return array(
          GD_DATALOAD_QUERYHANDLERRESPONSE_ERRORSTRINGS => array($error)
        );
      }

      $cmsapi = \PoP\CMS\FunctionAPI_Factory::getInstance();
      $cmsapi->logout();

      return array(
        GD_DATALOAD_QUERYHANDLERRESPONSE_SUCCESS => true
      );
    }

    return parent::execute($data_properties);
  }
}

存储和重用执行结果

在函数 execute 中获得的结果可以存储供其他对象(组件处理器、ActionExecuter)使用,并基于它们构建逻辑。例如,一个组件可以根据执行的成败决定是否加载数据。

通过从 ActionExecution_Manager 对象中调用函数 setResultgetResult 来存储和访问执行结果。例如,一个用于创建评论的 ActionExecuter 会存储新的评论 ID。

function execute(&$data_properties) 
{
  if ('POST' == \PoP\Root\App::server('REQUEST_METHOD')) 
  {
    // Function getFormData obtains the filled-in values in the form
    $form_data = $this->getFormData();

    $errors = array();
    if (empty($form_data['post_id'])) {
      $errors[] = __('We don\'t know what post the comment is for.');
    }
    if (empty($form_data['comment'])) {
      $errors[] = __('The comment is empty.');
    }    
    if ($errors) 
    {
      return array(
        GD_DATALOAD_QUERYHANDLERRESPONSE_ERRORSTRINGS => $errors
      );
    }

    $cmsapi = \PoP\CMS\FunctionAPI_Factory::getInstance();
    $comment_id = $cmsapi->insertComment($form_data);

    // Save the result
    $actionexecution_manager = \PoP\Engine\ActionExecution_Manager_Factory::getInstance();
    $actionexecution_manager->setResult($this->get_name(), $comment_id);

    // No errors => success
    return array(
      GD_DATALOAD_QUERYHANDLERRESPONSE_SUCCESS => true
    );      
  }

  return parent::execute($data_properties);
}

组件处理器可以通过函数 prepareDataPropertiesAfterMutationExecution 修改它将从数据库中获取哪些数据,该函数在执行组件对应的 ActionExecuter 后被调用。例如,在创建评论后,我们可以立即加载数据,或者如果创建不成功,则指示跳过加载数据库对象。

function prepareDataPropertiesAfterMutationExecution($component, &$props, &$data_properties) {
    
  parent::prepareDataPropertiesAfterMutationExecution($component, $props, $data_properties);

  switch ($component->name) {
    case self::COMPONENT_ADDCOMMENT:

      $actionexecution_manager = \PoP\Engine\ActionExecution_Manager_Factory::getInstance();
      if ($comment_id = $actionexecution_manager->getResult(GD_DATALOAD_ACTIONEXECUTER_ADDCOMMENT)) 
      {
        $data_properties[GD_DATALOAD_QUERYARGS]['include'] = array($comment_id);
      }
      else {

        $data_properties[GD_DATALOAD_SKIPDATALOAD] = true;
      }
      break;
  }
}

检查点

“检查点”是在执行操作访问验证时必须满足的条件。这些验证不包括内容验证,例如检查用户是否正确填写了表单;相反,它们用于确定用户是否可以访问某个页面或功能,例如检查用户是否登录以访问用户账户页面,检查用户 IP 是否被列入白名单以执行特殊脚本等。

组件可以通过组件处理器中的 2 个函数指定它们的检查点

  • getDataAccessCheckpoints:定义组件访问数据的检查点:加载数据或执行组件的动作执行器
  • getActionExecutionCheckpoints:定义执行组件动作执行器的检查点

这两个函数分开的原因是,允许页面只在发布数据时执行验证。然后,“添加帖子”页面在首次加载时可以不需要检查点,这使其可以缓存,并且仅在执行 POST 操作和触发动作执行器时执行验证(例如:用户是否登录?)。

例如,一个需要验证用户 IP 是否被列入白名单的组件可以这样做

function getDataAccessCheckpoints($component, &$props) 
{
  switch ($component->name) {
    case self::COMPONENT_SOMECOMPONENT:
    
      return [CHECKPOINT_WHITELISTEDIP];
  }
  
  return parent::getDataAccessCheckpoints($component, $props);
}

页面也可以通过它们的 设置处理器 分配检查点。每当组件直接与页面相关联(例如:组件 COMPONENT_MYPOSTS_SCROLL 直接关联到 POP_PAGE_MYPOSTS)时,它就会分配与该页面关联的检查点。通过组件处理器中的函数 getRelevantPage 将组件与页面关联,如下所示

function getRelevantPage($component, &$props) {
    
  switch ($component->name) {
    case self::COMPONENT_MYPOSTS_SCROLL:
    case self::COMPONENT_MYPOSTS_CAROUSEL:
    case self::COMPONENT_MYPOSTS_TABLE:

      return POP_PAGE_MYPOSTS;
  }

  return parent::getRelevantPage($component, $props);
}

检查点通过 检查点 解决。

检查点

检查点是一个从类 AbstractCheckpoint 继承的对象,它通过函数 process 处理检查点,判断检查点是否满足条件。当检查点不满足时,必须抛出错误。否则,基类最终会返回 true,表示验证已满足。

例如,验证用户IP是否在白名单中可以像这样实现

class WhitelistedIPCheckpoint extends \PoP\Engine\AbstractCheckpoint {

  function validateCheckpoint() 
  {
    // Validate the user's IP
    $ip = get_client_ip();
    if (!$ip) {          
      return new \PoP\ComponentModel\Error\Error('ipempty');
    }

    $whitelisted_ips = array(...);
    if (!in_array($ip, $whitelisted_ips)) {      
      return new \PoP\ComponentModel\Error\Error('ipincorrect');
    }
  
    return parent::validateCheckpoint();
  }
}

额外URI

将很快添加...

数据结构格式化器

将很快添加...

组件装饰器处理器

将很快添加...

组件过滤器

将很快添加...

上下文变量

它是一个全局变量,位于 PoP_ComponentManager_Vars::$vars 下,通过 PoP_ComponentManager_Vars::getVars 访问,自然地称为 $vars,它存储了处理网页所需的重要信息。在 $vars 中的属性是那些在应用程序中广泛访问的属性,并且当它们的值改变时,会改变组件层次结构。

1. 在应用程序中广泛访问的属性

$vars 充当信息的一个单一、中心存储库,其中属性可以仅计算一次或使用默认值初始化,并通过提供一个独特的地方从应用程序的任何地方检索某个值,从而提高一致性。

例如,属性 output 通过 $_GET["output"] 获取,接受值 "HTML""JSON",通过 $vars['output'] 访问,如果 $_GET["output"] 为空,则初始化为值 "HTML"

2. 当值改变时,会改变组件层次结构的属性

改变某些属性的值将改变组件层次结构。例如,在URL中传递参数 componentFilter=componentpaths 将使组件层次结构仅包括参数 componentpaths[] 下指定的组件。因此,属性 componentFilter 必须位于 $vars 中。

将以下属性保留在 $vars 中的原因如下

1. 计算模型实例ID: modelInstanceId 是表示组件层次结构特定实例的唯一标识符。此ID由函数 ModelInstanceProcessor_Utils::getModelInstanceID() 计算,该函数简单地计算所有改变组件层次结构的属性值的哈希。因为 $vars 中并非所有属性都改变组件层次结构,所以必须通过实现钩子 "ModelInstanceProcessor:model_instance_components" 定义这些属性。

2. 确定入口组件:组件层次结构的顶级组件称为入口组件。每个潜在的入口组件都必须定义一个条件列表,以便对 $vars 进行评估,以满足成为入口组件的条件(更多内容请参阅 PageComponentProcessors)。

3. 解耦处理页面与请求页面:将所有修改组件层次结构的属性存储在 $vars 中,确保在整个应用程序中仅通过 $vars 访问这些属性,然后在 $vars 中直接修改这些值,使得可以操纵响应,例如添加更多数据。这样,可以在单个请求中检索多个页面的内容(用于预先加载视图以在客户端缓存或其它用例),或在一个请求中向许多用户发送个性化的交易电子邮件,等等。

$vars 中设置属性

首次访问时,$vars 使用某些当前请求值初始化,例如

  • 层次结构(首页、单页、页面、作者等)
  • 输出(HTML、JSON等)
  • 组件过滤器(如果有)
  • 混乱的输出?
  • 查询的对象(单层中的帖子对象、作者层次结构中的用户对象等)
  • 其他

插件必须通过实现钩子 "\PoP\ComponentModel\Engine_Vars:add_vars" 添加自己的属性及其对应的值到 $vars。可以随时重置 $vars 并用不同的值填充,例如处理不同的请求。

页面组件处理器

将很快添加...

设置处理器

将很快添加...

组件模型缓存

将很快添加...

PHP版本

要求

  • 开发需使用PHP 8.1+
  • 生产需使用PHP 7.2+

支持的PHP特性

请查看GatoGraphQL/GatoGraphQL支持的PHP特性列表:支持的PHP特性

预览降级到PHP 7.2

通过Rector(干运行模式)

composer preview-code-downgrade

标准

PSR-1,PSR-4 和 PSR-12。PSR-1PSR-4PSR-12

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

composer check-style

要自动修复问题,请运行

composer fix-style

变更日志

请查看CHANGELOG以了解最近有哪些变更。

测试

要执行PHPUnit,请运行

composer test

静态分析

要执行PHPStan,请运行

composer analyse

报告问题

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

贡献

我们欢迎在GatoGraphQL monorepo(该包的源代码托管于此)上对此包的贡献。

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

安全

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

致谢

许可证

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