happycode/blueprint

用于描述、读取和格式化数据的工具。

v1.1.1 2023-11-25 22:04 UTC

This package is auto-updated.

Last update: 2024-09-25 23:59:24 UTC


README

一个用于定义、验证、读取和转换第三方数据的PHP库。

问题

[免责声明] - 目前仅支持JSON。其他功能即将推出!

好吧,这是一件很常见的事情。你的应用程序与某个地方的API进行通信。

你从第三方请求中得到了一些JSON,它似乎包含了你所需的所有内容(某处),但现在你必须弄清楚如何处理它。

你有两种选择

  • 使用json_decode并开始传递结果。 - 这样你就在应用程序的每个角落都散布了这种疯狂。
  • 开始构建自定义对象并加载数据 - 然后大约一个月后,有人问为什么有一个包含200个新文件的PR,你几乎因为无聊而死。

哦不,不,不,NO!!

蓝图

总结;

  • 定义数据形状
  • 验证输入
  • 应用转换
  • 以你想要的方式生成访问对象(模型)
  • 将数据渲染回你自定义的/期望的JSON

蓝图允许我们定义我们期望的数据,就像数据库中的表一样。我们告诉它JSON的模式(形状和类型),我们告诉它如何进行验证,我们的规则,不是他们的,我们过滤掉我们不想要的,以及如何改变我们不喜欢的部分。

然后我们提供数据。

验证是隐式的,并提供详细的错误信息。

它返回智能对象,安全的数据,以我们的应用程序所需的方式进行结构化和格式化。

让我们看看...

安装

composer require happycode/blueprint

用法

假设我们从第三方API得到这个结果 - 它在一个名为$json的变量中

  {
    "id": 1,
    "firstName": "Roger",
    "lastName": "Rabbit",
  }

让我们尝试定义它。

    use HappyCode\Blueprint\Model;

    $userSchema = Model::Define('User', [
        "id" => 'integer',
        "firstName" => 'string',
        "lastName" => 'string',
    ])

现在我们可以添加一些数据

    $user = $userSchema->adapt($json);

顺便说一句 - 那里就是验证发生的地方!所以现在...

    echo $user->getFirstName();

会给你你想象中的结果。

太棒了!- 让我们进一步了解。

更多帮助

类型

当我们指定一个字段...

    $userSchema = Model::Define('User', [
        "fieldName" => 'string',
    ])

它实际上是一个简写,表示

    $userSchema = Model::Define('User', [
        "fieldName" => Type::String(),
    ])

Type类使我们能够进行更复杂的配置。

可用的原始类型有Type::String() Type::Int() Type::Float() Type::Boolean()

元数据

所有类型都有一些相关的元数据,特别是以下布尔值

  • isNullable - 当为true时,字段允许null
  • isRequired - 当为true时,如果字段不存在,将发生验证错误
  • isHidden - 当为true时,所需字段不会在转换数据中渲染(稍后会有更多介绍),它们可以设置为以下内容
    $userSchema = Model::Define('User', [
        "fieldName" => Type::String(isNullable: true, isRequired: false, isHidden: false),
    ])

默认值如下(x表示无法设置的属性)

枚举

枚举值 - 只接受匹配指定集合的值

    $userSchema = Model::Define('User', [
        "status" => Type::Enum(values: ["PENDING", "ACTIVE", "DISABLED"]),
    ])

日期时间

日期和时间是行内转换的常见用例,因此我们能够创建PHP日期格式规范进行读取和渲染。示例

    $userSchema = Model::Define('User', [
        "status" => Type::DateTime(inputFormat: 'd-m-Y', outputFormat: 'm/d/Y'),
    ])

默认值为

  • 输入格式 - 'd/m/y H:i:s'
  • 输出格式 - 任何输入格式

数组Of

假设我们正在查看这种类型的JSON

  {
    "lotteryNumbers": [1,2,3,4,5,6]
  }

因为它可以描述为“基本类型数组”,所以我们可以这样建模...

    $userSchema = Model::Define('User', [
        "lotteryNumbers" => Type::ArrayOf(Type::Int()),
    ])
    // or with shorthand
    $userSchema = Model::Define('User', [
        "lotteryNumbers" => 'int[]'
    ])

