Дата обновления перевода 2021-06-12

Как создать пользовательское ограничение валидации

Вы можете создать пользовательское ограничение, расширив базовый класс ограничения Constraint. В качестве приера вы создадите простой валидатор, который проверяет, содержит ли строка только буквенно-цифровые знаки.

Создание класса ограничения

Для начала вам нужно создать класс ограничения (Constraint) и расширить Constraint:

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    // src/Validator/ContainsAlphanumeric.php
    namespace App\Validator;
    
    use Symfony\Component\Validator\Constraint;
    
    /**
     * @Annotation
     */
    class ContainsAlphanumeric extends Constraint
    {
        public $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
    }
    
  • Attributes
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    // src/Validator/ContainsAlphanumeric.php
    namespace App\Validator;
    
    use Symfony\Component\Validator\Constraint;
    
    #[\Attribute]
    class ContainsAlphanumeric extends Constraint
    {
        public $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
    }
    

Добавьте к классу ограничения @Annotation или #[\Attribute], если вы хотите использовать его как аннотацию/атрибут в других классах. Если содержание имеет опции конфигурации, определите их в качестве публичных свойств класса ограничения.

New in version 5.2: Возможность использовать PHP-атрибуты для конфигурации ограничений была представлена в Symfony 5.2. До этого, Аннотации Doctrine были единственным способом аннотировать ограничения.

Создание самого валидатора

Как вы видите, класс ограничения достаточно минимален. Сама валидация выполняется другим классом “валидатором ограничения”. Класс валидатора ограничения указывается методом ограничения validatedBy(), который включает некоторую простую логику по умолчанию:

// в базовом классе Symfony\Component\Validator\Constraint
public function validatedBy()
{
    return static::class.'Validator';
}

Другими словами, если вы создадите пользовательское Constraint (например, MyConstraint), Symfony автоматически будет искать другой класс, MyConstraintValidator при проведении самой валидации.

Класс валидатора так же прост, и имеет только один обязательный метод validate():

// src/Validator/ContainsAlphanumericValidator.php
namespace App\Validator;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

class ContainsAlphanumericValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        if (!$constraint instanceof ContainsAlphanumeric) {
            throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
        }

        // пользовательские ограничения должны игнорировать пустые значения и null, чтобы
        // позволить другим ограничениям (NotBlank, NotNull, и др.) позаботиться об этом
        if (null === $value || '' === $value) {
            return;
        }

        if (!is_string($value)) {
            // вызовите это исключение, если ваш валидатор не может обработать переданный тип, чтобы он мог быть отмечен как невалидный
            throw new UnexpectedValueException($value, 'string');

            // разделите множество типов, используя вертикальные черты
            // вызовите новое UnexpectedValueException($value, 'string|int');
        }

        if (!preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) {
            // аргумент должен быть строкой или объектом, реализующим implementing __toString()
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ string }}', $value)
                ->addViolation();
        }
    }
}

Внутри validate вам не нужно возвращать значение. Вместо этого, вы добавляете нарушения к свойству валидатора context и значение будет принято как валидное, если оно не вызывает никаких нарушений. Метод buildViolation() берёт сообщение об ошибке в качестве своего аргумента и возвращает экземпляр ConstraintViolationBuilderInterface. Метод вызова addViolation() в конце-концов добавляет наружение в контекст.

Использование нового валидатора

Использовать пользовательские валидаторы как и те, что предоставляются самой Symfony:

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // src/Entity/AcmeEntity.php
    namespace App\Entity;
    
    use App\Validator as AcmeAssert;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class AcmeEntity
    {
        // ...
    
        /**
         * @Assert\NotBlank
         * @AcmeAssert\ContainsAlphanumeric
         */
        protected $name;
    
        // ...
    }
    
  • Attributes
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    // src/Entity/AcmeEntity.php
    namespace App\Entity;
    
    use App\Validator as AcmeAssert;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class AcmeEntity
    {
        // ...
    
        #[Assert\NotBlank]
        #[AcmeAssert\ContainsAlphanumeric]
        protected $name;
    
        // ...
    }
    
  • YAML
    1
    2
    3
    4
    5
    6
    # config/validator/validation.yaml
    App\Entity\AcmeEntity:
        properties:
            name:
                - NotBlank: ~
                - App\Validator\ContainsAlphanumeric: ~
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- config/validator/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="App\Entity\AcmeEntity">
            <property name="name">
                <constraint name="NotBlank"/>
                <constraint name="App\Validator\ContainsAlphanumeric"/>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    // src/Entity/AcmeEntity.php
    namespace App\Entity;
    
    use App\Validator\ContainsAlphanumeric;
    use Symfony\Component\Validator\Constraints\NotBlank;
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    
    class AcmeEntity
    {
        public $name;
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('name', new NotBlank());
            $metadata->addPropertyConstraint('name', new ContainsAlphanumeric());
        }
    }
    

Если ваше ограничение содержит опции, то они должны быть публичными свойствами пользовательского класса ограничения, который вы создали ранее. Эти опции могут быть сконфигурированы как опции базовых ограничений Symfony.

Валидаторы ограничений с зависимостями

Если вы используете конфигурацию services.yml по умолчанию, то ваш валидатор уже зарегистрирован в качестве сервиса и тегирован необходимым validator.constraint_validator. Это означает, что вы можете внедрять сервисы или конфигурацию, как любой другой сервис.

Создайте повторно используемый набор ограничений

Если вам часто нужно применять общий набор ограничений в разных местах по всему вашему приложению, вы можете расширить ограничение Compound.

New in version 5.1: Ограничение Compound было представлено в Symfony 5.1.

Валидатор класса ограничений

Кроме валидации свойства класса, ограничение может иметь область действия класса, при определении цели в его классе Constraint:

public function getTargets()
{
    return self::CLASS_CONSTRAINT;
}

С помощью этого, метод валидатора validate() получает объект в качестве своего первого аргумента:

class ProtocolClassValidator extends ConstraintValidator
{
    public function validate($protocol, Constraint $constraint)
    {
        if ($protocol->getFoo() != $protocol->getBar()) {
            $this->context->buildViolation($constraint->message)
                ->atPath('foo')
                ->addViolation();
        }
    }
}

Tip

Метод atPath() определяет свойство, с которым ассоциируется ошибка валидации. Используйте любой валидный синтаксис PropertyAccess, чтобы определить это свойство.

Валидатор класса ограничения применяется к самому классу, а не к свойству:

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    // src/Entity/AcmeEntity.php
    namespace App\Entity;
    
    use App\Validator as AcmeAssert;
    
    /**
     * @AcmeAssert\ProtocolClass
     */
    class AcmeEntity
    {
        // ...
    }
    
  • Attributes
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    // src/Entity/AcmeEntity.php
    namespace App\Entity;
    
    use App\Validator as AcmeAssert;
    
    #[AcmeAssert\ProtocolClass]
    class AcmeEntity
    {
        // ...
    }
    
  • YAML
    1
    2
    3
    4
    # config/validator/validation.yaml
    App\Entity\AcmeEntity:
        constraints:
            - App\Validator\ProtocolClass: ~
    
  • XML
    1
    2
    3
    4
    <!-- config/validator/validation.xml -->
    <class name="App\Entity\AcmeEntity">
        <constraint name="App\Validator\ProtocolClass"/>
    </class>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    // src/Entity/AcmeEntity.php
    namespace App\Entity;
    
    use App\Validator\ProtocolClass;
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    
    class AcmeEntity
    {
        // ...
    
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addConstraint(new ProtocolClass());
        }
    }
    

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.