swaggest / json-schema
基于 JSON-schema 验证的 高定义 PHP 结构
Requires
- php: >=5.4
- ext-json: *
- phplang/scope-exit: ^1.0
- swaggest/json-diff: ^3.8.2
- symfony/polyfill-mbstring: ^1.19
Requires (Dev)
- phperf/phpunit: 4.8.37
Suggests
- ext-mbstring: For better performance
- dev-master
- v0.12.42
- v0.12.41
- v0.12.40
- v0.12.39
- v0.12.38
- v0.12.37
- v0.12.36
- v0.12.35
- v0.12.34
- v0.12.33
- v0.12.32
- v0.12.31
- v0.12.30
- v0.12.29
- v0.12.28
- v0.12.27
- v0.12.26
- v0.12.25
- v0.12.24
- v0.12.23
- v0.12.22
- v0.12.21
- v0.12.20
- v0.12.19
- v0.12.18
- v0.12.17
- v0.12.16
- v0.12.15
- v0.12.14
- v0.12.13
- v0.12.12
- v0.12.11
- v0.12.10
- v0.12.9
- v0.12.8
- v0.12.7
- v0.12.6
- v0.12.5
- v0.12.4
- v0.12.3
- v0.12.2
- v0.12.1
- v0.12.0
- v0.11.6
- v0.11.5
- v0.11.4
- v0.11.3
- v0.11.2
- v0.11.1
- v0.11.0
- v0.10.4
- v0.10.3
- v0.10.2
- v0.10.1
- v0.10.0
- v0.9.0
- v0.8.0
- v0.7.2
- v0.7.1
- v0.7.0
- v0.6.0
- v0.5.0
- v0.4.0
- v0.3.1
- v0.3.0
- v0.2.0
- v0.1.0
- dev-issue-136
- dev-wip-nested
This package is auto-updated.
Last update: 2024-09-12 16:47:05 UTC
README
基于 JSON-schema 验证的高定义 PHP 结构。
支持的 schema
安装
composer require swaggest/json-schema
使用
结构定义可以使用 json-schema
或使用扩展 Swaggest\JsonSchema\Structure\ClassStructure
的 PHP
类来完成
根据给定的 schema 验证 JSON 数据
定义您的 json-schema
$schemaJson = <<<'JSON' { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "orders": { "type": "array", "items": { "$ref": "#/definitions/order" } } }, "required":["id"], "definitions": { "order": { "type": "object", "properties": { "id": { "type": "integer" }, "price": { "type": "number" }, "updated": { "type": "string", "format": "date-time" } }, "required":["id"] } } } JSON;
加载它
use Swaggest\JsonSchema\Schema; $schema = Schema::import(json_decode($schemaJson));
验证数据
$schema->in(json_decode(<<<'JSON' { "id": 1, "name":"John Doe", "orders":[ { "id":1 }, { "price":1.0 } ] } JSON )); // Exception: Required property missing: id at #->properties:orders->items[1]->#/definitions/order
您还可以在字符串 uri
上调用 Schema::import
来导入 schema json 数据。
$schema = Schema::import('https://127.0.0.1:1234/my_schema.json');
或使用布尔参数。
$schema = Schema::import(true); // permissive schema, always validates $schema = Schema::import(false); // restrictive schema, always invalidates
理解错误原因
对于使用 oneOf
/anyOf
定义的复杂 schema,可能很难找到数据的问题所在。异常消息可能看起来像
No valid results for oneOf {
0: Enum failed, enum: ["a"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[0]
1: Enum failed, enum: ["b"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[1]
2: No valid results for anyOf {
0: Enum failed, enum: ["c"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]->anyOf[0]
1: Enum failed, enum: ["d"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]->anyOf[1]
2: Enum failed, enum: ["e"], data: "f" at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]->anyOf[2]
} at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo->oneOf[2]->$ref[#/cde]
} at #->properties:root->patternProperties[^[a-zA-Z0-9_]+$]:zoo
对于使用 oneOf
/anyOf
定义的模糊 schema,消息是缩进的多行字符串。
处理路径是 schema 和数据指针的组合。您可以使用 InvalidValue->getSchemaPointer()
和 InvalidValue->getDataPointer()
来提取 schema/data 指针。
您可以通过 InvalidValue->getFailedSubSchema
获取验证失败的 Schema
实例。
您可以使用 InvalidValue->inspect()
构建 error tree。
带验证的 PHP 结构化类
/** * @property int $quantity PHPDoc defined dynamic properties will be validated on every set */ class User extends ClassStructure { /* Native (public) properties will be validated only on import and export of structure data */ /** @var int */ public $id; public $name; /** @var Order[] */ public $orders; /** @var UserInfo */ public $info; /** * @param Properties|static $properties * @param Schema $ownerSchema */ public static function setUpProperties($properties, Schema $ownerSchema) { // You can add custom meta to your schema $dbTable = new DbTable; $dbTable->tableName = 'users'; $ownerSchema->addMeta($dbTable); // Setup property schemas $properties->id = Schema::integer(); $properties->id->addMeta(new DbId($dbTable)); // You can add meta to property. $properties->name = Schema::string(); // You can embed structures to main level with nested schemas $properties->info = UserInfo::schema()->nested(); // You can set default value for property $defaultOptions = new UserOptions(); $defaultOptions->autoLogin = true; $defaultOptions->groupName = 'guest'; // UserOptions::schema() is safe to change as it is protected with lazy cloning $properties->options = UserOptions::schema()->setDefault(UserOptions::export($defaultOptions)); // Dynamic (phpdoc-defined) properties can be used as well $properties->quantity = Schema::integer(); $properties->quantity->minimum = 0; // Property can be any complex structure $properties->orders = Schema::create(); $properties->orders->items = Order::schema(); $ownerSchema->required = array(self::names()->id); } } class UserInfo extends ClassStructure { public $firstName; public $lastName; public $birthDay; /** * @param Properties|static $properties * @param Schema $ownerSchema */ public static function setUpProperties($properties, Schema $ownerSchema) { $properties->firstName = Schema::string(); $properties->lastName = Schema::string(); $properties->birthDay = Schema::string(); } } class UserOptions extends ClassStructure { public $autoLogin; public $groupName; /** * @param Properties|static $properties * @param Schema $ownerSchema */ public static function setUpProperties($properties, Schema $ownerSchema) { $properties->autoLogin = Schema::boolean(); $properties->groupName = Schema::string(); } } class Order implements ClassStructureContract { use ClassStructureTrait; // You can use trait if you can't/don't want to extend ClassStructure const FANCY_MAPPING = 'fAnCy'; // You can create additional mapping namespace public $id; public $userId; public $dateTime; public $price; /** * @param Properties|static $properties * @param Schema $ownerSchema */ public static function setUpProperties($properties, Schema $ownerSchema) { // Add some meta data to your schema $dbMeta = new DbTable(); $dbMeta->tableName = 'orders'; $ownerSchema->addMeta($dbMeta); // Define properties $properties->id = Schema::integer(); $properties->userId = User::properties()->id; // referencing property of another schema keeps meta $properties->dateTime = Schema::string(); $properties->dateTime->format = Format::DATE_TIME; $properties->price = Schema::number(); $ownerSchema->setFromRef('#/definitions/order'); // Define default mapping if any. $ownerSchema->addPropertyMapping('date_time', Order::names()->dateTime); // Use mapped name references after the default mapping was configured. $names = self::names($ownerSchema->properties); $ownerSchema->required = array( $names->id, $names->dateTime, // "date_time" $names->price ); // Define additional mapping $ownerSchema->addPropertyMapping('DaTe_TiMe', Order::names()->dateTime, self::FANCY_MAPPING); $ownerSchema->addPropertyMapping('Id', Order::names()->id, self::FANCY_MAPPING); $ownerSchema->addPropertyMapping('PrIcE', Order::names()->price, self::FANCY_MAPPING); } }
动态属性的验证在设置时执行,这可以帮助找到无效数据的来源,但会以一些性能下降为代价
$user = new User(); $user->quantity = -1; // Exception: Value more than 0 expected, -1 received
原生属性的验证仅在导入/导出时执行
$user = new User(); $user->quantity = 10; User::export($user); // Exception: Required property missing: id
错误消息提供了无效数据的路径
$user = new User(); $user->id = 1; $user->name = 'John Doe'; $order = new Order(); $order->dateTime = (new \DateTime())->format(DATE_RFC3339); $user->orders[] = $order; User::export($user); // Exception: Required property missing: id at #->properties:orders->items[0]
嵌套结构
嵌套结构允许您进行组合:在一个对象中展开多个对象,然后分开。
$user = new User(); $user->id = 1; $info = new UserInfo(); $info->firstName = 'John'; $info->lastName = 'Doe'; $info->birthDay = '1970-01-01'; $user->info = $info; $json = <<<JSON { "id": 1, "firstName": "John", "lastName": "Doe", "birthDay": "1970-01-01" } JSON; $exported = User::export($user); $this->assertSame($json, json_encode($exported, JSON_PRETTY_PRINT)); $imported = User::import(json_decode($json)); $this->assertSame('John', $imported->info->firstName); $this->assertSame('Doe', $imported->info->lastName);
您还可以使用 \Swaggest\JsonSchema\Structure\Composition
动态创建 schema 组合。这可以帮助处理连接数据的数据库查询结果。
$schema = new Composition(UserInfo::schema(), Order::schema()); $json = <<<JSON { "id": 1, "firstName": "John", "lastName": "Doe", "price": 2.66 } JSON; $object = $schema->import(json_decode($json)); // Get particular object with `pick` accessor $info = UserInfo::pick($object); $order = Order::pick($object); // Data is imported objects of according classes $this->assertTrue($order instanceof Order); $this->assertTrue($info instanceof UserInfo); $this->assertSame(1, $order->id); $this->assertSame('John', $info->firstName); $this->assertSame('Doe', $info->lastName); $this->assertSame(2.66, $order->price);
键映射
如果 PHP 对象的属性名应与原始数据不同,您可以在所有者 schema 上调用 ->addPropertyMapping
。
// Define default mapping if any $ownerSchema->addPropertyMapping('date_time', Order::names()->dateTime); // Define additional mapping $ownerSchema->addPropertyMapping('DaTe_TiMe', Order::names()->dateTime, self::FANCY_MAPPING); $ownerSchema->addPropertyMapping('Id', Order::names()->id, self::FANCY_MAPPING); $ownerSchema->addPropertyMapping('PrIcE', Order::names()->price, self::FANCY_MAPPING);
它将影响数据映射
$order = new Order(); $order->id = 1; $order->dateTime = '2015-10-28T07:28:00Z'; $order->price = 2.2; $exported = Order::export($order); $json = <<<JSON { "id": 1, "date_time": "2015-10-28T07:28:00Z", "price": 2.2 } JSON; $this->assertSame($json, json_encode($exported, JSON_PRETTY_PRINT)); $imported = Order::import(json_decode($json)); $this->assertSame('2015-10-28T07:28:00Z', $imported->dateTime);
您可以有多个映射命名空间,通过 Context
的 mapping
属性进行控制
$options = new Context(); $options->mapping = Order::FANCY_MAPPING; $exported = Order::export($order, $options); $json = <<<JSON { "Id": 1, "DaTe_TiMe": "2015-10-28T07:28:00Z", "PrIcE": 2.2 } JSON; $this->assertSame($json, json_encode($exported, JSON_PRETTY_PRINT)); $imported = Order::import(json_decode($json), $options); $this->assertSame('2015-10-28T07:28:00Z', $imported->dateTime);
您可以创建自己的预处理器,实现 Swaggest\JsonSchema\DataPreProcessor
。
元数据
Meta
是一种用您自己的数据补充 Schema
的方法。您可以保留和检索它。
您可以存储它。
$dbMeta = new DbTable(); $dbMeta->tableName = 'orders'; $ownerSchema->addMeta($dbMeta);
并检索它。
// Retrieving meta $dbTable = DbTable::get(Order::schema()); $this->assertSame('orders', $dbTable->tableName);
无验证的映射
如果您想容忍无效数据或提高映射性能,您可以在处理 Context
中指定 skipValidation
标志。
$schema = Schema::object(); $schema->setProperty('one', Schema::integer()); $schema->properties->one->minimum = 5; $options = new Context(); $options->skipValidation = true; $res = $schema->in(json_decode('{"one":4}'), $options); $this->assertSame(4, $res->one);
覆盖映射类
如果您想将数据映射到不同的类,您可以在导入器结构的顶层注册映射。
class CustomSwaggerSchema extends SwaggerSchema { public static function import($data, Context $options = null) { if ($options === null) { $options = new Context(); } $options->objectItemClassMapping[Schema::className()] = CustomSchema::className(); return parent::import($data, $options); } }
或在处理上下文中指定它
$context = new Context(); $context->objectItemClassMapping[Schema::className()] = CustomSchema::className(); $schema = SwaggerSchema::schema()->in(json_decode( file_get_contents(__DIR__ . '/../../../../spec/petstore-swagger.json') ), $context); $this->assertInstanceOf(CustomSchema::className(), $schema->definitions['User']);
代码质量和测试覆盖率
这里故意违反了一些代码质量最佳实践(见 )以允许在维护成本最低的情况下获得最佳性能。
这些违反是通过全面测试覆盖来保证的
- JSON-Schema-Test-Suite 的 draft-04、draft-06、draft-07
- 测试用例(不包括
$data
和少数测试)来自 epoberezkin/ajv(一个成熟的js实现)
贡献
欢迎提交问题和拉取请求!
由 JetBrains 支持。