vanilla/garden-schema

基于 JSON Schema 的简单数据验证和清洗库。


README

Packagist Version MIT License CLA

花园模式(Garden Schema)是一个基于 OpenAPI 3.0 模式 的简单数据验证和清洗库。

特性

  • 定义任意深度的 PHP 数组的数据结构,并进行验证。

  • 验证后的数据将被清洗并转换为适当的类型。

  • 模式定义了允许数据的白名单,并移除所有多余的数据。

  • Schema 类理解 OpenAPI 模式格式中的一部分数据。随着时间的推移,我们将添加更多内置 JSON 模式验证的支持。

  • 开发者可以使用更短的模式格式快速定义模式。我们构建这个类以便尽可能易于使用。避免开发者在使用锁定数据时发出叹息。

  • 添加自定义验证回调以支持几乎所有验证场景。

  • 覆盖验证类以自定义应用程序中错误显示的方式。

用法

花园模式旨在成为数据验证的通用包装器。当您想要使代码针对用户提交的数据进行加固时,它将非常有价值。以下是一些示例用法:

  • 检查提交到您的 API 端点的数据。在您的端点开头定义模式,并在执行任何其他操作之前验证数据。这样,您可以确保使用的是干净的数据,并在代码的后期避免大量乱麻式的检查。这正是我们开发花园模式的原因。

  • 清洗用户输入。Schema 对象将数据转换为适当的类型,并优雅地处理常见的用例(例如,将字符串 "true" 转换为布尔值)。这允许您在代码中使用更多的 "===" 检查,这有助于在长期内避免错误。

  • 在传递到数据库之前验证数据,以呈现易于理解的错误,而不是神秘的数据库生成的错误。

  • 在返回之前清洗输出。许多数据库驱动程序将数据作为字符串返回,即使它被定义为不同的类型。Schema 将适当清洗数据,这对于非 PHP 世界的消费尤其重要。

基本用法

要验证数据,您首先创建 Schema 类的一个实例,然后调用其 validate() 方法。

namespace Garden\Schema;

$schema = Schema::parse([...]);
try {
    $valid = $schema->validate($data);
} catch (ValidationException $ex) {
    ...
}

在上面的示例中,使用传递给其构造函数的模式定义创建了一个 Schema 对象(更多关于这一点稍后介绍)。然后可以将要验证的数据传递给 validate() 方法。如果数据没有问题,则返回一个干净的版本,否则抛出 ValidationException

定义模式

Schema 类通过一个定义模式的数组进行实例化。该数组可以是 OpenAPI 3.0 模式格式,也可以是自定义短格式。建议您使用 OpenAPI 格式定义您的模式,但对于想要快速编写原型的人来说,短格式是很好的。本节将描述短格式。

默认情况下,模式是一个数组,其中数组的每个元素定义一个对象属性。当我们说 "对象" 时,我们指的是 JavaScript 对象或具有字符串键的 PHP 数组。属性可以以几种方式定义

[
    '<property>', // basic property, can be any type
    '<property>?', // optional property
    '<property>:<type>?', // optional property with specific type

    '<property>:<type>?' => 'Description', // optional, typed property with description
    '<property>?' => ['type' => '<type'>, 'description' => '...'], // longer format

    '<property>:o' => [ // object property with nested schema
        '<property>:<type>' => '...',
        ...
    ],
    '<property>:a' => '<type>', // array property with element type
    '<property>:a' => [ // array property with object element type
        '<property>:<type>' => '...',
        ...
    ]
]

您可以通过提供所需的信息来快速定义对象模式。您可以根据需要创建深度嵌套的方案,以验证非常复杂的数据。这个简短的方案在内部转换为与JSON模式兼容的数组,您可以通过 jsonSerialize() 方法查看此数组。

我们提供一流的支持描述,因为我们相信从一开始就编写可读的代码。如果您不喜欢这种方式,可以简单地省略描述,它们将在方案中留空。

类型和简短类型

Schema 类支持以下类型。每种类型都有一个或多个别名。在定义代码中的方案时,您可以使用别名来简化,它将在内部转换为正确的类型,包括在错误中使用。

数组和对象

数组和对象类型有些特殊,因为它们包含多个元素而不是单个值。因此,您可以定义那些属性应该包含的数据类型。以下是一些示例

