Tips for Implementing Data Validation in Drupal 8

Blue State Digital knows Drupal. Increasingly, we’re recommending Drupal 8 to our clients as a way to future-proof development and take advantage of the more flexible content model and templating system.

The Entity Validation API is new to Drupal 8 — if you’re moving from Drupal 7, you may have checked that data includes all fields and that content is in the right format within a form validation function. Drupal 8’s new approach allows that data to be validated even if the entity is created programmatically, without a user submitting a form. Drupal’s own documentation is a great start, but doesn’t cover more complex validation examples.

In Drupal 8, you can create additional validation rules that will be checked any time the validate() method is called (for example, when creating or editing the entity). Below, I’ve used the example of a donation ask with a maximum value using Drupal Commerce. However, you could easily apply this methodology to any field that requires data validation.

Create the Constraint

The Constraint is where we’ll introduce a new validation rule and set up the error message:

  1. In a custom module, create the directory src/Plugin/Validation/Constraint.
  2. Create a new PHP file with the name of your rule, e.g. PriceBelowMaxConstraint.
  3. Create a class in the namespace Drupal\<your module>\Plugin\Validation\Constraint that extends Symfony\Component\Validator\Constraint.
  4. Add the annotation. As with most Drupal plugins, Constraints (validation rules) are registered and defined via an annotation.
 /**
 * @Constraint(
 *   id = "SomeUniqueID"
 *   label = @Translation("Some human-readable descriptor", context="Validation")
 * )
 */

The annotation contains just two items, the ID and the label. The ID will be used later to attach the constraint to a field.

The Constraint class contains one or more public properties that define the error message when validation fails. For instance, if a field must contain a unique integer, you could define multiple messages, one to display if the field is not an integer and a different one to display if it is not unique, as done here. The message can contain placeholders that will be filled in when the error is set, the same way that t() can.

Altogether, the Constraint class will look something like this:

namespace Drupal\my_module\Plugin\Validation\Constraint;
use Drupal\Core\Annotation\Translation;
use Symfony\Component\Validator\Constraint;
 
/**
 * @Constraint(
 *   id = "PriceBelowMax",
 *   label = @Translation("Donation amount is below the maximum", context="Validation")
 * )
 */
class PriceBelowMaxConstraint extends Constraint {
  public $message = 'The donation amount must be below %value';
}

Create the ConstraintValidator

The ConstraintValidator is where we’ll define the actual validation function:

  1. In the same Constraint directory where you created the first file, create a second one, e.g. PriceBelowMaxConstraintValidator.
  2. Create a class in the same namespace as before (Drupal\<your module>\Plugin\Validation\Constraint) and extend the class Symfony\Component\Validator\Constraint\Validator.

The new class must implement the method validate, which has two parameters: $items (the value(s) put into the field) and $constraint (the Constraint being validated, i.e. the one you just created). If the rule might be used on a field with multiple values, loop through $items and check each value. If you know there will only ever be one value, you can retrieve it with $items->first(). You can retrieve the entity to which the field is attached with $items->getEntity().

In the PriceBelowMax example, we use this to check the current value against a maximum that has been defined on a related entity. You can also use entityQuery or other query methods to check the value against something in the database. For instance, if a value must be unique, you could use query for any existing values that match, as DrupalCommerce does for SKUs.

If the item you are checking is not valid, add a violation using $this->context->addViolation(). addViolation() takes two parameters: the error message ($constraint-><property you defined earlier>) and an array of substitutions to fill into that message ([‘%value’ => <the thing you want to fill in>]).

In this example, the donation value (which uses DrupalCommerce’s “unit price”) entered into a field must be less than a maximum value we retrieve from the related ProductVariation entity:

namespace Drupal\my_module\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
 
/**
 * Validates the PriceBelowMax Constraint
 *
 * @package Drupal\projectname_commerce\Plugin\Validation\Constraint
 */
class PriceBelowMaxConstraintValidator extends ConstraintValidator {
 
  /**
   * {@inheritdoc}
   */
  public function validate($items, Constraint $constraint) {
    /** @var \Drupal\commerce_price\Plugin\Field\FieldType\PriceItem $unit_price */
    $unit_price = $items->first();
    /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $product_variation */
    $product_variation = $items->getEntity()->getPurchasedEntity();
    /** @var \Drupal\commerce_price\Price $max */
    $max = $product_variation->getPrice();
    // Compare the entered price with the max
    if ($unit_price->toPrice()->greaterThan($max)) {
      $this->context->addViolation(
        $constraint->message,
        ['%value' => $max->__toString()]
      );
    }
  }
}

Add the Constraint to a Field

If your Constraint is for an entity that you are also creating in a custom module, you can simply add the constraint to the BaseFieldDefinition with ->addConstraint(‘<the ID you set up in the annotation>’). If you are attaching it to an entity defined by another module (including a custom node type, since the Node entity is defined by Core), you will need to alter the field info using hook_entity_base_field_info_alter (note that there are some pending changes to how this hook works, so this might change in a future version of Drupal). If the field is for a specific bundle (i.e. a specific node type), you may want to use hook_entity_bundle_field_info_alter instead. Add the constraint to the field on which you want to enforce the validation rule:

/**
 * Implements hook_entity_base_field_info_alter().
 */
function projectname_commerce_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
  if ($entity_type->id() === 'commerce_order_item') {
    if (!empty($fields['unit_price'])) {
      $fields['unit_price']->addConstraint('PriceBelowMax');
    }
  }
}

And that’s it — now any time validate() is called for the entity, your field will be validated according to the rule you set up.


If you need help with your Drupal project or your database, get in touch with us.