happycode / blueprint
用于描述、读取和格式化数据的工具。
Requires (Dev)
- phpunit/phpunit: ^10.4
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);
- 这里发生的第一件事是
验证
- 蓝图知道它需要从 JSON 中得到什么,所以它会确保它在那里。 - 第二件事是
过滤
- 如果 JSON 中有数据,但你的模式在任何地方都没有定义它,它将被忽略。 - 最后,它返回一个代表你所需要数据的模型,
看看这个...
$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 不包括隐藏字段 first
和 last
异常
在运行时,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 了解更多信息。