$schema = Schema::parse([
    'items:a', // array of any type
    'tags:a' => 's', // array of strings

    'attributes:o', // object of any type
    'user:o' => [ // an object with specific properties
        'name:s',
        'email:s?'
    ]
]);

非对象模式

默认情况下,模式定义一个对象,因为这是模式最常用的用途。如果您想使模式表示一个数组或甚至是一个基本类型,您只需定义一个没有名称的单个字段。以下示例定义了一个对象数组(即数据库查询的输出)。

$schema = Schema::parse([
    ':a' => [
        'id:i',
        'name:s',
        'birthday:dt'
    ]
]);

此方案适用于以下类似的数据

[
    ['id' => 1, 'name' => 'George', 'birthday' => '1732-02-22'],
    ['id' => 16, 'name' => 'Abraham', 'birthday' => '1809-02-12'],
    ['id' => 32, 'name' => 'Franklin', 'birthday' => '1882-01-30']
]

可选属性和可空属性

在定义对象方案时,您可以使用一个 "?" 来表示属性是可选的。这意味着在验证过程中可以完全省略该属性。这与为属性提供一个 null 值不同,这在可选属性中被认为是无效的。

如果您想使属性允许 null 值,可以在属性上指定 nullable 属性。有两种方法可以这样做

[
    // You can specify nullable as a property attribute.
    'opt1:s?' => ['nullable' => true],

    // You can specify null as an optional type in the declaration.
    'opt2:s|n?' => 'Another nullable, optional property.'
]

默认值

您可以使用 default 属性指定一个默认值。如果在验证过程中省略了值,则将使用默认值。请注意,在稀疏验证期间不应用默认值。

验证数据

一旦您有了方案,您就可以使用 validate()isValid() 方法来验证数据。

Schema::validate() 方法

您将想要验证的数据传递给 Schema::validate(),它将返回您数据的清理副本或抛出一个 ValidationException

$schema = Schema::parse(['id:i', 'name:s']);
try {
    // $u1 will be ['id' => 123, 'name' => 'John']
    $u1 = $schema->validate(['id' => '123', 'name' => 'John']);

    // This will thow an exception.
    $u2 = $schema->validate(['id' => 'foo']);
} catch (ValidationException $ex) {
    // $ex->getMessage() will be: 'id is not a valid integer. name is required.'
}

在用户提交的数据上调用 validate() 允许您尽早检查数据并在数据不正确时退出。如果您只想检查数据而不抛出异常,则 isValid() 方法是一个方便的方法,它根据数据是否有效返回 true 或 false。

$schema = Schema::parse(['page:i', 'count:i?']);

