getpop/component-model-configuration

将配置层添加到组件模型中

1.0.6 2023-09-07 09:15 UTC

README

将配置级别添加到组件层次结构中,通过它可以扩展数据API到应用程序

安装

通过Composer

composer require getpop/component-model-configuration

开发

源代码托管在GatoGraphQL monorepo中,位于SiteBuilder/packages/component-model-configuration

用法

初始化组件

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

架构设计和实现

配置

在函数下添加配置值

  • function getImmutableConfiguration($component, &$props)
  • function getMutableonmodelConfiguration($component, &$props)
  • function getMutableonrequestConfiguration($component, &$props)

例如

// Implement the components properties ...
function getImmutableConfiguration($component, &$props) 
{
  $ret = parent::getImmutableConfiguration($component, $props);

  switch ($component->name) {
    case self::COMPONENT_SOMENAME:
      $ret['description'] = __('Some description');
      $ret['classes']['description'] = 'jumbotron';
      break;
  }

  return $ret;
}

请注意,配置接收$props参数,因此可以打印通过props设置的配置值。通过initModelProps初始化immutablemutable on model配置值,而通过initRequestProps初始化mutable on request配置值。

// Implement the components properties ...
function getImmutableConfiguration($component, &$props) 
{
  $ret = parent::getImmutableConfiguration($component, $props);

  switch ($component->name) {
    case self::COMPONENT_SOMENAME:
      $ret['showmore'] = $this->getProp($component, $props, 'showmore');
      $ret['class'] = $this->getProp($component, $props, 'class');
      break;
  }

  return $ret;
}

function initModelProps($component, &$props) 
{
  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);
}

客户端缓存

为了在客户端缓存所有组件的配置,并使其可重用,我们只需将所有响应深度合并在一起。例如,如果第一个请求带来了这个响应

{
  "component1": {
    configuration: {
      class: "topcomponent"
    },
    components: {
      "component2": {
        configuration: {
          class: "some-class"
        }
      }
    }
  }
}

并且第二个响应带来了这个响应

{
  "component3": {
    configuration: {
      class: "topcomponent"
    },
    components: {
      "component4": {
        configuration: {
          class: "another-class"
        }
      }
    }
  }
}

然后深度合并响应将得到

{
  "component1": {
    configuration: {
      class: "topcomponent"
    },
    components: {
      "component2": {
        configuration: {
          class: "some-class"
        }
      }
    }
  },
  "component3": {
    configuration: {
      class: "topcomponent"
    },
    components: {
      "component4": {
        configuration: {
          class: "another-class"
        }
      }
    }
  }
}

然后我们可以完美地重用从"component1"开始的配置以重新打印第一个请求,以及从"component3"开始的配置以重新打印第二个请求。

这很简单,但是从现在开始事情变得更复杂。如果组件的后代不是静态的,而是可以依赖于上下文而改变,比如请求的URL或其他输入,会发生什么?例如,我们可能有一个名为"single-post"的组件,根据请求对象的帖子类型改变其子组件,选择"layout-post"或"layout-event"组件,这样组件层次结构就会从这样

"single-post"
  components
    "layout-post"

变成这样

"single-post"
  components
    "layout-event"

同样,即使在同一组件层次结构中,组件的属性值也可能因不同的URL而变化。例如,一个名为"post-layout"的组件可以有一个值为"post-{id}"的属性"class",其中"{id}"是请求的帖子的id,这样我们就可以为特定的帖子添加样式,例如.post-37 { background-color: red; }.post-224 { background-color: green; }。然后,id为37和224的帖子,尽管它们具有相同的组件层次结构,但它们的配置将交替出现

"single-post"
  components
    "layout-post"
      configuration
        class: "post-37"

变成这样

"single-post"
  components
    "layout-post"
      configuration
        class: "post-224"

让我们来看看上述两种情况在深度合并结果时会发生什么。例如,如果第一个请求带来了这个响应

{
  "component1": {
    configuration: {
      class: "topcomponent"
    },
    components: {
      "component2": {
        configuration: {
          class: "some-class"
        }
      }
    }
  }
}

并且第二个响应带来了这个响应

{
  "component1": {
    configuration: {
      class: "topcomponent"
    },
    components: {
      "component3": {
        configuration: {
          class: "another-class"
        }
      }
    }
  }
}

然后深度合并响应将得到

{
  "component1": {
    configuration: {
      class: "topcomponent"
    },
    components: {
      "component2": {
        configuration: {
          class: "some-class"
        }
      },
      "component3": {
        configuration: {
          class: "another-class"
        }
      }
    }
  }
}

如我们所见,合并第二个请求的响应后,相同的数据(class: "topcomponent")没有影响合并后的对象,新的信息被附加到现有对象中,但没有覆盖任何数据。然而,原始的第一个响应中只有"component1"具有子组件"component2",但在合并后,"component1"有两个子组件,"component2"和"component3"。然后,如果再次加载第一个响应的URL并重用缓存的配置,下面"component1"将打印"component2"和"component3"而不是应该只打印"component2"。

为了解决这个问题,可以在配置中显式地添加一个“descendants”属性,以声明其子组件,这样就可以知道哪些组件必须被渲染,忽略其余部分,尽管它们的数据仍然是合并后的JSON对象的一部分。然后,第一和第二次响应将如下所示

{
  "component1": {
    configuration: {
      class: "topcomponent",
      descendants: ["component2"]
    },
    components: {
      "component2": {
        configuration: {
          class: "some-class"
        }
      }
    }
  }
}

