celestriode / constructure-json
用于JSON结构的Constructure实现。
Requires
- php: ^7.3|^8.0
- ext-json: *
- celestriode/constructure: ^0.6.0
- ramsey/uuid: ^4.1
- seld/jsonlint: ^1.8
Requires (Dev)
- phpunit/phpunit: ^8.2
README
用于Constructure的JSON格式的实现。简而言之,这可以用来验证用户可能提交的JSON结构。
composer require celestriode/constructure-json
入门
首先需要一个新的JsonConstructure
对象。此对象将保存事件处理程序和任何全局审计。它将把格式良好的JSON字符串转换为有效的结构,用于比较。它还可以启动比较并返回成功。
$constructure = new JsonConstructure(new EventHandler(), new TypesMatch());
事件处理程序持有可以通过各种方式触发的事件。审计是对特定JSON输入结构必须通过的要求,而全局审计是对JSON输入中的所有嵌套字段或元素进行检查。
事件和审计将在后面进行描述。现在,一个全新的事件处理程序就足够了。没有它,库的通用用法将毫无用处,但有时您可能不关心数据类型。
转换原始输入
此库的假设是字符串化的JSON将是原始输入。可以使用JsonConstructure.toStructure()
方法将此类输入转换为结构。
$raw = '{"test": "hello"}'; $input = $constructure->toStructure($raw);
输入将基于库中使用的JSON对象构建,允许将其与预期结构进行比较。
构建预期结构
在开始比较之前,必须使用库提供的各种工具构建一个预期结构。这是您大部分工作的地方,包括构建结构、创建审计和创建事件。
结构
结构的根可以是任何JSON数据类型。例如,最简单的预期结构可能是检查输入是否为非空字符串。
$expected = Json::string();
然后用户输入可以是以下任何一种
$raw1 = '4'; // Invalid, not a string. $raw2 = null; // Invalid, is null. $raw3 = '"Hello"'; // Valid, a string with value "hello".
然后可以将输入与预期结构进行比较,返回true或false,取决于其成功与否。
$constructure->validate($constructure->toStructure($raw1), $expected); // false $constructure->validate($constructure->toStructure($raw2), $expected); // false $constructure->validate($constructure->toStructure($raw3), $expected); // true
当然,使用此库以根的形式检查原始类型有些过度。目的是更复杂的结构。《nullable()`选项允许输入结构为null。此外,对象可以使用《required()`选项区分可选字段和必选字段。选项可以串联以便于创建。
$expected = Json::object() ->addChild("time", Json::integer()->required()->nullable()) ->addChild("event", Json::object() ->addChild("name", Json::string()->required()));
预期的结构应该是一个以对象为根的结构。在该对象中,有一个必须为整数或null的必选time
字段。旁边是一个可选的event
对象。当指定该对象时,其中必须有一个名为name
的字符串字段。
// Valid, does not need optional "event" object. $raw1 = '{"time": null}'; // Invalid, missing required "time" integer or null. $raw2 = '{"event": {"name": ""}}'; // Invalid, missing the required nested "name" string. $raw3 = '{"time": 1, "event": {}}';
审计
审计是您可以创建来自定义结构要求的自定义检查。例如,如果您想限制字符串仅允许某些值,则需要审计。此库提供了少量默认审计以覆盖一些基本需求。
其中一个默认审计是HasValue
。它可以与任何原始数据类型一起使用,以限制输入到特定值。
$expected = Json::object() ->addChild("method", Json::string()->addAudit(new HasValue("get", "post")));
在method
字符串中添加了审计,以限制其值只能是"get"和"post"。
$raw1 = '{"method": "get"}'; // true. $raw2 = '{"method": "postt"}'; // false (there is a typo). $raw3 = '{}'; // true, "method" is not required.
事件
默认情况下,通过验证输入与预期结构的一致性所给出的反馈是整个结构的简单真或假。如果您想提供更多的反馈,或对结构问题做出其他响应,则需要使用事件。
在特定情况下,根据名称触发事件。如果输入的值不是所需的值之一,则HasValue
审计将触发名为53fb094f-62ca-4853-9d57-9b100e22eb52
的事件(使用HasValue::INVALID_VALUE
更容易访问)。
应将任何事件添加到构造对象的事件处理程序中。它接受一个名称和一个闭包,闭包的输入取决于触发的事件。当您在自己的自定义审计中触发事件时,请确保与您提供的参数保持一致。
$event = function (HasValues $hasValueAudit, AbstractJsonPrimitive $input, AbstractJsonPrimitive $expected) { echo "Unexpected value '{$input->getString()}', must be one of: " . implode(", ", $hasValueAudit->getValues()); } $constructure->getEventHandler()->addEvent(HasValue::INVALID_VALUE, $event);
现在每当触发HasValue::INVALID_VALUE
事件时,它将回显其消息。
$raw = '{"method": "postt"}';
意外的值' postt',必须是以下之一:get,post
结构
所有扩展AbstractJsonStructure
的结构都可以访问以下方法。
结构类型
可以使用Utils\Json
实用程序将JSON数据类型添加到结构中。虽然大多数这些类型接受一个值,但预期结构不需要值。除非您手动构建输入结构,否则您不需要提供值。
原始结构
所有原始数据类型(布尔值、整数、双精度浮点数、字符串和null)都扩展了AbstractJsonPrimitive
,它提供了以下方法的访问。
对于JsonString
结构,getBoolean()
返回字符串是否为空。getInteger()/getDouble()
方法取决于字符串的值:如果值是严格的数字,它将以整数/双精度浮点数的形式返回该值。否则,它将返回字符串的长度。
父结构
所有具有某种形式子项(对象和数组)的数据类型都扩展了AbstractJsonParent
,它提供了以下方法的访问。
如果预期结构中的对象添加了一个没有键的子项,则该子项被视为一个占位符,其中输入的键可以是任何内容。
对象结构
所有JsonObject
结构都可以访问以下方法。
由于对象也是父对象,因此它们可以访问父方法以添加子项。
$expected = Json::object() ->addChild("first", Json::string()) ->addChild("second", Json::number()); $raw1 = '{"first": "hello", "second": 40}'; // Passes. $raw2 = '{"first": 40, "second": "hello"}'; // Both fail.
占位符
当向预期对象结构添加一个没有键的子项(例如,addChild(null, Json::string())
)时,输入可以包含任何键,只要它完美匹配子项的结构。在检查期间,事件不会立即触发。如果没有完美的匹配,则在那些检查期间发生的所有事件都将被触发。
$expected = Json::object()->addChild(null, Json::string()); $raw1 = '{"hello": "yes", "goodbye": "no}'; // Both "hello" and "goodbye" pass. $raw2 = '{"hello", "yes", "goodbye", 3}'; // "hello" passes but "goodbye" fails.
您可以有多个具有不同结构的占位符。但是,在这样做时请小心,因为输入首先匹配的结构将阻止它检查其余部分,即使还有另一个完美的匹配。
数组结构
所有JsonArray
结构都可以访问以下方法。
如果预期数组结构有多个子项,则输入的每个元素都将与每个子项进行比较,直到找到完美的匹配(类似于对象占位符)。
$expected = Json::array()->addElements(Json::string(), Json::boolean()); $raw1 = '[true, true, "hello", false, "goodbye"]'; // All elements pass. $raw2 = '[3, true, "hello", false]'; // Element at index 0 fails.
审计
库附带了一些非常有用的审计。
分支
分支是一种根据输入调整预期结构的技术。例如,如果一个对象中的字符串具有特定值,那么应该有大量其他字段可供验证。分支是通过预先打包的Branch
审计执行的,但您也可以创建具有特定功能的自己的分支。
以下示例中,如果method
字段值为"first",则分支A可用;如果值为"second",则分支B可用。根对象仅验证"method"字段,但一旦某个分支可用,与之关联的结构将有效地与对象合并。
$branchA = new Branch("Branch A", Json::object()->addChild("with", Json::boolean()->required()), new ChildHasValue("method", "first")) $branchB = new Branch("Branch B", Json::object()->addChild("next", Json::boolean()->required()), new ChildHasValue("method", "second")) $expected = Json::object() ->addChild("method", Json::string()) ->addAudits($branchA, $branchB) $raw1 = '{"method": "first", "with": true}'; // Passes. $raw2 = '{"method": "second", "next": false}'; // Passes. $raw3 = '{"method": "third"}'; // Passes because there is no HasValue audit on the "method" string itself. $raw4 = '{}'; // Passes because the "method" string is not required. $raw5 = '{"with": 4}'; // Fails because "with" was not expected. $raw6 = '{"method": "first", "with": 4}'; // Fails because "with" must be a boolean. $raw7 = '{"method": "first"}'; // Fails because "with" is required.
创建自己的JSON审计
所有针对JsonConstructure
的审计都应该从扩展AbstractJsonAudit
类开始,该类验证传递给它的结构是否为JSON。你需要两个方法:auditJson()
和getName()
。
class CustomAudit extends AbstractJsonAudit { protected function auditJson(AbstractConstructure $constructure, AbstractJsonStructure $input, AbstractJsonStructure $expected): bool { ... } public static function getName(): string { return "custom_audit"; } }
getName()
方法返回审计的更友好的名称,如果需要,可以将其包含在用户反馈中。它没有其他定义的用途。
在auditJson()
方法中,你将获得用于验证输入的结构对象。你可以用它来根据审计中的操作触发事件。除此之外,你将获得输入结构本身,你可以用它来验证审计的输入,以及预期的结构,你可以用它来进一步控制输入所期望的内容。
例如,以下审计将确保字符串输入长度至少为10
protected function auditJson(AbstractConstructure $constructure, AbstractJsonStructure $input, AbstractJsonStructure $expected): bool { // Ensure the input and expected structures are both strings. // Feedback from this can be covered by the TypesMatch audit. if (!($input instanceof JsonString) || !($expected instanceof JsonString)) { return false; } // Check if the length of the string is less than 10. if (strlen($input->getValue()) < 10) { // Trigger an event. Pass in the audit, the input, and expected structure. // The event can do what it likes with these. $constructure->getEventHandler()->trigger("some unique event name", $this, $input, $expected); // Since the audit failed, return false. return false; } // The string has a length of 10 or more, so the audit passes. return true; }
事件名称应指代一些唯一处理此自定义审计的事件。在这种情况下,你可能需要一个更可能独一无二的名称。使用生成的UUID是一个不错的选择。将此类名称作为类上的常量设置,使其更容易在类外引用,而不需要记住UUID。
class CustomAudit extends AbstractJsonAudit { const INVALID_VALUE = '1627ea1f-e25c-4f08-aa31-eeb0cf6bdfec'; protected function auditJson(...): bool { ... $constructure->getEventHandler()->trigger(self::INVALID_VALUE, $this, $input, $expected); ... } ... }
以这种方式使用审计对多个特定用例之外的帮助不大。审计的理想情况是通用化,适用于许多情况。例如,如果你想要另一个审计检查字符串长度为15或更高,而不是仅10呢?
你可以使用__construct()
魔术方法来接收最小(甚至最大)字符串长度,审计的主体可以使用这些输入进行通用化。更进一步,扩展其他审计有助于减少代码重复。例如,NumberRange
类在其构造函数中已经有了最小和最大输入,并确保传入的结构是原始数据类型。它还提供了一个withinRange(float $value): bool
方法来验证任何基于最小和最大的数值。
class CustomAudit extends NumberRange { const INVALID_VALUE = '1627ea1f-e25c-4f08-aa31-eeb0cf6bdfec'; protected function auditPrimitive(AbstractConstructure $constructure, AbstractJsonPrimitive $input, AbstractJsonPrimitive $expected): bool { // Get the value of the input as a string and get its length. $length = strlen($input->getString()); // Check the length of the string against the min and max. if ($this->withinRange($length)) { // The length is correct, so the audit passes. return true; } // The length is incorrect, so the audit fails. $constructure->getEventHandler()->trigger(self::INVALID_VALUE, $this, $input, $expected); return false; } ... }
正如你可能注意到的,这实际上与预包装的StringLength
审计相同。
抽象审计类
有几个更高级的抽象审计类可以扩展,以减少你需要自己编写的检查数量。
事件
事件是在某些条件下触发的事件。事件将主要从失败的审计中触发。主要事件处理器存储在结构对象中。你可以选择在实例化结构之前构建事件处理器,或者在其之后构建。
$eventHandler = new EventHandler(); $eventHandler->addEvent(...); $constructure = new JsonConstructure($eventHandler);
$constructure = new JsonConstructure(new EventHandler()); $constructure->getEventHandler()->addEvent(...);
当你有各种结构可用于验证时,前者可能更容易。