if ($schema->isValid(['page' => 5]) {
    // This will be hit.
}

if ($schema->isValid(['page' => 2, 'count' => 'many']) {
    // This will not be hit because the data isn't valid.
}

ValidationException 和 Validation 类

当您调用 validate() 并且验证失败时,将抛出一个 ValidationException。此异常包含一个属性,该属性是一个包含有关已失败字段更多信息的 Validation 对象。

如果您正在编写 API,则可以 json_encode() ValidationException,它应提供一个丰富的数据集,以帮助任何消费者确切地了解他们犯了什么错误。您还可以使用 Validation 属性的各个属性来帮助适当地呈现错误输出。

Validation JSON 格式

Validation 对象和 ValidationException 都编码为特定的格式。以下是一个示例

ValidationError = {
    "message": "string", // Main error message.
    "code": "integer", // HTTP-style status code.
    "errors": { // Specific field errors.
        "<fieldRef>": [ // Each key is a JSON reference field name.
            {
                "message": "string", // Field error message.
                "error": "string", // Specific error code, usually a schema attribute.
                "code": "integer" // Optional field error code.
            }
        ]
    }
}

此格式优化用于帮助向用户界面呈现错误。您可以通过循环特定的 errors 集合,在用户界面上将错误与其输入对齐。对于深度嵌套的对象,字段名称是 JSON 引用。

模式引用

OpenAPI 允许使用 $ref 属性通过引用访问模式。使用引用允许您在一个地方定义常用模式,然后从多个位置引用它们。

要使用引用,您必须

  1. 在某个地方定义要引用的模式。
  2. 使用 $ref 属性引用模式。
  3. 使用 Schema::setRefLookup() 将模式查找函数添加到主模式中

定义可重用模式

OpenAPI 规范将所有可重用模式放置在 /components/schemas 下。如果您正在定义一个大的数组,这是一个放置它们的好地方。

$components = [
    'components' => [
        'schemas' => [
            'User' => [
                'type' => 'object',
                'properties' => [
                    'id' => [
                        'type' => 'integer'
                    ],
                    'username' => [
                        'type' => 'string'
                    ]
                ]
            ]
        ]
    ]
]

使用 $ref 引用模式

使用 / 字符分隔键来引用模式路径。

$userArray = [
    'type' => 'array',
    'items' => [
        '$ref' => '#/components/schemas/User'
    ]
]

使用 Schema::setRefLookup() 解析引用

Schema 类有一个 setRefLookup() 方法,允许您添加一个用于解析引用的可调用函数。该函数应具有以下签名

function(string $ref): array|Schema|null {
   ...
}

函数接受来自 $ref 属性的字符串,并返回一个模式数组、Schema 对象或 null(如果找不到模式)。Garden Schema 在 ArrayRefLookup 类中具有默认的 ref 查找实现,可以从静态数组中解析引用。这对于大多数用途来说应该足够好了,但您始终可以定义自己的。

您可以将所有内容组合如下

$sch = new Schema($userArray);
$sch->setRefLookup(new ArrayRefLookup($components));

$valid = $sch->validate(...);

引用在验证期间解析,所以如果您的引用有任何错误,那么在设置您的模式或引用查找函数时,将抛出 RefNotFoundException,而不是在验证期间。

模式多态性

通过允许您根据其值验证不同的模式,模式支持实现模式多态性。

属性 discriminator

模式的 discriminator 允许您指定一个对象属性,该属性指定了对象的类型。然后使用该属性引用对象的特定模式。该判别器的格式如下

{
    "discriminator": {
        "propertyName": "<string>", // Name of the property used to reference a schema.
        "mapping": {
          "<propertyValue1>": "<ref>", // Reference to a schema.
          "<propertyValue>": "<alias>" // Map a value to another value.
        }
    }
}

如上所示,propertyName 指定了哪个属性用作判别器。还有一个可选的 mapping 属性,允许您控制如何将模式映射到值。判别器按以下方式解析

  1. 使用映射属性将属性值进行映射。
  2. 如果值是有效的 JSON 引用,则进行查找。只有映射中的值可以指定这种方式下的 JSON 引用。
  3. 如果值不是有效的 JSON 引用,则在其前面添加 #/components/schemas/ 以形成一个 JSON 引用。

以下是一个示例

{
  "discriminator": {
    "propertyName": "petType",
    "mapping": {
      "dog": "#/components/schemas/Dog", // A direct reference.
      "fido": "Dog" // An alias that will be turned into a reference.
    }
  }
}

属性 oneOf

oneOf 属性与 discriminator 一起使用,以限制对象允许验证的模式。如果您没有指定 oneOf,则 #/components/schemas 下的任何模式都是合法的。

要使用 oneOf 属性,您必须指定如下所示的 $ref 节点

{
  "oneOf": [
    { "$ref": "#/components/schemas/Dog" },
    { "$ref": "#/components/schemas/Cat" },
    { "$ref": "#/components/schemas/Mouse" },
  ],
  "discriminator": {
    "propertyType": "species"
  }
}

在上面的示例中,“species”属性将被用来构建一个对模式的引用。该引用必须匹配 oneOf 属性中的其中一个引用。

如果您熟悉 OpenAPI 规范,请注意 Garden Schema 目前不支持为 oneOf 使用内联模式。

验证选项

validate()isValid() 都可以接受一个额外的 $options 参数,该参数根据选项修改验证的行为。

选项 request

您可以通过传递选项 ['request' => true] 来指定您正在验证请求数据。在验证请求数据时,标记为 readOnly: true 的属性将视为不存在,即使它们被标记为必需。

response 选项

您可以通过传递选项 ['response' => true] 来指定您正在验证响应数据。在验证响应数据时,标记为 writeOnly: true 的属性将视为不存在,即使它们被标记为必需。

sparse 选项

您可以通过传递选项 ['sparse' => true] 来指定稀疏验证。当您执行稀疏验证时,缺失的属性不会引发错误,并将稀疏数据返回。稀疏验证允许您使用相同的模式进行记录的插入和更新。这在具有 POST 与 PATCH 请求的数据库或 API 中很常见。

标志

标志可以应用于模式以更改其继承的验证。

use Garden\Schema\Schema;
$schema = Schema::parse([]);

// Enable a flag.
$schema->setFlag(Schema::VALIDATE_STRING_LENGTH_AS_UNICODE, true);

// Disable a flag.
$schema->setFlag(Schema::VALIDATE_STRING_LENGTH_AS_UNICODE, false);

// Set all flags together.
$schema->setFlags(Schema::VALIDATE_STRING_LENGTH_AS_UNICODE & Schema::VALIDATE_EXTRA_PROPERTY_NOTICE);

// Check if a flag is set.
$schema->hasFlag(Schema::VALIDATE_STRING_LENGTH_AS_UNICODE); // true

VALIDATE_STRING_LENGTH_AS_UNICODE

默认情况下,模式以字节为单位验证字符串长度。这对于数据库等存储单位来说是很有用的。

一些 Unicode 字符需要超过 1 个字节。例如,一个表情符号 😱 需要 4 个字节。

启用此标志以验证 Unicode 字符长度而不是字节长度。

VALIDATE_EXTRA_PROPERTY_NOTICE

设置此标志以在验证对象具有不在模式中定义的属性时触发通知。

VALIDATE_EXTRA_PROPERTY_EXCEPTION

设置此标志以在验证对象具有不在模式中定义的属性时抛出异常。

使用 addValidator() 自定义验证

您可以使用 Schema::addValidator() 来自定义验证。此方法允许您将回调附加到模式路径。回调具有以下形式

function (mixed $value, ValidationField $field): bool {
}

如果值有效,则回调应返回 true,否则返回 false。您可以使用提供的 ValidationField 添加自定义错误消息。

过滤数据

您可以使用 Schema::addFilter() 在验证之前过滤数据。此方法允许您在模式路径上过滤数据。回调具有以下形式

function (mixed $value, ValidationField $field): mixed {
}

回调应返回过滤后的值。过滤器在验证之前被调用,因此您可以使用它们来清理可能需要额外处理的数据。

Schema::addFilter() 还接受 $validate 参数,允许您的过滤器验证数据并绕过默认验证。如果您以这种方式验证日期,则可以向 ValidationField 参数添加自定义错误,并在验证失败时返回 Invalid::value()

格式过滤器

您还可以使用 Schema::addFormatFilter() 过滤具有特定格式的所有字段。此方法与 Schema::addFilter() 类似,但它应用于匹配给定 format 的所有字段。您甚至可以使用格式过滤器覆盖默认格式处理。

$schema = new Schema([...]);

// By default schema returns instances of DateTimeImmutable, instead return a string.
$schema->addFormatFilter('date-time', function ($v) {
    $dt = new \DateTime($v);
    return $dt->format(\DateTime::RFC3339);
}, true);

重写验证类和本地化

由于模式生成错误消息,本地化可能是一个问题。尽管 Garden Schema 本身不提供任何本地化功能,但它被设计为可以扩展以添加本地化。您可以通过子类化 Validation 类并重写其 translate() 方法来实现这一点。以下是一个基本示例

class LocalizedValidation extends Validation {
    public function translate($str) {
        if (substr($str, 0, 1) === '@') {
            // This is a literal string that bypasses translation.
            return substr($str, 1);
        } else {
            return gettext($str);
        }
    }
}

// Install your class like so:
$schema = Schema::parse([...]);
$schema->setValidationClass(LocalizedValidation::class);

在上述示例中有几点需要注意

  • 在重写 translate() 时,请确保处理以 '@' 字符开头的字符串的情况。此类字符串不应翻译且应移除字符。

  • 您可以使用setValidationClass()方法告诉Schema对象使用您特定的Validation子类。此方法接受一个类名或对象实例。如果您传递一个对象,则每次需要验证对象时都会对其进行克隆。当您想要使用依赖注入并且您的类需要更复杂的实例化时,这很有用。

JSON Schema 支持

Schema对象是OpenAPI Schema数组的包装器。这意味着您可以将有效的JSON schema传递给Schema的构造函数。下表列出了支持的JSON Schema属性。

OpenAPI Schema 支持

OpenAPI定义了一些在验证期间应用的扩展属性。