Providing a custom validation constraint

Last updated on
20 June 2017

Creating a custom constraint requires three steps:

  1. Define the constraint
  2. Create the validation for the constraint
  3. Set the constraint to the field it needs to validate

Step 1: Define the constraint

The constraint definition will go in the namespace Drupal\[MODULENAME]\Plugin\Validation\Constraint, and will extend Symfony\Component\Validator\Constraint. In this class, the types of constraint violations are defined, along with the error messages that will be displayed for the given violations.

namespace Drupal\[MODULENAME]\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * Checks that the submitted value is a unique integer
 *
 * @Constraint(
 *   id = "unique_integer",
 *   label = @Translation("Unique Integer", context = "Validation"),
 * )
 */
class UniqueInteger extends Constraint
{
  // The message that will be shown if the value is not an integer
  public $notInteger = '%value is not an integer';

  // The message that will be shown if the value is not unique
  public $notUnique = '%value is not unique';
}

Step 2: Create the validation for the constraint

The next step is to create the class that will validate the constraint. The constraint validation will go in the namespace Drupal\[MODULENAME]\Plugin\Validation\Constraint, and will extend Symfony\Component\Validator\ConstraintValidator. In this class, the submitted values will be returned, and any violations will be registered.

Please note, that the name of your validator class is expected to be ${ConstraintClassName}Validator by default. If you want to use a different name, you may overwrite the validatedBy() method of your Constraint class, that you created in step 1.

namespace Drupal\[MODULENAME]\Plugin\Validation\Constraint;

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

/**
 * Validates the UniqueInteger constraint.
 */
class UniqueIntegerValidator extends ConstraintValidator {

  /**
   * {@inheritdoc}
   */
  public function validate($items, Constraint $constraint) {
    foreach ($items as $item) {
      // First check if the value is an integer.
      if (!is_int($item->value)) {
        // The value is not an integer, so a violation, aka error, is applied.
        // The type of violation applied comes from the constraint description
        // in step 1.
        $this->context->addViolation($constraint->notInteger, ['%value' => $item->value]);
      }

      // Next check if the value is unique.
      if (!$this->isUnique($item->value)) {
        $this->context->addViolation($constraint->notUnique, ['%value' => $item->value]);
      }
    }
  }

  private function isUnique($value) {
    // Here is where the check for a unique value would happen.
  }

}

Step 3: Set the constraint to the field it needs to validate

The method of applying the constraint differs depending on whether the constraint is being added to the entity definition, or being added to an entity provided by core or another module.

Method 1 - adding a constraint in the entity definition.

To add a constraint to in the entity definition, the constraint is added to ContentEntityBase::baseFieldDefinitions(), using BaseFieldDefinition::addConstraint():

public static function baseFieldDefinitions(EntityTypeInterface $entityType) {
  $fields['unique_number'] = BaseFieldDefinition::create('integer')
    ->setLabel(t('Unique Number'))
    // Use the ID of the constraint as it was defined
    // in the annotation of the constraint definition
    ->addConstraint('unique_integer');

  return $fields;
}

Method 2 - adding a constraint to an entity provided by core or another module

In [MODULENAME].module, implement hook_entity_bundle_field_info_alter():

/**
 * Implements hook_entity_bundle_field_info_alter().
 */
function MODULENAME_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle) {
  if ($bundle === 'my_bundle') {
    if (isset($fields['unique_number'])) {
      // Use the ID as defined in the annotation of the constraint definition
      $fields['unique_number']->addConstraint('unique_integer', []);
    }
  }
}

Using options

It is possible to have so-called options for your constraint. To do so, first add a public property for your new option to the constraint class. E.g., if you add a new public property $count, the name of your option would be 'count'. You may also implement the methods Constraint::getRequiredOptions() and Constraint::getDefaultOption(), if needed.

Once you have done that, you can pass values for these options to the constraint when you add a new constraint to a field or an entity. The second parameter of the addConstraint() method shown in step 3 is an array of option values keyed by option name. For example, to pass a count option with value 3 to a constraint: addConstraint('example_constraint', ['count' => 3]);

To access the values of any constraint options in your constraint validator, just access the property you created, e.g. $constraint->count for the count option.

Using different error messages for singular and plural

If you need to show different error messages for singular and plural of a value, first adjust the error message of your constraint so that it uses a pipe between the singular and the plural form (syntax: <singular>|<plural>): '%field_name must have at least %count value.|%field_name must have at least %count values.'. Then, in your constraint validator, use the following code to add your violation:

if (count($items) < $constraint->count) {
  $this->context->buildViolation($constraint->errorMessage)
    ->setParameter('%field_name', $items->getFieldDefinition()->label())
    ->setParameter('%count', $constraint->count)
    // We will set the value, that is used to determine, if the error message should be shown
    // in singular or plural form using ConstraintViolationBuilderInterface::setPlural().
    ->setPlural((int) $constraint->count)
    ->addViolation();
}

Targeting specific properties in complex field widgets

If you are working with fields/field widgets using multiple values or properties and you want to target a specific property or your constraint is validated and fails correctly, but the error message is not showing up on the form or the field marked is not the expected field, take a look at the property path of your violation. For constraints validating fields, this is just the field name by default. You may need to extend that to target a specific delta or property in the widget. Use ConstraintViolationBuilderInterface::atPath() to do that. Check out ValidReferenceConstraintValidator::validate() for an example.