В Drupal 8 валидация сущностей представляет собой отдельное API, которое отделено от механизма валидации форм. Оно основывается на компоненте Symfony Validator и связано с Typed Data API. поскольку сущности и поля являются типизированными данными. Это также означает, что можно добавлять валидацию к любым типизированным данным.

Так как валидация сущностей не привязана к формам (хотя она там тоже используется), её можно использовать по требованию. К примеру, это полезно при реализации веб-сервиса, что отражено в модулях JSON:API и RESTful Web Services.

 

Использование

Компонент Symfony Validator построен на концепции ограничений (constraints) и валидаторов. Первые определяют правила для валидации, а вторые описывают её логику. В Drupal 8 ограничения интегрированы в систему плагинов, то есть являются плагинами. Все ограничения наследуют класс Symfony\Component\Validator\Constraint, а все валидаторы - Symfony\Component\Validator\ConstraintValidator. Помимо ограничений из Symfony, в Drupal 8 описаны собственные ограничения. Например, уникальность значения поля, соответствие сущности типу, корректность формата даты и т.д.

Чтобы выполнить валидацию объекта типизированных данных, нужно вызвать метод validate(). Результатом выполнения метода будет список нарушений, выявленных при проверке. Если список пуст, значит валидация прошла успешна. При этом некоторые типы данных сразу описывают, какие ограничения на них налагаются. К примеру, тип данных Email сразу связан с одноимённым ограничением.

В данном примере валидация пройдёт успешно, так как мы указали валидный email:

$definition = DataDefinition::create('email');
$typed_data = \Drupal::typedDataManager()->create($definition, 'foo-bar@example.com');
$violations = $typed_data->validate();

Можно добавлять свои ограничения:

$definition = DataDefinition::create('email');
$definition->addConstraint('Length', ['max' => 100]);
$typed_data = \Drupal::typedDataManager()->create($definition, 'foo-bar@example.com');
$violations = $typed_data->validate();

Альтернативный способ выполнения валидации:

$violations = \Drupal::typedDataManager()->getValidator()->validate($typed_data);

Валидация отдельной сущности:

$violations = $entity->validate();

Валидация отдельного поля:

$violations = $entity->get('foo_bar')->validate();

Добавляем ограничение для поля (когда есть доступ к классу сущности):

public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
  $fields['foo_bar'] = BaseFieldDefinition::create('string')
    ->setLabel(t('Foo bar'))
    ->addConstraint('CustomFieldConstraint');
 
  return $fields;
}

Добавляем ограничение для поля (когда нет доступа к классу сущности):

/**
 * Implements hook_entity_bundle_field_info_alter().
 */
function MODULE_NAME_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
  if ($entity_type->id() == 'foo' && $bundle == 'bar') {
    if (isset($fields['foo_bar'])) {
      $fields['foo_bar']->addConstraint('CustomFieldConstraint');
    }
  }
}

Ограничения можно указывать в аннотации сущности. В этом случае они будут применяться на все сущности данного типа. Если доступа к классу сущности нет, используем хук:

/**
 * Implements hook_entity_type_alter().
 */
function MODULE_NAME_entity_type_alter(array &$entity_types) {
  $entity_types['foo']->addConstraint('CustomEntityConstraint');
}

 

Собственный валидатор

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

Сначала определим ограничение (файл src/Plugin/Validation/Constraint/FutureDateConstraint.php):

<?php
 
namespace Drupal\MODULE_NAME\Plugin\Validation\Constraint;
 
use Symfony\Component\Validator\Constraint;
 
/**
 * Custom validation constraint.
 *
 * @Constraint(
 *   id = "FutureDate",
 *   label = @Translation("Future date", context = "Validation"),
 * )
 */
class FutureDateConstraint extends Constraint {
 
  /**
   * The default violation message.
   *
   * @var string
   */
  public $message = '@name field must be a future date.';
 
}

Далее определяем валидатор (файл src/Plugin/Validation/Constraint/FutureDateConstraintValidator.php). Имя класса должно следовать шаблону ${ConstraintClassName}Validator. Если имеется необходимость использовать другое имя, то оно указывается в классе ограничения в методе validatedBy().

<?php
 
namespace Drupal\MODULE_NAME\Plugin\Validation\Constraint;
 
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Constraint;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
 
/**
 * Custom constraint validator.
 */
class FutureDateConstraintValidator extends ConstraintValidator {
 
  /**
   * {@inheritdoc}
   */
  public function validate($items, Constraint $constraint) {
    /** @var \Drupal\datetime\Plugin\Field\FieldType\DateTimeFieldItemList $items */
    if (!isset($items) || $items->isEmpty()) {
      return;
    }
 
    /** @var \Drupal\datetime\Plugin\Field\FieldType\DateTimeItem $item */
    foreach ($items as $item) {
      $value = $item->getValue()['value'];
 
      if (is_string($value)) {
        $date1 = new DrupalDateTime('now', DateTimeItemInterface::STORAGE_TIMEZONE);
        $date2 = new DrupalDateTime($value, DateTimeItemInterface::STORAGE_TIMEZONE);
 
        if ($date2->getTimestamp() < $date1->getTimestamp()) {
          $this->context->addViolation($constraint->message, [
            '@name' => $items->getFieldDefinition()->getLabel(),
          ]);
        }
      }
    }
  }
 
}

Наконец, добавим ограничение к полю (файл MODULE_NAME.module):

/**
 * Implements hook_entity_bundle_field_info_alter().
 */
function MODULE_NAME_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
  if ($entity_type->id() == 'node' && $bundle == 'foo') {
    if (isset($fields['field_foo_date'])) {
      $fields['field_foo_date']->addConstraint('FutureDate');
    }
  }
}

Добавить комментарий