非原生日型(自定义对象)

是的,嵌套结构也可以进行类型定义... 这里有一个例子

  {
    "geoLocation": {
      "lat": "84.9999572",
      "long": "-135.000413,21"
    }
  }

我们可以动态创建一个自定义(子)模型

    $mapPinSchema = Model::Define('MapPin', [
        "geoLocation" => Type::Model(
            Model::Define('GeoLocation', [
                  "lat" => Type::String(),
                  "long" => Type::String()
            ])
        ),
    ])

尽管如果需要重复使用对象,你总是可以使其更具可重用性

  {
    "pickupLocation": {
      "lat": "84.9999572",
      "long": "-135.000413,21"
    },
    "dropLocation": {
      "lat": "49.4296032",
      "long": "0.737196,7"
    }
  }

例如...

    $geoLocationSchema = Model::Define('GeoLocation', [
          "lat" => Type::String(),
          "long" => Type::String()
    ]);
    $deliverySchema = Model::Define('Delivery', [
        "pickupLocation" => Type::Model($geoLocationSchema),
        "dropLocation" => Type::Model($geoLocationSchema),
    ])

集合

好的 - 让我们结合自定义对象和数组 - 在蓝图(Blueprint)中,一组自定义对象(模型模式)被称为集合

使用来自自定义对象示例(上面)的 $geoLocationSchema

    $geoLocationSchema = Model::Define('GeoLocation', [
          "lat" => Type::String(),
          "long" => Type::String()
    ]);
    
    $deliverySchema = Model::Define('Delivery', [
        "journeyTracking" => Type::Collection($geoLocationSchema),
    ])

允许像 json 一样

  {
    "journeyTracking": [
       { "lat": "84.9999572", "long": "-135.000413,21" },
       { "lat": "49.4296032", "long": "0.737196,7" },
    ]
  }

根集合

有时 JSON 将在根级别有一个数组而不是对象 - 这是有效的,令人讨厌,但很常见。

[
 {
  "name": "Roger"
 },
 {
  "name": "Jessica"
 }
]

很容易完成...

    $rabbitSchema = Model::Define('Rabbit', [
          "name" => Type::String()
    ]);
    
    $loadsOfRabbitsSchema = Model::CollectionOf($rabbitSchema)

虚拟字段(转换)

假设你想要一个不存在的字段,并且你可以从已有的数据中构建它。例如

{
 "first": "Roger",
 "last": "Rabbit"
}

你难道不希望有一个 fullName 字段吗?好吧...

    $rabbitSchema = Model::Define('Rabbit', [
          "first" => Type::String(isHidden: true),
          "last" => Type::String(isHidden: true),
          "fullName" => Type::Virtual(function($rabbit) {
                return $rabbit['first'] . ' ' . $rabbit['last'];
          }),
    ]);

传递给 Type::Virtual() 方法的函数将有一个关联数组,其中包含所有已解码的属性(包括隐藏的)和来自输入 JSON 的值。

唯一的要求是任何被使用的字段都必须在模式中存在(显然)。

顺便说一句,这就是我们可能想要使用 isHidden 隐藏字段的原因 - 当我们到达 渲染 充水模型时,我们可能不希望它们可见。

简写

只要你不改变默认值,原生日型都有简写符号,以防你发现它更易读。

    $schema = Model::Define('Thing', [
          "name" => Type::String(),
          "weight" => Type::Int(),
          "lotteryNumbers" => Type::ArrayOf(Type::Int())
    ]);
// is equivalent to
    $schema = Model::Thing([
          "name" => 'string',           // or 'text'
          "rich" => 'bool',             // or 'boolean'
          "weight" => 'float',          // 'double' or 'decimal' also work
          "lotteryNumbers" => 'int[]'   // works for all primitive types
    ]);

适应(充水)

呼呼,老虎!!! - 你有一个模式!做得好!

