romanko/object-graph

该软件包已被废弃,不再维护。没有建议的替代包。

一个用于与 JSON 对象一起使用的数据映射器,它允许将不同版本的负载塑形为具有预定义属性集的值对象

v1.0.0 2018-05-28 04:07 UTC

This package is not auto-updated.

Last update: 2021-10-06 05:27:37 UTC


README

Minimum PHP Version Build Status Code Coverage

介绍

Object Graph 包装了一个普通的 PHP 对象(例如,JSON 对象),并公开了一个具有预定义属性集的值对象(一个 GraphNode 实例)。

Object Graph 还可用于在 JSON 负载的不同版本之间引入兼容性,并生成具有适合这两个负载的常见属性集的 GraphNode。

这个库是从最初为 NewsCorp Australia 开发的项目的 ground-up 版本编写的。感谢 Juan Zapata (@juankk),Salvatore Balzano (@salvo1404) 和 Michael Chan (@michaelChanNews) 在这个库上的时间和贡献。

目录

概述

让我们直接进入正题。假设我们有来自我们的 User API 的两个版本的负载

/v1/user/123

{
  "userName": "Arnold Schwarzenegger",
  "dob": "1947-07-30",
  "emailAddress": "arnold.schwarzenegger@gov.ca.gov"
}

/v2/user/123

{
  "firstName": "Arnold",
  "lastName": "Schwarzenegger",
  "dateOfBirth": "1947-07-30",
  "email": "arnold.schwarzenegger@gov.ca.gov"
}

让我们创建一个具有此属性集的 User 模型,为每个负载版本构建一个架构,最后构建一个解析器,该解析器将自动将相应的架构应用于特定的负载版本,并为我们生成一个 User 模型。

  • 模型:一个 GraphNode 的实例,它表示一个普通的 PHP 对象,其中对象字段定义为模型属性;
  • 架构:包含模型属性/字段列表;解析器、默认值、每个字段的 PHP 类型转换配置以及更多内容;
  • 解析器:执行魔法并将模型从普通 PHP 对象构建出来;
  • 上下文:允许在字段解析器之间共享变量。

这些都是 ObjectGraph 库的主要组件。

模型

首先,我们从确定模型应该具有哪些属性开始建模

<?php
/**
 * @property string   $firstName
 * @property string   $lastName
 * @property string   $fullName
 * @property DateTime $dateOfBirth
 * @property string   $email
 * @property string   $schema      Payload version
 */
class User extends GraphNode
{
    const SCHEMA_V1 = 'v1';
    const SCHEMA_V2 = 'v2';
}

模型类必须扩展 GraphNode 并在类头文档块中列出属性。这将启用 IDE 自动建议。

注意,在本版本中,模型是不可变的,尝试在模型上设置或删除值将抛出异常。

您的模型类可以扩展另一个模型类,也可以包含 API、常量,就像普通的 PHP 类一样,它确实就是这样。

模型属性可以通过 PHP 对象属性 $user->dateOfBirth 或作为数组元素 $article['heraldsun.com.au']->titleOverride 访问。

模型 API

  • Model::getData(),返回原始的底层普通 PHP 对象
  • Model::asArray(),将模型转换为数组,根据 Schema 中定义的字段;
  • Model::asObject(),将模型转换为对象,根据 Schema 中定义的字段;

架构

下一步,我们将为每个有效载荷版本定义一个模式。一个模式类必须扩展Schema

Schema中有一些有用的方法,您可以选择或可能希望重写。

  • Schema::getGraphNodeClassName(),此方法必须返回一个模型类名,在我们的例子中是User
  • Schema::isStrict(),必须返回一个布尔标志,指示此模式是否严格。见下文。
  • Schema::build(),添加模式字段和可能的其他配置工作必须发生在此方法中,它作为Schema类的自定义类__constructor
严格模式

当模式是严格的,那么您只能访问在Scheme实例上定义的模型字段。尝试访问存在于数据源上但未在模式上定义的字段将返回NULL。

当将模型转换为数组或对象时,结果数据将只包含在严格模式上定义的字段。如果模式不是严格的,则结果数据将包含在源对象上定义的所有字段和模式上的字段的组合。

默认情况下,Schema 不是严格的

版本 1

<?php
class UserSchemaV1 extends Schema
{
    public function getGraphNodeClassName(): string
    {
        return User::class; // the Resolver must use User model class
    }

    public function isStrict(): bool
    {
        return true; // this is a strict schema
    }

