借助 Symfony 进行验证

API Platform 负责验证客户端发送到 API 的数据(通常是通过表单输入的用户数据)。默认情况下,该框架依赖于强大的 Symfony 验证器组件执行此任务,但您可以根据需要将其替换为您喜欢的验证库,例如 PHP 过滤器扩展 。

验证提交的数据

验证提交的数据就像添加 Symfony 的内置约束一样简单 或直接在标有 #[ApiResource] 属性的类中自定义约束 :

<?php
// api/src/Entity/Product.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use App\Validator\Constraints\MinimalProperties; // A custom constraint
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert; // Symfony's built-in constraints

/**
 * A product.
 *
 */
#[ORM\Entity]
#[ApiResource]
class Product
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    #[ORM\Column]
    #[Assert\NotBlank]
    public string $name;

    /**
     * @var string[] Describe the product
     */
    #[MinimalProperties]
    #[ORM\Column(type: 'json')]
    public $properties;

    // Getters and setters...
}

这是自定义 MinimalProperties 约束和相关的验证器:

<?php
// api/src/Validator/Constraints/MinimalProperties.php

namespace App\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

#[\Attribute]
class MinimalProperties extends Constraint
{
    public $message = 'The product must have the minimal properties required ("description", "price")';
}
<?php
// api/src/Validator/Constraints/MinimalPropertiesValidator.php

namespace App\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

final class MinimalPropertiesValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint): void
    {
        if (array_diff(['description', 'price'], $value)) {
            $this->context->buildViolation($constraint->message)->addViolation();
        }
    }
}

如果客户端提交的数据无效,则 HTTP 状态代码将设置为 422 Unprocessable Entity,响应正文将包含以符合请求格式的格式序列化的违规列表。例如,如果请求的格式为 JSON-LD(默认值),则验证错误将如下所示:

{
  "@context": "/contexts/ConstraintViolationList",
  "@type": "ConstraintViolationList",
  "title": "An error occurred",
  "description": "properties: The product must have the minimal properties required (\"description\", \"price\")",
  "violations": [
    {
      "propertyPath": "properties",
      "message": "The product must have the minimal properties required (\"description\", \"price\")"
    }
  ]
}

请查看错误处理指南 ,了解 API Platform 如何将验证错误等 PHP 异常转换为 HTTP 错误。

使用验证组

如果没有特定的配置,总是使用默认的验证组,但这种行为是可定制的:框架能够利用 Symfony 的验证组 。

您可以直接通过 ApiResource 属性配置验证时要使用的组:

<?php
// api/src/Entity/Book.php

use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;

#[ApiResource(validationContext: ['groups' => ['a', 'b']])]
class Book
{
    #[Assert\NotBlank(groups: ['a'])]
    public string $name;

    #[Assert\NotNull(groups: ['b'])]
    public string $author;

    // ...
}

在前面的配置中,执行验证时将使用验证组 a 和 b

与序列化组一样,可以全局指定验证组,也可以按作指定验证组。

当然,如果您愿意,您可以使用 XML 或 YAML 配置格式而不是属性。

您也可以传入组序列来代替组名称数组。

在作中使用验证组

可以对与资源相关的每个作进行不同的验证。

<?php
// api/src/Entity/Book.php

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;

#[ApiResource]
#[Delete]
#[Get]
#[Put(validationContext: ['groups' => ['Default', 'putValidation']])]
#[GetCollection]
#[Post(validationContext: ['groups' => ['Default', 'postValidation']])]
class Book
{
    #[Assert\Uuid]
    private $id;

    #[Assert\NotBlank(groups: ['postValidation'])]
    public $name;

    #[Assert\NotNull]
    #[Assert\Length(min: 2, max: 50, groups: ['postValidation'])]
    #[Assert\Length(min: 2, max: 70, groups: ['putValidation'])]
    public $author;

    // ...
}

使用此配置,有三个验证组:

默认包含不属于其他组的约束。

postValidation 仅包含对 name 和 author(长度从 2 到 50)字段的约束。

putValidation 仅包含对作者(长度从 2 到 70)字段的约束。

动态验证组

如果您需要动态确定在不同场景中对实体使用哪些验证组,只需传入可调用。回调将接收实体对象作为其第一个参数,并应返回组名称数组或组序列。

在以下示例中,我们使用静态方法返回验证组:

<?php
// api/src/Entity/Book.php

use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;

#[ApiResource(
    validationContext: ['groups' => [Book::class, 'validationGroups']]
)]
class Book
{
    /**
     * Return dynamic validation groups.
     *
     * @param self $book Contains the instance of Book to validate.
     *
     * @return string[]
     */
    public static function validationGroups(self $book)
    {
        return ['a'];
    }

