Data validation is a very important part of any application. Drupal 7 has a great Form API that can handle complex validation of submitted data, which can then be turned into entities. However, form level validation is problematic. For example, it becomes difficult to handle entity (data) validation programmatically. We have to either reimplement validation logic or mimic form submissions in code. This makes any data interaction dependent on the form system and this is a bad idea.
With the introduction of subsystems such as the REST API, Drupal 8 needed something better to handle this problem. The Symfony Validation component was chosen and Drupal 8 builds on top of it to tailor for the realties of the Typed Data and plugin based Entity system. So now, form submissions validate entities just like REST calls do or any other programmatic interaction with the entities easily can.
In this article, and its followup, we will explore the Drupal 8 Entity Validation API, see how it works, and how it can be extended. To better understand this, we will also take a look at the Typed Data API which underpins the entity system in Drupal 8. There will be some code examples that already exist in core but we will also write a bit of our own code which can be found in this git repository within the demo
module.
Typed Data
Typed Data is the Drupal 8 API that was built to provide a consistent way of interacting with data or metadata about the data itself. Why is this important for our topic? Because validation is defined and invoked on typed data objects.
Two important components of this API stand out: the data definition and the DataType
plugins. The role of the former is to define data and how interaction works with it (including things like settings or validation constraints). The role of the latter is to provide a way to get and set values from that type of data. When they are instantiated, data type plugins make use of data definition instances passed on by the plugin manager. The latter can also infer which data definition needs to be used by a DataType
plugin type.
Let’s see an example:
$definition = DataDefinition::create('string')
->addConstraint('Length', array('max' => 20));
We created a string
data definition and applied the Length
constraint to it. Constraints are a key part of the validation API and they define the type of validation that will run on the data. They are coupled with a validator that actually performs the task, but we will see more about constraints and validators in the second part of this series.
Next, we use the DataType
plugin manager to create the actual data type plugin instance:
$string_typed_data = \Drupal::typedDataManager()->create($definition, 'my string');
We loaded the manager service statically here for brevity but you should use dependency injection in your project if you are in a class context. The create()
method on the TypedDataManager
takes the data definition as the first parameter, the actual value as its second and returns a DataType
plugin instance of the type that matches the definition: in our case StringData
. For complex data, DataType
plugins can specify in their annotations which data definition class they need to use. And for more information about plugins in Drupal 8, make sure you check out my previous articles on the topic.
One of the methods on this plugin instance is validate()
, which triggers data validation against all the constraints that have been applied to the definition and returns an instance of Symfony’s ConstraintViolationList. By iterating over it or using methods like count()
or get()
we can check if the data is valid and which constraints have failed if not.
In our example, the violation list should have no validation errors because the string we used is under 20 characters. Had we used a longer string when creating the plugin, we would have had one violation represented by a Symfony ConstraintViolationInterface
instance with a message, offending value and even a property path.
Typed Data and Content Entities
Now that we know a bit about the Typed Data API, let’s see how this applies to content entities.
Entity data in Drupal 7 is split between entity properties (usually the columns on the entity table) and Field API fields that are configured through the UI. In Drupal 8, they have been brought under the same umbrella so the old properties become fields as well. However, a difference still remains in that some fields (mainly the old entity properties) are defined as base fields while the rest are configurable fields.
The data definitions for these fields are BaseFieldDefinition
and FieldConfig
, but they are both implementors of the same FieldDefinitionInterface
(which is a complex extender of the DataDefinitionInterface
– the interface directly implemented by DataDefinition
we saw earlier).
Each individual field holds data in a special FieldItemListInterface
implementation and is always a list of individual FieldItem
plugins (even if there is only one real value in the field). Each of these plugins extends a DataType
plugin and uses a type of DataDefinitionInterface
implementation itself (usually FieldItemDataDefinition
). Things are quite complex at this level so it’s well worth taking a closer look at the makeup of entity fields to better understand this architecture.
Adding Entity and Field Constraints
Constraints in Drupal 8 are also plugins which usually hold a small amount of information about how data is actually being validated, what error message should be used in case of failure and any additional options the validator needs. The validator class (which is referenced by the constraint) is responsible for checking the data. We’ve seen one example, Length
, which is in fact the LengthConstraint
class validated directly by Symfony’s LengthValidator
class. We will see in the second part how to create our own constraint and validator. For now, though, let’s see how we can add existing constraints to content entities and fields.
Entity level constraints
Entity level constraints are added in the annotation of the entity class itself. For example, this is how the Comment
entity has defined a constraint for its name/author fields combination:
...
constraints = {
"CommentName" = {}
}
...
In this example, CommentName
is the plugin ID of the constraint that will be validated against when saving a comment entity. The opening and closing braces means that the plugin takes no options.
If we wanted to add to or remove a constraint from an existing entity, we’d have to implement hook_entity_type_alter()
:
function demo_entity_type_alter(array &$entity_types) {
/** @var \Drupal\Core\Entity\ContentEntityType $node */
$node = $entity_types['node'];
$node->addConstraint('ConstraintPluginName', ['array', 'of', 'options']);
}
In this example, we are adding a fictitious constraint to the Node entity and passing an array of options to it.
Field level constraints
There is more than one way to add field level constraints depending on whether the content entity type is defined in our module and whether the type of field we are talking about is a base field or configurable.
If we are defining our own entity type, one of the methods on the actual entity class that we have to implement is baseFieldDefinitions()
. This is where we return an array of BaseFieldDefinition
field definitions and we can easily add our constraints there. For example, this is how the Node ID base field is being defined:
$fields['nid'] = BaseFieldDefinition::create('integer')
->setLabel(t('Node ID'))
->setDescription(t('The node ID.'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
Similarly to how we added constraints to the DataDefinition
instance earlier, the Node entity could also add constraints to the Node ID field, more specifically:
- to the
BaseFieldDefiniton
itself, which is, as we saw, the definition for theFieldItemListInterface
implementation (the list) - to the individual
FieldItemDataDefinition
items, which are, as we saw, a type of complex data definition for theFieldItemInterface
implementations (the items)
If we want to add a constraint to a Node base field (or of any other content entity type not defined by our module), we have to implement hook_entity_base_field_info_alter()
and add our constraint there:
function demo_entity_base_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type) {
if ($entity_type->id() === 'node') {
/** @var \Drupal\Core\Field\BaseFieldDefinition $title */
$title = $fields['title'];
$title->addPropertyConstraints('value', ['Length' => ['max' => 5]]);
}
}
In the example above, we are adding a constraint to the Node title field to make sure that no title can be longer than 5 characters. To notice is that we are using the addPropertyConstraint()
method instead of the addConstraint()
one we saw earlier. This is because we are not targeting the definition for the list of items but the individual item definitions themselves (FieldItemDataDefinition
).
By using the addConstraint()
method, we are adding a constraint to the entire list of items. This means that when the constraint gets validated, the validator receives the entire list not just the value of the individual item.
If we want to add a constraint to a configurable field, the process is quite similar. The difference is that we need to implement hook_entity_bundle_field_info_alter()
and work with FieldConfig
instances instead of BaseFieldDefinition
.
If we want to inspect the constraints that are already set on the field, we can do something like this:
$title = $fields['title'];
$constraints = $title->getConstraints();
$property_constraints = $title->getItemDefinition()->getConstraints();
In this example, $title
is either an instance of BaseFieldDefinition
or FieldConfig
. The $constraints
array is, as expected, a list of constraints applied to the entire list of field items while the $property_constraints
is an array of constraints applied to the individual field items themselves. We can notice, though, that if we run this code after we’ve applied the Length
constraint, we will find inside $property_constraints
a ComplexData
constraint which wraps over the individual constraints applied to the field values. This is because there can be multiple individual data definitions for a single field item and Drupal by default groups them like so.
Conclusion
In this article, we’ve started looking at the Entity Validation API in Drupal 8. To this end, we also had to get a sense of the Typed Data API which is used as a foundation for the entity system. Once that became a bit clearer, we’ve seen how constraints can be added to various types of data definitions, including those used by entities and fields.
In the next part, we will look at how the actual validation works and how handling the violations that may occur should be done. Additionally, we will create our own constraint and validator and apply our knowledge from this part to various data definition types. Fun!
Frequently Asked Questions on Drupal 8 Entity Validation and Typed Data
How can I validate data in Drupal 8?
In Drupal 8, data validation is done using the Entity Validation API. This API provides a set of constraints that you can apply to your entities and fields to ensure that the data entered by the user meets certain criteria. For example, you can use the ‘NotNull’ constraint to ensure that a field is not left empty, or the ‘Length’ constraint to ensure that a text field contains a certain number of characters. To apply these constraints, you need to define them in your entity’s baseFieldDefinitions() method.
What is Typed Data in Drupal 8?
Typed Data in Drupal 8 is a system that provides a consistent way to interact with data. It allows developers to define the data type of a particular piece of data, such as a string, integer, or boolean, and then interact with that data in a consistent way, regardless of where it comes from. This makes it easier to write code that works with data, as you can rely on the data behaving in a certain way based on its type.
How can I create custom validation constraints in Drupal 8?
To create a custom validation constraint in Drupal 8, you need to create a new class that extends the Constraint class and defines the validation logic. This class should contain a validate() method, which is called when the constraint is checked. If the data does not meet the constraint, the validate() method should add a violation to the execution context.
How can I apply validation constraints to a form in Drupal 8?
To apply validation constraints to a form in Drupal 8, you need to add the constraints to the form fields in the form’s buildForm() method. You can do this using the ‘#constraints’ property of the form field array. This property should be an array of constraint objects, which are created using the Constraint class or one of its subclasses.
How can I validate a custom entity in Drupal 8?
To validate a custom entity in Drupal 8, you need to define the validation constraints in the entity’s baseFieldDefinitions() method. This method should return an array of field definitions, each of which can have an array of constraints. The constraints are checked when the entity is saved, and if any of them fail, the save operation is aborted and an error message is displayed.
How can I validate a field programmatically in Drupal 8?
To validate a field programmatically in Drupal 8, you can use the validate() method of the field’s TypedData instance. This method checks all of the field’s constraints and returns a list of violations. You can then iterate over this list to handle the violations as needed.
What is the difference between form validation and entity validation in Drupal 8?
Form validation in Drupal 8 is used to check the data entered by the user in a form before it is submitted. Entity validation, on the other hand, is used to check the data of an entity before it is saved. While form validation is specific to a particular form, entity validation applies to all instances of an entity, regardless of where the data comes from.
How can I create a custom validation constraint for a specific field in Drupal 8?
To create a custom validation constraint for a specific field in Drupal 8, you need to define the constraint in the field’s baseFieldDefinitions() method. This method should return an array of field definitions, each of which can have an array of constraints. The constraints are checked when the entity is saved, and if any of them fail, the save operation is aborted and an error message is displayed.
How can I handle validation errors in Drupal 8?
In Drupal 8, validation errors are handled by the ViolationList class. This class provides methods for iterating over the list of violations and retrieving the violation messages. You can use these methods to display the error messages to the user or log them for debugging purposes.
How can I use the Typed Data API in Drupal 8?
The Typed Data API in Drupal 8 provides a consistent way to interact with data. To use this API, you need to create a TypedData instance for your data. This instance provides methods for getting and setting the data, validating the data, and getting metadata about the data, such as its type and constraints.
Daniel Sipos is a Drupal developer who lives in Brussels, Belgium. He works professionally with Drupal but likes to use other PHP frameworks and technologies as well. He runs webomelette.com, a Drupal blog where he writes articles and tutorials about Drupal development, theming and site building.