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 comment_form_validate()
.
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 \StdClass
objects; 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:
$node->field_make_it_stop['LANGUAGE_NONE'][0]['wtf_bbq'];
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:
$node->field_rainbows->value; $node->title->value;
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.
Each FieldItemListInterface
consists of one or more FieldItemInterface
objects.
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 validate
method:
$violations = $node->validate();
That will give you an instance of EntityConstraintViolationListInterface
which helpfully implements \Countable
and \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:
- NotNull;
- Length (supporting minimum and maximum);
- Count;
- Range (for numeric values);
- IsNull;
- Email;
- AllowedValues;
- 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 setRequired(TRUE)
and 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()
or 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 addConstraint
or 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 setPropertyConstraints
or 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.'), ], ], ]);
Multiple Fields
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 ContentEntityInterface
, 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.)
Wrapping Up
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!
Image: celebration of light 2007 by Jon Rawlinson is licensed under CC BY 2.0