Drupal 8 comes with plenty of new features: the high visibility ones, like CKEditor or Views in core, and those less obvious but equally pivotal to Drupal 8’s strength and flexibility, like the Entity Validation API.
Never heard of it? You're not alone – much of the fanfare around Drupal 8 is devoted to the shiny parts. But under the hood, rock solid developer APIs like the Entity Validation API are what will make Drupal 8 a pleasure to work with for client projects and contributed modules alike.
So what is this Entity Validation API and why should you care?
For Those Who Came in Late
In Drupal versions up to and including Drupal 7, any validation was done in the Form API. Consider the Comment entity, provided by the Comment module. There is a lot of validation relating to comments, such as:
- If the comment is being updated, we confirm the timestamp is a valid date.
- If the comment is being updated and the username is changed, we confirm the username is valid.
- If the comment is anonymous, we validate that the name used for the comment doesn't match an existing username.
- If the comment is anonymous, we confirm that the e-mail address entered is valid.
- If the comment is anonymous, we confirm that the homepage is a valid URL.
In Drupal 7 and earlier all of this happens in the comment form validation logic, in
The issue here is that this validation is tied to a form submission. If you're saving a comment via some other method, then you have to duplicate all this logic to ensure you don’t end up with invalid comment entities. Common alternate methods include:
- using Rules;
- using a Restful, Services, or Rest WS endpoint;
- programmatically saving via custom code;
- using a custom comment form.
The same scenario is repeated for Nodes, Users, Taxonomy terms, and custom Blocks (which aren’t entities per se in Drupal 7, but the story is the same).
It’s Like an Onion, or Maybe a Layer Cake
But before we can talk about Drupal 8's Entity Validation API, we need to go over some background on the Entity API itself.
In Drupal 7, entities are
\StdClassobjects; accessing field values depends on the entity and the field. There is no real unification. For example,
$node->title is a string, while
$node->field_tags is an array.
And so, in Drupal 7 you might see things like this:
In Drupal 8, the Entity Field API brings unified access to field properties and first-class objects for each entity type. So in Drupal 8, you see consistency like this:
When you work with fields and entities in Drupal 8, you’re likely to interact with a suite of interfaces that comprise the API.
Key Interfaces in Drupal 8 Entity Field API
Let’s start with
ContentEntityInterface, the overarching interface that content entities in Drupal 8 implement. (Node, Comment, Taxonomy Term, and BlockContent, among others, implement this interface.)
Each field and each property on a content entity is an instance of
FieldItemListInterface. Even fields like the node title are lists, which just contain a single value.
FieldItemListInterface consists of one or more
At the lowest level of the API, each
FieldItemInterface is comprised of one or more
DataDefinitionInterface objects that make up the properties or columns in each item value.
Perhaps a diagram might make this clearer. (See Diagram 1.)
Note that there are two broad types of fields: base fields and configurable fields. Base fields are the fields that are largely fixed in entity type. Configurable fields are those added during site building using the Field API.
Determining if an Entity is Valid
In Drupal 8, to check if an entity is valid, simply call its
$violations = $node->validate();
That will give you an instance of
EntityConstraintViolationListInterface which helpfully implements
\ArrayAccess. If the resulting violations are empty, the entity is valid.
There are some handy methods on
EntityConstraintViolationListInterface that help you work with any violations, including these:
getEntityViolations()filters those violations at the entity level, but is not specific to any field.
getByFields(array $field_names)filters the violations for a series of fields.
filterByFieldAccess()filters only those violations the current user has access to edit.
As the return implements
\ArrayAccess, you can loop over the results and work with each
ConstraintViolationInterface item in turn:
getMessage()gets the reason for the violation.
getPropertyPath()gets the name of the field in error. For example, if the third tag in field_tags is in error, the property path might be field_tags.2.target_id. These align with the form structure if you're in the context of validating a form.
getInvalidValue()returns the value of the field that is in error.
Interacting With the Entity Field Validation API
To enable an entity or field to be validated, Drupal needs to know which constraints apply to which field or entity type. This is done by using the API to add and modify constraints. Once you've gathered your requirements, you need to attach your validation constraints to your entity types and fields. How and where you do this depends on the type of constraint; whether you define the entity type in your code; the type of field; and the nature of the constraint. Before we address these, let's take a quick look at the anatomy of a constraint.
Constraints are Plugins
Continuing the learn once – apply everywhere paradigm that runs through much of Drupal 8, validation constraints are defined as plugins. Helpfully, core comes with a plethora of existing constraints such as:
- Length (supporting minimum and maximum);
- Range (for numeric values);
- ComplexData (more on that later).
In many cases, you’ll be able to implement your validation logic by combining the existing constraint plugins in core. However, if you do need to create a custom constraint, all that is required is a new constraint plugin.
Adding Constraints to Base Fields
How you add constraints to base fields depends on whether or not your module defines the entity type in question.
If you're dealing with your own entity type, then you will have already implemented
FieldableEntityInterface::baseFieldDefinitions(). In this method, you will already be defining your entity properties as an array using the
BaseFieldDefinition::create() factory and its various builder methods. One such method is
addConstraint. You call this passing the plugin ID for the required constraint as the first argument, and any configuration for the second argument.
This example from Aggregator module adds the FeedTitle constraint to the title property:
$fields['title'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setDescription(t('The name of the feed (or the name of the website providing the feed).')) ->setRequired(TRUE) ->setSetting('max_length', 255) ->setDisplayOptions('form', array( 'type' => 'string_textfield', 'weight' => -5, )) ->setDisplayConfigurable('form', TRUE) ->addConstraint('FeedTitle', );
Here, the constraint plugin being added has an ID of
FeedTitle. Note that this field definition also includes
setSetting('max_length', 255). Behind the scenes, those two calls are also calling
addConstraint, once each for the NotNull and Length plugins.
Finding the Plugin ID
As with every other plugin in Drupal 8, each constraint plugin defines its ID in its annotation. So, in order to determine what to use in the first argument to
addConstraint, it’s simply a matter of opening the required plugin class and inspecting the class-level docblock.
/** * Supports validating feed titles. * * @Constraint( * id = "FeedTitle", * label = @Translation("Feed title", context = "Validation") * ) */
If your module doesn't define the entity type, but you wish to add a constraint to a base field, you need to implement either
hook_entity_base_field_info_alter(). Then, you can add or remove constraints from entity base fields, or even define new base fields – a little-known power feature of Drupal 8.
Adding Constraints to Configurable Fields
This functionality is also possible for configurable fields added by the Field API. Now, that can only be done in code, but if Drupal's history is anything to go by, pretty soon a contributed module will debut which allows site builders to wire up validation in the UI.
To add a constraint to a configurable field, you need to implement
hook_entity_bundle_field_info_alter(). At that point, you have an array of
FieldConfigInterface objects, keyed by field name. For any of these, depending on both the entity type and bundle, you can call
setConstraint to add or replace the field level constraints.
At the Data Type Level
Until now we've dealt with constraints at the
FieldItemInterface level, but what if you need to apply validation at the
DataDefinitionInterface level? For example, the
EntityReferenceItem field item (each item in the list) contains a number of properties. Each of these is a DataDefinitionInterface. In this case, it has the target_id and entity properties. Similarly
TextLongItem, which is used for rich text fields, has two properties in value and format, one to hold the field value and another to track the input format. If you want to add a constraint on a configurable field (
FieldConfigInterface) directly to one of these properties, you can implement
hook_entity_bundle_field_info_alter() and call
addPropertyConstraints, nominating the constraints for the required property name. If you need to do the same for a
BaseFieldDefinition, you can add a new
ComplexData constraint using one of the techniques detailed above. The
ComplexData constraint takes a nested array of constraints keyed by each property name. For example:
$field->addConstraint('ComplexData', [ 'value' => [ 'Length' => [ 'max' => 150, 'maxMessage' => t('Title may not be longer than 150 characters.'), ], ], ]);
In some cases you may need a constraint that depends on the value of more than one field. For example, the Comment entity contains a name and an author field. Whether or not the entity or either of these fields is valid depends on the comment author and the values of those fields. Attaching validation constraints at the entity level depends again on whether your module declares the entity type, or whether you are adding the constraint to another module’s entity type.
For your entity type, entitylevel constraints are added using the entity type annotation, just like most of the other entitylevel metadata. Nominate these in the constraints property of the annotation. This is an array of constraint configuration, keyed by the constraint plugin ID.
To add entity-level constraints for another module, implement
hook_entity_type_build() to add a new constraint, or
hook_entity_type_alter() to alter an existing one.
Creating Your Own Constraint Plugins
To create your own constraint plugin, as with every other plugin in Drupal 8, you need to place a class in the correct folder structure and add an annotation.
Your class needs to live in the
Drupal\your_module\Plugin\Validation\Constraint namespace which corresponds to the src/Plugin/Validation/Constraint folder in your module.
Your class needs to include an
@Constraint annotation, like that shown above for FeedTitle, and would normally extend from
\Symfony\Component\Validator\Constraint. Your constraint plugin really only needs to provide a default value for the violation message, which is a public property called message. You can use the same placeholders as you would for string translations in place of dynamic text.
You then need a validator object. By default, each constraint is validated by a class in the same namespace as the constraint, with the word Validator appended. For example,
CommentNameConstraint is validated by
CommentNameConstraintValidator. If you don't want to use this pattern, or you’re reusing one validator for several constraints, you can override
Constraint::validatedBy() in your constraint plugin to nominate the validator class.
The validator class needs to extend from
ConstraintValidator and implement the
validate() method, receiving the data to validate and the constraint itself as arguments. Depending on the level at which the constraint is attached, the data to validate may be a
FieldItemListInterface, or a primitive value.
If you're creating an entity-level constraint, your constraint plugin must extend from
CompositeConstraintBase instead of
Constraint, and implement the
coversFields() method. (See
CommentNameConstraint in core.)
Having a solid API means that when you approach client projects on Drupal 8, you can think about your domain model first. Normally, during the requirements-gathering process, business rules regarding validation of the client's domain are captured. The new validation API will allow you to think differently about how to implement this logic.
For the first time, you will be able to bake this validation into your entities, whether you use custom entity types or rely on the common building blocks provided by core (Node, Comments, Terms, etc). With validation that isn't tied to form submission, you can write unit tests for your model, which allows you to rapidly iterate and refactor, knowing you've not introduced regressions. Although it may not be one of the most visible features in Drupal 8, the new validation API will change the way we work with Drupal.
Bring it on!