As Drupal developers, we often need to implement validations and constraints to maintain data integrity. In this tutorial, we'll learn to use Drupal's Entity Validation API to validate entities, fields, properties and also discuss about constraints that check multiple fields / properties.

As good developers, we might add helpful guidelines in forms, for example, “Please enter a 10 digit phone number”. But this doesn’t guarantee that the user will actually follow them. Drupal’s form API helps validate user input, but form validations are for “validating forms” – they don’t work when entities are saved programatically or through web services.

Thus, the Entity Validation API ensures that all entities pass the validation criteria no matter how they’re being saved – it’s not limited to form submissions. Now, let’s see how to implement custom entity validation.

2 Minute Version

  • Refer to the sample code provided with this module.
  • Plan your constraint and think of a short machine-name for it in PascalCase.
    • For this tutorial, we use the name NotEmptyWhenPublished.
  • Create a custom module to contain custom code.
    • For this tutorial, we use a module named custom_validation.
  • Create the following classes in the  custom_validation\Plugin\Validation\Constraint namespace as per the sample code:
    • NotEmptyWhenPublished – Contains error messages for the constraint.
    • NotEmptyWhenPublishedValidator – Contains code for handling the validation. Especially, the validate() method.
  • Add the constraint to an entity or a field using one of the following hooks:
    • hook_entity_base_info_alter()
    • hook_entity_bundle_field_info_alter()
    • hook_entity_type_alter()

Prerequisites

  • Know the fundamentals of OOP with PHP – namespaces and classes.
  • Know some basic concepts of Drupal – entities, fields and plugins.
  • Know how to create a custom module in Drupal 8.

Step 1: Getting started

Let’s take the example of a constraint that I wrote for this website. Editors can create articles without cover images, but they cannot publish without one.

Screenshot of Entity Validation API at work
Screenshot of an error generated by a custom constraint I created for jigarius.com

Start by creating a module named custom_validation – you can name the module as you like.

Step 2: Defining a constraint

First, we define a constraint by creating the following class in a custom module.

## Filename: custom_validation/src/Plugin/Validation/Constraint/NotEmptyWhenPublished.php

namespace Drupal\custom_validation\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * Requires a field to have a value when the entity is published.
 *
 * @Constraint(
 *   id = "NotEmptyWhenPublished",
 *   label = @Translation("Not empty when published", context = "Validation"),
 *   type = "string"
 * )
 */
class NotEmptyWhenPublished extends Constraint {

  public $needsValue = '%field-name field cannot be empty at the time of publication.';

}

The constraint simply contains metadata and error messages. Drupal detects this constraint because of the @Constraint annotation. The constraint may also contain more than one messages.

Note: See the section about constraint options to learn how to use them.

Step 3: Defining a validator

Next, we define a validator – a class responsible for enforcing the validation.

## Filename: custom_validation/src/Plugin/Validation/Constraint/NotEmptyWhenPublishedValidator.php
namespace Drupal\custom_validation\Plugin\Validation\Constraint;

use Drupal\Core\Entity\EntityPublishedInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/**
 * Validates the NotEmptyWhenPublished constraint.
 */
class NotEmptyWhenPublishedValidator extends ConstraintValidator {

  /**
   * {@inheritdoc}
   */
  public function validate($value, Constraint $constraint) {
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $this->context->getRoot()->getValue();
    if (
      // If the entity can be published.
      $entity instanceof EntityPublishedInterface &&
      $entity->isPublished() &&
      $value->isEmpty()
    ) {
        $this->context->addViolation($constraint->needsValue, [
          '%field' => $value->getFieldDefinition()->getLabel(),
        ]);
    }
  }

}

Depending on the scope of your constraint, the $value will contain one of the following:

  1. An instance of FieldItemListInterface if you add your constraint to a field of an entity. It will contain a list of values present in the field.
  2. An instance of EntityInterface if you add your constraint to an entity type. It will contain the entire entity being validated.
  3. A scalar value, say, a string or a number depending on the property to which you add the constraint.

Now, we’re left with the last step, i.e. applying the constraint to the field which we want to validate.

Step 4: Applying the constraint

Drupal allows us to apply constraints in multiple ways, depending on the use case.

After adding the constraint, clear the cache and it will be detected by Drupal.

Scenario A: Applying constraints to base fields of entities you have defined

If you’ve created your own entity type, you can apply constraints to base fields in the baseFieldDefinitions() method.