让我们

    $model = $schema->adapt($jsonString);
  1. 这里发生的第一件事是 验证 - 蓝图知道它需要从 JSON 中得到什么,所以它会确保它在那里。
  2. 第二件事是 过滤 - 如果 JSON 中有数据,但你的模式在任何地方都没有定义它,它将被忽略。
  3. 最后,它返回一个代表你所需要数据的模型,

看看这个...

    $user = (Model::Define('User', [ 'name' => 'string' ])->adapt('{ "name": "Roger" }'));

    echo $user->getName(); // Roger

渲染

现在这个

    $user = (Model::Define('User', ['name' => 'string']))->adapt('{ "name": "Roger" }');

    echo $user->json(); // { "name": "Roger" }

这有意义吗? - 还是说它有意义?

    $spy = (Model::Define('Spy', [
        "first" => Type::String(isHidden: true),
        "last" => Type::String(isHidden: true),
        "fullName" => Type::Virtual(function($who) {
                return sprintf("%s, %s %s!", $who['last'], $who['first'], $who['last']);
        }),
    ])->adapt('{ "first": "James", "last": "Bond" }'));

    echo $spy->json(); // A string = {"fullName":"Bond, James Bond!"}

注意 渲染后的 JSON 不包括隐藏字段 firstlast

异常

在运行时,blueprint 将抛出各种异常,在所有情况下,它们都将扩展 HappyCode\Blueprint\Error\BlueprintError 异常。

像下面这样包装适应方法可以保证封装所有情况

try {
    $user = $userSchema->adapt('{ "name": "Roger" }');
} catch (\HappyCode\Blueprint\Error\BlueprintError $e) {
    // handle this
}

以获得更精细的控制

try {
    $user = $userSchema->adapt('{ "name": "Roger" }');
} catch (\HappyCode\Blueprint\Error\BuildError $e) {
    // Type Help code generator errors
} catch (\HappyCode\Blueprint\Error\ValidationError $e) {
    // Validation based issues while parsing the data
    // $e->getMessage() - will be helpful
}

IDE 和类型帮助

当蓝图解析了输入 JSON($schema->adapt($json))后 - 你可能会注意到模型中存在一个问题。

你的 IDE(PHPStorm / Visual Studio / 任何东西) - 无法提供任何智能感知,这是一个问题!由于数据模型是在运行时创建的,你的编辑器无法对它们执行任何静态分析。这意味着你可能会在编写完全合法的代码如 $model->getId() 时得到一些波浪线。

为了解决这个问题,我们有一个解决方案 - 这不是 必要的,但我们都喜欢没有波浪线的文件,对吧!

在你的模式定义上添加 ->setHelperNamespace(<NS>) 并提供一个类型帮助器的命名空间。在第一次运行时,适配器将为你的项目中的每个模型生成一个文件。

你提供的命名空间将映射到你项目目录中的一个文件路径

如果你的 composer.json 文件有任何 psr-4 映射 - 它将遵守这些映射 - 否则,映射将从项目根目录开始。(Composer 文件所在的位置)

示例

- /<project_root>
  - /src
    - index.php
  - /lib
   - helper.php
 - composer.json

假设你的 composer.json autoload 设置看起来像。

{
 "autoload": {
  "psr-4": {
   "App\\": "src/"
  }
 }
}

在你的代码中

    $userSchema = Model::Define('User', [
        "name" => 'string',
    ])->setHelperNamespace("App\TypeHintThing");

    $user = $userSchema->adapt('{ "name": "Roger" }');

在第一次运行这个之后 - 你会注意到在你的项目中有一个或多个新文件

- /<project_root>
    - /src
        - index.php
        - /TypeHintThing
          - ...?
          - UserModel.php
    - /lib
    - helper.php
- composer.json

你可以使用这些文件在你的代码中告诉你的 IDE 关于模型类型。

    /** @var \App\TypeHintThing\UserModel $user */
    $user = $userSchema->adapt('{ "name": "Roger" }');

重要提示:

一旦生成这些文件,它们将不会被重新创建——即使您更新了模式,您也应该删除辅助文件,并允许它们重新生成。

版权和许可

happycode/blueprint 库的版权为 © Paul Rooney 所有,并按照 MIT 许可协议(MIT)授权使用。请参阅 LICENSE 了解更多信息。