    protected function build(SchemaBuilder $schema)
    {

        /**
         * Use $schema to define fields: addField() returns an instance of the FieldBulder class
         */

        $schema->addField('firstName')->withResolver(function (stdClass $data) {
            if (empty($data->userName)) {
                return null;
            }

            $name = preg_split('/\s+/', $data->userName, 2);

            return $name[0];
        });

        /**
         * Field resolver function is the most powerful way to extract data from the data source.
         */

        $schema->addField('lastName')->withResolver(function (stdClass $data) {
            if (empty($data->userName)) {
                return null;
            }

            $name = preg_split('/\s+/', $data->userName, 2);

            return (sizeof($name) === 2 ? $name[1] : null);
        });

        /**
         * Defining a field aliase allows to avoid using resolver. This code below tells that, 
         * there is a Model property "fullName", which must receive data from the source object 
         * field named "userName".
         */

        $schema->addField('fullName')->asAliasOf('userName');

        /**
         * Additionally, a type of resulting value can be specified on a field
         */

        $schema->addField('dateOfBirth')->asAliasOf('dob')->asScalarValue(ScalarType::DATE_TIME);
        $schema->addField('email')->asAliasOf('emailAddress');

        /**
         * You can specify a default value for an existing field or define a "virtual" 
         * field with the default value.
         */

        $schema->addField('schema')->withDefaultValue(User::SCHEMA_V1);
    }
}

版本 2

<?php
class UserSchemaV2 extends Schema
{
    public function getGraphNodeClassName(): string
    {
        return User::class;  // the Resolver use User model class
    }

    public function isStrict(): bool
    {
        return true; // this is a strict schema
    }

    protected function build(SchemaBuilder $schema)
    {

        /**
         * If source object field name and Model property name match and you are happy with 
         * the type of the source value, this is what you only need to register a field.
         */

        $schema->addField('firstName');
        $schema->addField('lastName');

        $schema->addField('fullName')->withResolver(function (stdClass $data) {
            if (empty($data->firstName) || empty($data->lastName)) {
                return null;
            }

            return sprintf('%s %s', $data->firstName, $data->lastName);
        });

        $schema->addField('dateOfBirth')->asScalarValue(ScalarType::DATE_TIME);
        $schema->addField('email');
        $schema->addField('schema')->withDefaultValue(User::SCHEMA_V2);
    }
}
关于嵌套对象和字段解析器的一番话

字段解析器函数接收的第一个参数总是字段所属的父对象。

例如,有一个源PHP对象

User {
  "social": SocialIntegration {
    "facebook" { ... }
  }
}
  • social字段解析器函数将接收到根对象;
  • facebook字段解析器函数将接收到分配给social的对象

每个嵌套对象都可以有自己的Schema,它可以定义一个自定义模型类来使用。

解析器

实际上我们可以立即开始使用上述内容

$resolver = new Resolver();

$model1 = $resolver->resolveObject($userPayloadV1, UserSchemaV1::class);
$model2 = $resolver->resolveObject($userPayloadV2, UserSchemaV2::class);

echo $model1->firstName; // outputs "Arnold"
echo $model2->firstName; // outputs "Arnold"

但是,这并不有趣,因为我们仍然需要决定使用哪种模式与数据。让我们自动化它

<?php
class UserResolver extends Resolver
{

    /**
     * Let's override Resolver::resolveObject() in our own resolver and
     * make it inspect the raw source object to decide which schema version 
     * to use
     */
  
    public function resolveObject(
        stdClass $data = null,
        string $schemaClassName = null,
        Context $context = null
    ): GraphNode {
        if (empty($data)) {
            return null;
        }

        switch (true) {
            case (
                  isset($data->fullName) || 
                  isset($data->dob) || 
                  isset($data->emailAddress)
            ): // it is definitely a v1 User payload
                $schemaClassName = UserSchemaV1::class; 
                break;

            case (
                (isset($data->firstName) && isset($data->lastName)) ||
                isset($data->emailName) ||
                isset($data->dateOfBirth)
            ): // it is clearly a v2 User payload
                $schemaClassName = UserSchemaV2::class; 
                break;

            default:
                throw new ObjectGraphException('Unable to detect schema from the user data object');
        }

        /**
         * Call parent::resolveObject() with a Schema class we have just detected
         */

        return parent::resolveObject($data, $schemaClassName, $context);
    }
}

现在尝试一下

$resolver = new Resolver();
$model    = $resolver->resolveObject($anyUserPayload);

echo $model->firstName; // Woo Hoo!! It still outputs "Arnold"!

上下文

上下文用于将外部变量传递给字段解析器

  • 根对象将接收到全局上下文的一个副本;
  • 其嵌套对象将接收到根对象上下文的一个副本

注意:上下文是为了包含标量变量而设计的,不支持深度克隆。如果上下文中存储了对象,那么当上下文被克隆时,克隆的上下文和原始上下文将保留对彼此的引用。

有一个包含特定语言问候语的JSON文件,在我们的对象图模型中,我们希望在可能的情况下使用澳大利亚英语

{
  "greetings": {
    "en-us": "Hi! How are you doing?",
    "en-au": "G'day! How are you going? "
  }
}

所需的区域设置可以设置在上下文中,然后在字段解析器中访问

<?php

$context = new Context();
$context['locale'] = 'en-au';

$og = new ObjectGraph($context);
$model = $og->resolveObject($json);

echo $model->sayHi;

并且在“sayHi”字段解析器中

<?php
// defining new field and its resolver in Schema
$schema
  ->addField("sayHi")
  ->withResolver(function(stdClass $data, Context $context) {
    $locale = $context['locale']; // get locale from the Context
    $fallbackLocale = 'en-us';

    return (isset($data->greetings->$locale) ? 
      $data->greetings->$locale : 
      $data->greetings->$fallbackLocale);
  });