swaggest/json-schema

基于 JSON-schema 验证的 高定义 PHP 结构


README

Build Status codecov time tracker Code lines Comments

基于 JSON-schema 验证的高定义 PHP 结构。

支持的 schema

安装

composer require swaggest/json-schema

使用

结构定义可以使用 json-schema 或使用扩展 Swaggest\JsonSchema\Structure\ClassStructurePHP 类来完成

根据给定的 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);

您可以有多个映射命名空间,通过 Contextmapping 属性进行控制

$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']);

代码质量和测试覆盖率

这里故意违反了一些代码质量最佳实践(见 Scrutinizer Code Quality)以允许在维护成本最低的情况下获得最佳性能。

这些违反是通过全面测试覆盖来保证的

贡献

欢迎提交问题和拉取请求!

JetBrains 支持。