vanilla/garden-hydrate

基于JSON的数据模板语言。


README

基于JSON的数据模板语言。

CI Packagist Version MIT License CLA

在计算机科学中,数据活化涉及从内存中获取一个不包含任何域数据(“真实”数据)的对象,然后填充它以包含域数据(如来自数据库、网络或文件系统的数据)。数据活化可以采取许多形式,通常需要自定义逻辑,这些逻辑可能重复且编写起来繁琐。介绍 Garden Hydrate。

Garden Hydrate 允许您定义智能但简单的 JSON 数据结构,然后在运行时进行转换。它类似于 JSON 的模板语言。

大多数工作使用 DataHydrator 类完成。您调用 resolve 并提供一个规范和一个可选的参数数组。然后,活化器解析规范,将其转换为结果。

使用 DataHydrator 类

以下是一个超级基本的示例,展示了活化器的工作方式。

$spec = [
    '$hydrate' => 'sprintf',
    'format' => 'Hello %s',
    'args' => [
        'World'
    ]
];

$hydrator = new DataHydrator();
$result = $hydrator->resolve($spec);

// $result will be 'Hello World'

您可以看到特殊的 $hydrate 键。每当活化器看到该键时,它会寻找一个解析器来决定如何解析数组。上面的示例使用了内置的 sprintf 解析器,该解析器仅使用在其他键中提供的参数调用 sprintf()

让我们在上面的示例中进一步使用另一个内置解析器。

$spec = [
    '$hydrate' => 'sprintf',
    'format' => 'Hello %s',
    'args' => [
        [
            '$hydrate' => 'param',
            'ref' => 'who'
        ]
    ]
];

$hydrator = new DataHydrator();
$result = $hydrator->resolve($spec, ['who' => 'Foo']);

// $result will be 'Hello Foo'

param 解析器允许您引用传递给规范的参数。这是利用查询字符串或控制器结果的好方法。如果您想访问嵌套参数,则使用 "/" 字符分隔嵌套键。引用遵循 JSON 引用 标准。

内置解析器

默认情况下,以下解析器与 DataHydrator 类一起提供。

literal

将 literal 值解析到 data 键下。当您想使用保留的 @hyrdrate 键时很有用。

示例

{
  "@hyrdrate": "literal",
  "data": {
    "$hydrate": "literal",
    "data": "Nothing here will get resolved."
  }
}

param

解析到具有 ref 键的参数。 ref 的值应是一个 JSON 引用。

示例

{
  "$hydrate": "param",
  "ref": "path/to/key"
}

ref

解析到当前解析规范中的引用。引用位于 ref 键中,应是一个 JSON 引用。

{
  "$hydrate": "ref",
  "ref": "/path/to/key"
}

您可以使用引用解析到之前活化的值。但是,要注意解析顺序。您不能在规范中引用较晚出现的内容,因为当引用被解析时,它尚未被解析。

sprintf

在节点上调用 sprintf()。节点使用 format 键和 args 键作为函数参数。

示例

{
    "$hydrate": "sprintf",
    "format": "Hello %s",
    "args": [
    	"World"
    ]
}

添加您自己的解析器

DataHydrator 类的内置解析器并不提供很多功能。要真正释放库的强大功能,您将需要添加自己的解析器。为此,请按照以下步骤操作

  1. 创建一个实现 DataResolverInterface 接口的类。您需要实现一个 resolve() 方法。
  2. 如果希望您的解析器在解析之前验证其规范,则可选地实现 ValidatableResolverInterface。您需要实现一个 validate() 方法。这推荐提供良好的开发者体验。
  3. 使用 DataHydrator::addResolver() 注册您的解析器。
  4. 使用与任何其他解析器相同的名称通过 @hyrdate 键引用您的解析器。

示例

让我们举一个例子,其中我们想要一个将字符串转换为小写的 lcase 解析器。Garden Hydrate 提供了一个巧妙的 FunctionResolver 辅助类,帮助您通过反射将任何可调用对象映射到解析器。

use Garden\Hydrate\Resolvers\FunctionResolver;
use \Garden\Hydrate\DataHydrator;

$hydrator = new DataHydrator();
$lcase = new FunctionResolver(function (string $string) {
  return strtolower($string);
});
$hydrator->addResolver($lcase);

$r = $hydrator->resolve([
  '@hydrate' => 'lcase',
  'string' => 'STOP YELLING'
]);
// $r will be "stop yelling"

注意:您也可以直接将 'strtolower' 传递给 FunctionResolver 构造函数,而不是将其封装在闭包中。

处理异常

默认情况下,如果有异常发生,它将被抛出。这意味着单个异常将破坏整个填充过程。这通常不是所希望的,因为您可能希望从异常中恢复以向用户显示有用的消息。

DataHydrator 类允许您通过使用 DataHydrator::setExceptionHandler() 方法注册自己的异常处理程序来完全自定义在填充过程中发生的异常的行为。为此,请按照以下步骤操作

  1. 实现 ExceptionHandlerInterface 来创建您的异常处理程序。
  2. 使用 DataHydrator::setExceptionHandler() 方法注册异常处理程序。
  3. 当发生异常时,您的异常处理程序将被调用,包括导致异常的节点和抛出的异常。然后,您可以返回修正后的数据或重新抛出异常。

示例

通过一个具体的例子可以更好地理解异常处理。假设您的规范代表一个将传递到视图层进行渲染的 widget 系统。每个 widget 都通过具有 widget 类型名称的 $widget 键定义,参数定义在其他键中。

在这种情况下,您将想要渲染成功的 widget,并且只在出现错误的地方显示错误,以防止单个 widget 杀死整个页面。在这种情况下,我们可以创建一个自定义的异常处理程序,用通用错误 widget 代替它,以渲染错误消息。