{
  "component1": {
    configuration: {
      class: "topcomponent",
      descendants: ["component3"]
    },
    components: {
      "component3": {
        configuration: {
          class: "another-class"
        }
      }
    }
  }
}

合并后的配置将如下所示

{
  "component1": {
    configuration: {
      class: "topcomponent",
      descendants: ["component3"]
    },
    components: {
      "component2": {
        configuration: {
          class: "some-class"
        }
      },
      "component3": {
        configuration: {
          class: "another-class"
        }
      }
    }
  }
}

但是,现在缓存的“descendants”属性值已经被第二次响应的值覆盖,带来了之前提到的第二个问题关于不同的属性值。然后,如果再次加载第一个响应的URL并重用缓存的配置,它将在“component1”下打印“component3”而不是应该的“component2”。

关于不同属性的问题源于配置值不仅根据组件层次结构设置,还根据请求的URL设置。例如,以下组件层次结构

"single-post"
  components
    "layout-post"

可以产生以下两种不同的配置输出

"single-post"
  components
    "layout-post"
      configuration
        class: "post-37"

"single-post"
  components
    "layout-post"
      configuration
        class: "post-224"

解决方案是在不同请求中深度合并配置,不覆盖不同的属性,无论是在组件层次结构还是URL级别,将配置分成3个单独的部分:“immutable”(不可变)、“mutableonmodel”(其中“model”相当于“组件层次结构”)和“mutableonrequest”。配置中的每个属性必须放在这三个部分中的 exactly 1 个部分,如下所示

  • immutable: 包含永远不会改变的属性,例如 class: "topcomponent"
  • mutableonmodel: 包含可以根据组件层次结构更改的属性,例如 descendants: ["component2"]
  • mutableonrequest: 包含可以根据请求的URL更改的属性,例如 class: "post-37"

按照这个方案,第一次请求可能产生以下响应

{
  immutable: {
    "single-post": {
      configuration: {
        class: "topcomponent"
      }
    }
  },
  mutableonmodel: {
    "single-post": {
      configuration: {
        descendants: ["layout-post"]
      }
    }
  },
  mutableonrequest: {
    "single-post": {
      components: {
        "layout-post": {
          configuration: {
            class: "post-37"
          }
        }
      }
    }
  }
}

如我们所见,由于来自三个部分的属性没有重叠,因此将请求的三个部分合并会产生完整的配置

{
  "single-post": {
    configuration: {
      class: "topcomponent",
      descendants: ["layout-post"]
    },
    components: {
      "layout-post": {
        configuration: {
          class: "post-37"
        }
      }
    }
  }
}

接下来,客户端的缓存被保持为3个单独的对象,每个对象对应一个子部分,其中“mutableonmodel”和“mutableonrequest”在适当的键下存储其数据:“mutableonmodel”在名为“modelInstanceId”的键下,它代表组件层次结构的散列,而“mutableonrequest”在请求的URL下。上面的请求将像这样被缓存(假设“modelInstanceId”的值为“bwKtq*8H”,URL为“/posts/some-post/”)

immutable => 
  {
    "single-post": {
      configuration: {
        class: "topcomponent"
      }
    }
  }

mutableonmodel => 
  {
    "bwKtq*8H": {
      "single-post": {
        configuration: {
          descendants: ["layout-post"]
        }
      }
    }
  }

mutableonrequest => 
  {
    "/posts/some-post/": {
      "single-post": {
        components: {
          "layout-post": {
            configuration: {
              class: "post-37"
            }
          }
        }
      }
    }
  }

如果随后我们获得第二次请求的响应,缓存将更新如下(假设“modelInstanceId”的值为“6C7Lu$\3”,URL为“/posts/some-event/”)

immutable => 
  {
    "single-post": {
      configuration: {
        class: "topcomponent"
      }
    }
  }

mutableonmodel => 
  {
    "bwKtq*8H": {
      "single-post": {
        configuration: {
          descendants: ["layout-post"]
        }
      }
    },
    "6C7Lu$\3": {
      "single-post": {
        configuration: {
          descendants: ["layout-event"]
        }
      }
    }
  }

mutableonrequest => 
  {
    "/posts/some-post/": {
      "single-post": {
        components: {
          "layout-post": {
            configuration: {
              class: "post-37"
            }
          }
        }
      }
    },
    "/posts/some-event/": {
      "single-post": {
        components: {
          "layout-event": {
            configuration: {
              class: "post-45"
            }
          }
        }
      }
    }
  }

如我们所见,“immutable”包含结构的共同部分,而“mutableonmodel”和“mutableonrequest”包含差异。因此,此方案识别共同数据并只存储一次,并且所有不同的条目都分别存储和可访问。如果在组件层次结构中大部分配置没有变化,那么在“immutable”下存储的信息将是存储信息的主体,从而成功减少缓存的数据量。

最后,对于任何请求的“modelInstanceId”和URL,我们可以从三个部分获得三个单独的分支,并将它们全部合并在一起,从缓存中重新创建整个配置。

合并也可以在服务器端进行:如果不需要在客户端缓存配置,则可以通过将参数 dataoutputmode=combined 添加到URL中来避免处理三个子部分的额外复杂性。

PHP版本

要求

  • PHP 8.1+ 用于开发
  • PHP 7.2+ 用于生产

支持的PHP特性

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

预览降级到PHP 7.2

通过 Rector(dry-run 模式)

composer preview-code-downgrade

标准

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(或更高版本)。有关更多信息,请参阅 许可文件