    #[Assert\NotBlank(groups: ['a'])]
    public $name;

    #[Assert\NotNull(groups: ['b'])]
    public $author;

    // ...
}

或者,您可以使用服务检索要使用的组:

<?php
// api/src/Validator/AdminGroupsGenerator.php

namespace App\Validator;

use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface;
use App\Entity\Book;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class AdminGroupsGenerator implements ValidationGroupsGeneratorInterface
{
    private $authorizationChecker;

    public function __construct(AuthorizationCheckerInterface $authorizationChecker)
    {
        $this->authorizationChecker = $authorizationChecker;
    }

    public function __invoke($book): array
    {
        assert($book instanceof Book);

        return $this->authorizationChecker->isGranted('ROLE_ADMIN', $book) ? ['a', 'b'] : ['a'];
    }
}

此类根据当前用户的角色选择要应用的组:如果当前用户具有 ROLE_ADMIN 角色,则返回组 a 和 b。在其他情况下,仅返回 a

由于 Symfony DependencyInjection 组件的自动连接功能 ,该类会自动注册为服务。

然后,将实体类配置为使用此服务检索验证组:

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use App\Validator\AdminGroupsGenerator;
use Symfony\Component\Validator\Constraints as Assert;

#[ApiResource(validationContext: ['groups' => AdminGroupsGenerator::class])
class Book
{
    #[Assert\NotBlank(groups: ['a'])]
    public $name;

    #[Assert\NotNull(groups: ['b'])]
    public $author;

    // ...
}

顺序验证组

如果需要指定必须测试验证组的顺序,可以使用组序列。首先,您需要创建排序组。

<?php
namespace App\Validator;

use Symfony\Component\Validator\Constraints\GroupSequence;

class MySequencedGroup
{
    public function __invoke()
    {
        return new GroupSequence(['first', 'second']); // now, no matter which is first in the class declaration, it will be tested in this order.
    }
}

仅仅创建类是不够的,因为 Symfony 不认为该服务正在被使用。因此,若要防止删除服务,需要强制将其公开。

# api/config/services.yaml
services:
    App\Validator\MySequencedGroup: ~
        public: true

然后,您需要将您的类用作验证组。

<?php
// api/src/Entity/Greeting.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use App\Validator\One; // classic custom constraint
use App\Validator\Two; // classic custom constraint
use App\Validator\MySequencedGroup; // the sequence group to use
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource]
#[Post(validationContext: ['groups' => MySequencedGroup::class])]
class Greeting
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    /**
     * @var A nice person
     *
     * I want this "second" validation to be executed after the "first" one even though I wrote them in this order.
     */
    #[One(groups: ['second'])]
    #[Two(groups: ['first'])]
    #[ORM\Column]
    public string $name = '';

    public function getId(): int
    {
        return $this->id;
    }
}

验证删除作

缺省情况下,在 DELETE 作期间不会评估在 API 资源上指定的验证规则。如果需要,您需要在代码中触发验证。

假设您有以下使用自定义删除验证器的实体:

<?php
// api/src/Entity/MyEntity.php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use App\State\MyEntityRemoveProcessor;
use App\Validator\AssertCanDelete;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource(
    operations: [
        new Delete(validationContext: ['groups' => ['deleteValidation']], processor: MyEntityRemoveProcessor::class)
    ]
)]
#[AssertCanDelete(groups: ['deleteValidation'])]
class MyEntity
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    #[ORM\Column]
    public string $name = '';
}

创建一个处理器,该处理器接收默认处理器,您将在其中触发验证:

<?php
// api/src/State/MyEntityRemoveProcessor.php

namespace App\State;

use ApiPlatform\Doctrine\Common\State\RemoveProcessor as DoctrineRemoveProcessor;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\Validator\ValidatorInterface;
use App\Entity\MyEntity;

final readonly class MyEntityRemoveProcessor implements ProcessorInterface
{
    public function __construct(
        private DoctrineRemoveProcessor $doctrineProcessor,
        private ValidatorInterface $validator,
    )
    {
    }

    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
    {
        $this->validator->validate($data, ['groups' => ['deleteValidation']]);
        $this->doctrineProcessor->process($data, $operation, $uriVariables, $context);
    }
}

错误级别和有效负载序列化

正如 Symfony 文档中所述,您可以使用 payload 字段来定义错误级别。您可以通过在 API Platform 配置中将 serialize_payload_fields 设置为空数组来检索有效负载字段:

# api/config/packages/api_platform.yaml

api_platform:
  validator:
    serialize_payload_fields: ~