class WidgetExceptionHandler implements ExceptionHandlerInterface {
	public function handleException(\Throwable $ex, array $data, array $params) {
    if (isset($data['$widget'])) {
      // If we are on a node that represents a widget then replace that widget with an error widget.
      return ['$widget' => 'error-message', 'message' => $ex->getMessage()];
    } else {
      // This isn't a widget node. Best to throw the exception to be caught by a parent widget node.
      throw $ex;
    }
  }
}

在这个例子中,您可以看到您的异常处理程序在每个父节点上被调用,直到处理完异常或用完父节点。这样,您可以决定在哪里以及如何处理异常。

通常,您想要在数据中决定可接受的错误边界并在那里处理异常。上面的 widget 示例是一个非常常见的例子。以下是一些其他示例

  • 也许您正在标记一个 JSON RSS Feed,并确保错误显示为新闻条目,以便该源仍能正确显示。
  • 也许您想实现一个简陋的 GraphQL,其中一个或多个 API 调用由一个 JSON 数组表示。如果某个 API 调用失败,您希望它返回一个内联错误消息来替代 API 结果。您可以在您的 API 客户端中实现此功能,但如果您有多个客户端,则可能希望使用自定义异常处理程序。

中间件

中间件实现目前处于测试阶段,可能会更改。请暂时将其视为不受支持,因为它可能会更改。

中间件是一个重要的功能,它允许您以编程方式控制填充行为,以实现支持缓存、记录、数据转换、调试插件等功能。这些设施可能有任何数量的特定领域实现,因此提供一种添加机制比以可能不适合特定实现的方式添加这些功能要好。

要编写中间件,您创建一个实现 MiddlewareInterface 的类,然后使用 DataHydrator::addMiddleware() 方法注册它。中间件包含一个方法:process()。它将传递一个从您想要处理的数据节点中提取的数据,传递给 resolve() 的参数以及您负责调用的 $next 解析器。

如果您熟悉中间件,那么应该对$next参数很熟悉。如果不熟悉,它将解析数据。它以DataResolverInterface的形式传递,这样您可以控制您的中间件何时执行。

  • 如果您想在解析之前增强数据,请修改$data$params,然后调用$next->resolve()
  • 如果您想增强结果,请在调用$next->resolve()后让您的中间件执行其功能。
  • 如果您想在不处理节点的情况下做些事情,则根本不要调用$next->resolve()。这是缓存通常实现的常见方式。

有时您的中间件在实例化时全局配置,有时您希望根据传递给转换的数据来配置它。如果您想在数据上配置中间件,则应从数据的$middleware键中读取中间件。惯例是您定义一个带有您中间件名称的键,然后在那里放置参数。

{
  "$middleware": {
    "middleware-name": {"param1":  "value1", "param2": "Value2", /* ... */ },
    // ...
  }
}

您的中间件将负责读取其配置并根据它采取行动。如果它不适用于节点,则只需返回$next->resolve()

中间件是一个非常强大的范式,可以为hydrate添加很多功能。只是要小心,您的中间件要健壮。它通常总是调用$next->resolve()并返回该结果,除非您明确不想这样做。如果您不调用$next->resolve(),则节点根本不会解析。

transform 中间件

transform 中间件用于使用Garden JSONT规范转换节点上的解析数据。您可以这样应用它

{
  "$middleware": {
    "transform": { "key": "json ref", /* ... */ },
  }
}

这是一种整理略微不符合规范API输出的便捷方式,以匹配标准格式。目前,您不能在$middleware键中使用$hydrate关键字,但如果有充分的理由,我可以被说服放宽这一限制;)

案例研究

以下是一些案例研究,以说明Garden Hydrate最有可能是使用情况。

数据本地化

假设您正在提供一些将显示给用户的静态字符串。您可能希望添加根据用户选择的区域设置翻译这些字符串的能力。您可以通过注册自己的翻译解析器来实现这一点。

{
  "$hydrate": "translate",
  "string": "Translation code"
}

让我们看看一个基本示例。

{
  "title": {
    "$hydrate": "translate",
    "string": "Hello World"
  }
}

读取配置或用户偏好

假设您有一些数据或参数依赖于配置设置或用户偏好。您可以添加一个解析器,以便读取这些设置,以便将它们用于其他目的。

警告!如果未正确权限门控,则配置设置的连接将很可能导致安全漏洞。考虑将允许的设置嵌套在单个键下或使用某些元设施以确保敏感信息不会被暴露。

实用函数

您可能想要连接一串助手和实用函数。也许您想要访问PHP标准库的更多部分,或者也许您想为您的hydrate系统添加一些特定领域的功能。

穷人的GraphQL

假设我们想要实现将多个API调用包装成一个调用的能力,以减少客户端和服务器之间的往返次数。您决定添加一个POST /hydrate端点,它接受Garden Hydrate规范并返回结果。在这种情况下,您将希望将您的内部分发器连接到一个解析器。让我们看看规范可能是什么样子

{
  "$hydrate": "api",
  "path": "/resource/path",
  "query": {}
}

让我们看看这在实际中可能是什么样子

{
  "discussion": {
    "$hydrate": "api",
    "path": "/discussions/123"
  },
  "comments": {
    "$hydrate": "api",
    "path": "/comments",
    "query": {
      "discussionID": 123
    }
  }
}

如果您考虑将此实现与上面的一些其他案例研究结合使用,您真的可以体验到使用不同组合和嵌套水合规范所能达到的力量和灵活性。连接您现有的RESTful API将为您提供

注意:我们建议最初仅在该端点支持GET请求。