// See FieldableEntityInterface::baseFieldDefinitions().
public static function baseFieldDefinitions(EntityTypeInterface $entityType) {
  // ...
  $fields['subtitle'] = BaseFieldDefinition::create('string')
    ->setLabel(t('Subtitle'))
    ->setDescription(t('A text that accompanies the primary title.'));
    ->addConstraint('NotEmptyWhenPublished');
  // ...
}

Scenario B: Applying constraints to fields of entities you’ve not defined

Even though you didn’t define an entity, you can still add your constraints to it. To do this, you need to implement one of the following hooks, depending on the field you want to validate.

Entity base fields

Use hook_entity_base_field_info_alter() to add constraints to base fields defined by the entity. These fields are available for all bundles of the entity. For example: title and path are available for all nodes, irrespective of their bundle.

Entity bundle fields

Use hook_entity_bundle_field_info_alter() to add constraints to fields added to an entity bundle. These are mostly the fields added using the field module. For example: an image field – field_image – which is only available for article nodes.

Here’s how a constraint was added to article nodes for this website:

## See hook_entity_bundle_field_info_alter().
## See hook_entity_bundle_field_info_alter().
function custom_validation_entity_bundle_field_info_alter(
  array &$fields,
  EntityTypeInterface $entity_type,
  $bundle
) {
  if (
    $entity_type->id() === 'node' &&
    $bundle === 'article' &&
    isset($fields['field_image'])
  ) {
    $fields['field_image']->addConstraint('NotEmptyWhenPublished', []);
  }
}

Scenario C: Applying constraints to an entity type as a whole (not fields)

Use hook_entity_type_alter() to add constraints to an entity as a whole. This helps add constraints which span over multiple fields. For example, for a user profile either field_phone_fixed or field_phone_mobile must have a value. This can be achieved by adding a constraint to an entity type instead of a field.

## See hook_entity_type_alter().
function custom_validation_entity_type_alter(array &$entity_types) {
  if (isset($entity_types['user'])) {
    $entity_types['user']->addConstraint('PhoneNumberRequired');
  }
}

The PhoneNumberRequiredValidator::validate() method will receive the user entity being validated. It can then ensure that at least one phone number is present.

Constraint options

It is also possible to “options” for constraints. Here’s a quick example of a AgeRange constraint which takes options min and max to check if the value lies in a given range.

## In class AgeRangeConstraint.
class AgeRangeConstraint {
  // ...
  public $min;
  public $max;
  public $mustRespectAgeRange = 'Age must be between @min and @max.';
  // ...
}

## In class AgeRangeConstraintValidator.
class AgeRangeConstraintValidator {
  public function validated($value, Constraint $constraint) {
    // ...
    // Here's how you access the constraint options.
    if ($age < $constraint->min || $age > $constraint->max) {
      $this->context->addViolation(
        $constraint->mustRespectAgeRange,
        ['min' => $constraint->min, 'max' => $constraint->max]
      );
    }
  }
}

## While adding the constraint.
$fields['field_age']->addConstraint(
  'AgeRange',
  ['min' => 18, 'max' => 65]
);

This increases the reusability of the constraint – you can apply it to different fields with different options instead of having to create multiple similar constraints.

Singular and plural messages

Sometimes it might be necessary to show a singular or a plural error message depending on the context. This can be achieved as follows:

# In class WordCount Constraint.
class WordCount {
  public $min;
  // Format: "singlular message|plural message"
  public $errorMessage = '%field must have at least %count word.|%field must have at least %count words.'
}

# In class WordCountValidator.
if (count($words) < $constraint->min) {
  $this->context
    ->buildViolation($constraint->errorMessage)
    ->setParameter('%field', $items->getFieldDefinition()->label())
    ->setParameter('%count', $constraint->count)
    // This sets the number that determines if the error message should be
    // singular or plural using ConstraintViolationBuilderInterface::setPlural().
    ->setPlural((int) $constraint->count)
    ->addViolation();
}

Conclusion

The Drupal Entity Validation API allows us to define constraints which can then be applied to one or more fields across multiple entities in a uniform manner. This allows for consistent validation of entities irrespective of the method used to create / update them.

What's Next?

On this page

Recent articles

Sep 6, 2019

Signing Git Commits and Tags with GPG

Learn to sign your Git commits and tags with GPG to let your team know that they are authentic and that they weren’t maliciously altered by someone.
Aug 9, 2019

Choose a Password: Strong, Secure and Easy to Remember

Ever forgotten a password? Luckily, I haven’t forgotten one in ~5 years! Read about how to choose strong, secure and memorable passwords.
Jul 22, 2019

Doing Transactions Exceeding your Credit Card’s Limit

Do transactions beyond your current credit limit! Learn a workaround for bypassing your credit limit temporarily or increasing it permanently.

Thoughts and comments