Entity validation in Drupal 8 - part 2

In the first part, we saw how entity validation works in Drupal 8, why it is a separate component and how most parts are adopted from Symfony's entity validation framework. We will try our hands on creating our own custom validator in this post.This validator will fail to create a node if the user ID is not specified. In other words, prevent creation of anonymous nodes.

We've already seen that constraints are Drupal 8 plugins. Let's go ahead and create them by checking out tag entity-level-validation of the repo(fastest), using Drupal Console(fast) or manually(slowest). Either ways, we have 2 classes, the Constraint class which holds the metadata of the constraint.

namespace Drupal\custom_validation\Plugin\Validation\Constraint;

use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;

/**
 * Prevent anon nodes from being created.
 *
 * @Constraint(
 *   id = "PreventAnon",
 *   label = @Translation("Prevent nodes being created by anon users", context = "Validation"),
 *   type = "entity:node"
 * )
 */
class PreventAnonConstraint extends CompositeConstraintBase {

  /**
   * Message shown when an anonymous node is being created.
   *
   * @var string
   */
  public $message = 'Cannot create a node that does not belong to any user.';

  /**
   * {@inheritdoc}
   */
  public function coversFields() {
    return ['uid'];
  }
}

Note that PreventAnonConstraint extends CompositeConstraintBase, which should be the case for constraints at the entity level. We shall see the reason for this shortly. The constaint class also indicates what fields/properties this constraint applies to(just the uid in our example).

…and the validator class which does the actual validation.

namespace Drupal\custom_validation\Plugin\Validation\Constraint;

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

/**
 * Validates the PreventAnon constraint.
 */
class PreventAnonConstraintValidator extends ConstraintValidator {

  /**
   * {@inheritdoc}
   */
  public function validate($entity, Constraint $constraint) {
    if (!isset($entity)) {
      return;
    }
    if (!$entity->getOwnerId()) {
      $this->context->addViolation($constraint->message);
    }
  }
}

The constraint does not take effect until we add it using hooks in our module.

/**
 * Implements hook_entity_type_alter().
 */
function custom_validation_entity_type_alter(array &$entity_types) {
  $node = $entity_types['node'];
  $node->addConstraint('PreventAnon', []);
}

And now, to test out our custom validator.

use Drupal\node\Entity\Node;

$node = Node::create([ 'title' => 'New article', 'type' => 'article']);
$violations = $node->validate();
if ($violations->count() > 0) {
  foreach($violations as $violation) {
    print_r($violation->getMessage()->render());
    print("\n");
    print_r($violation->getPropertyPath());
    print("\n");
  }
}

Here's what we get,

$ drush scr node-validate.php
Cannot create a node that does not belong to any user.
uid

Field level validation

Let's add a custom validator at the field level now. To add a dash of variety, we will also pass arguments to our validator, as to what set of values the field can hold. Imagine I'm running an e-commerce store and I'm having a field called shirt size, which can hold only the following set of values: XXS, XS, S, M, L, XL and XXL.

Check out tag field-level-validation from the repo or create your own using drupal console.

First, the constraint class.

namespace Drupal\custom_validation\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\MissingOptionsException;

/**
 * Shirt sizes.
 *
 * @Constraint(
 *   id = "ShirtSize",
 *   label = @Translation("Shirt size constraint", context = "Validation"),
 * )
 */
class ShirtSizeConstraint extends Constraint {

  /**
   * @var array
   */
  public $sizes;

  /**
   * Message shown when shirt size is incorrect.
   *
   * @var string
   */
  public $message = '(%shirt_size) is an invalid shirt size.';

    /**
     * ShirtSize constructor.
     * @param mixed $options
     */
    public function __construct($options = null)
    {
        if (null !== $options && !is_array($options)) {
            $options = array(
                'sizes' => $options
            );
        }
        parent::__construct($options);
        if (null === $this->sizes) {
            throw new MissingOptionsException(sprintf('The option "sizes" must be given for constraint %s', __CLASS__), ['sizes']);
        }
    }
}

We can pass various allowed shirt sizes as constructor arguments while initializing the constraint.

Next, create a dropdown field(list of text) named field_shirt_size for the article content type. The validator class looks like this:

namespace Drupal\custom_validation\Plugin\Validation\Constraint;

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

/**
 * Validates the ShirtSize constraint.
 */
class ShirtSizeConstraintValidator extends ConstraintValidator {

  /**
   * {@inheritdoc}
   */
  public function validate($field, Constraint $constraint) {
    $value = $field->value;
    if (!isset($value)) {
      return NULL;
    }
    if (!in_array($value, $constraint->sizes)) {
        $this->context->addViolation($constraint->message, array('%shirt_size' => $value));
    }
  }
}

A fairly simple validator. It checks whether the given field value is in the list of values given in the constraint. Let's apply the constraint to the specific field and bundle.

/**
 * Implements hook_entity_bundle_field_info_alter().
 */
function custom_validation_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle) {
  if ($entity_type->id() == 'node' && $bundle == 'article') {
    $fields['field_shirt_size']->addConstraint('ShirtSize', ['sizes' => ['M','L','XL']]);
  }
}

We effectively allow only "M", "L" and "XL" sizes. Let's quickly try to create a node with an invalid size.

$node = Node::create([ 'title' => 'New article - clean', 'type' => 'article']);
$node->uid = 1;
$node->field_shirt_size = 'XS';
$violations = $node->validate();
if ($violations->count() > 0) {
  foreach($violations as $violation) {
    print_r($violation->getMessage()->render());
    print("\n");
    print_r($violation->getPropertyPath());
    print("\n");
  }
} else {
  $node->save();
}

As "XS" is not in our allowed values list, it shoots an error.

$ drush @dev scr f.php
(<em class="placeholder">XS</em>) is an invalid shirt size.
field_shirt_size

I'm not too sure where the <em> tags are coming from, but our custom validation works good enough. This takes precedence even while creating nodes from the node add form. Let's confirm this by creating an article node via UI with an invalid value for shirt size.

In the next part, we shall wrap up the remaining bits and pieces of Drupal 8 entity validation.

Drupal 8 module development

Like what you read?

Then you will definitely love my new book about Drupal 8 module development.