然后,序列化程序将在错误响应中返回所有有效负载值。

如果只想序列化某些有效负载字段,请在配置中定义它们,如下所示:

# api/config/packages/api_platform.yaml

api_platform:
  validator:
    serialize_payload_fields: [severity, anotherPayloadField]

在此示例中,将仅序列化 severity 和 anotherPayloadField

集合关系的验证

使用有效约束。

注意:这与集合关系非规范化有关。当尝试验证表示 Doctrine 的 ArrayCollection (toMany) 的关系时,您可能会遇到问题。 使用属性 getter 修复非规范化。返回一个数组而不是 ArrayCollection,并使用 $collectionRelation->getValues() 。然后,在 getter 而不是属性上定义验证。

例如:

<getter property="cars">
    <constraint name="Valid"/>
</getter>
<?php
// api/src/Entity/Brand.php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

final class Brand
{
    // ...

    public function __construct()
    {
        $this->cars = new ArrayCollection();
    }

    #[Assert\Valid]
    public function getCars()
    {
        return $this->cars->getValues();
    }
}

从验证元数据生成的开放词汇表

API Platform 会自动检测 Symfony 的内置验证器并相应地生成 schema.org IRI 元数据。这允许富客户端(如管理组件)推断大多数基本用例的字段类型。

涵盖以下验证约束:

约束词汇
Urlhttps://schema.org/url
Emailhttps://schema.org/email
Uuidhttps://schema.org/identifier
CardSchemehttps://schema.org/identifier
Bichttps://schema.org/identifier
Ibanhttps://schema.org/identifier
Datehttps://schema.org/Date
DateTimehttps://schema.org/DateTime
Timehttps://schema.org/Time
Imagehttps://schema.org/image
Filehttps://schema.org/MediaObject
Currencyhttps://schema.org/priceCurrency
Isbnhttps://schema.org/isbn
Issnhttps://schema.org/issn

规范属性限制

API Platform 根据 Symfony 的内置验证器生成规范属性限制。

例如,从正则表达式约束 API Platform 构建模式限制。

要根据自定义验证约束构建自定义属性架构,您可以创建一个自定义类来生成属性方案限制。

要创建属性模式,您必须实现 PropertySchemaRestrictionMetadataInterface .此接口仅定义 2 种方法:

  • create:创建属性架构
  • supports:检查属性和约束是否受支持

下面是一个实现示例:

// api/src/PropertySchemaRestriction/CustomPropertySchemaRestriction.php

namespace App\PropertySchemaRestriction;

use ApiPlatform\Metadata\ApiProperty;
use Symfony\Component\Validator\Constraint;
use App\Validator\CustomConstraint;

final class CustomPropertySchemaRestriction implements PropertySchemaRestrictionMetadataInterface
{
    public function supports(Constraint $constraint, ApiProperty $propertyMetadata): bool
    {
        return $constraint instanceof CustomConstraint;
    }

    public function create(Constraint $constraint, ApiProperty $propertyMetadata): array
    {
      // your logic to create property schema restriction based on constraint
      return $restriction;
    }
}

如果您使用自定义依赖注入配置,则需要注册相应的服务并将 api_platform.metadata.property_schema_restriction 标记。priority 属性可用于服务排序。

# api/config/services.yaml

services:
  # ...
  'App\PropertySchemaRestriction\CustomPropertySchemaRestriction':
    ~
    # Uncomment only if autoconfiguration is disabled
    #tags: [ 'api_platform.metadata.property_schema_restriction' ]

收集非规范化错误

提交数据时,可以使用 COLLECT_DENORMALIZATION_ERRORS 选项收集非规范化错误。

它可以直接在 #[ApiResource] 属性(或作中)中完成:

<?php
// api/src/Entity/Book.php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;

#[ApiResource(
    collectDenormalizationErrors: true
)]
class Book
{
    public ?bool $boolean;
    public ?string $property1;
}

如果提交的数据存在非规范化错误,则 HTTP 状态代码将设置为 422 Unprocessable Content,响应正文将包含错误列表:

{
  "@context": "/api/contexts/ConstraintViolationList",
  "@type": "ConstraintViolationList",
  "title": "An error occurred",
  "description": "boolean: This value should be of type bool.\nproperty1: This value should be of type string.",
  "violations": [
    {
      "propertyPath": "boolean",
      "message": "This value should be of type bool.",
      "code": "0"
    },
    {
      "propertyPath": "property1",
      "message": "This value should be of type string.",
      "code": "0"
    }
  ]
}

您还可以在全局资源默认值中启用全局收集非规范化错误。

Next

Frequently asked questions

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注