The last thing we are going to talk about in this chapter is entity validation and how we can make sure that field and entity data as a whole contains valid data. When I say valid, I don't mean whether it complies with the strict TypedData definition but whether, within that, it complies with certain restrictions (constraints) we impose on it.
Drupal 8 uses the Symfony Validator component for applying constraints and then validating entities, fields and any other data against those constraints. I do recommend that you check out the Symfony documentation page on this component to better understand its principles. For now, let's quickly see how it is applied in Drupal 8.
There are three main parts to a validation--a constraint plugin, a validator class and potential violations. The first is mainly responsible for defining what kind of data it can be applied to, the error message it should show, and which validator class is responsible for validating it. If it omits the latter, the validator class name defaults to the name of the constraint class with the word Validator appended to it. The validator, on the other hand, is called by the validation service to validate the constraint and build a list of violations. Finally, the violations are data objects which provide helpful information about what went wrong in the validation things like the error message from the constraint, the offending value, the path to the property that failed. To better understand things, we have to go back to the TypedData and see some simple examples because that is the level at which the validation happens.
So, let's look at the same example I introduced TypedData with:
$definition = DataDefinition::create('string');
$definition->addConstraint('Length', ['max' => 20]);
The data definitions have methods for applying and reading constraints. If you remember, one of the reasons why we need this API is to be able to enrich data with meta information. Constraints are such information. In the preceding example, we are applying a constraint called Length (the plugin ID) with some arbitrary parameters expected by that constraint (in this case a maximum length but also a minimum would work). Having applied this constraint,we are essentially saying that this piece of string data is only valid if it's shorter than 20 characters. We can use this, like so:
/** @var TypedDataInterface $data */
$data = \Drupal::typedDataManager()->create($definition, 'my value that is too long');
$violations = $data->validate();
DataType plugins have a validate() method on them which uses the validation service to validate their underlying data definition against any of the constraints applied to it. The result is an instance of ConstraintViolationList iterator that contains a ConstraintViolationInterface instance for each validation failure. In the preceding example, we should have a violation from which we can get some information like so:
/** @var ConstraintViolationInterface $violation */
foreach ($violations as $violation) {
$message = $violation->getMessage();
$value = $violation->getInvalidValue();
$path = $violation->getPropertyPath();
}
The $message is the error message that comes from the failing constraint, the $value is the actual incorrect value, and the $path is a string representation of the hierarchical path down to the value that has failed. If you remember our license plate example or the content entity fields, TypedData can be nested, which means you can have all sorts of values at different levels. In our previous example, the $path is, however, going to be "" (an empty string) because the data definition has only one level.
Let's revisit our license plate example and see how such a constraint would work there. Imagine we wanted to add a similar constraint to the state code definition:
$state_code_definition = DataDefinition::create('string');
$state_code_definition->addConstraint('Length', array('max' => 2));
// The rest of the set up code we saw earlier.
/** @var Map $plate */
$plate = \Drupal::typedDataManager()->create($plate_definition, ['state' => 'NYC', 'number' => '405-307']);
$violations = $plate->validate();
If you look closely, I instantiated the plate with a state code longer than two characters. Now, if we ask our individual violations for the property path, we get state, because that is what we called the state definition property within the bigger map definition.
Finally, let's see an example of validating constraints on entities. First of all, we can run the validate() method on an entire entity which will then use its TypedData wrapper (EntityAdapter) to run a validation on all the fields on the entity + any of the entity level constraints. The latter can be added via the EntityType plugin definition. For example, the Comment entity type has this bit:
* constraints = {
* "CommentName" = {}
* }
This means that the constraint plugin ID is CommentName and it takes no parameters (since the braces are empty). We can even add constraints to entity types that do not belong to us by implementing hook_entity_type_alter(), for example:
function my_module_entity_type_alter(array &$entity_types) {
/** @var ContentEntityType $node */
$node = $entity_types['node'];
$node->addConstraint('ConstraintPluginID', ['option']);
}
Going one level below and knowing that content entity fields are built on top of the TypedData API, it follows that all those levels can have constraints. We can add the constraints regularly to the field definitions or in the case of either fields that are not "ours" or configurable fields, we can use hooks to add constraints. Using hook_entity_base_field_info_alter() , we can add constraints to base fields while with hook_entity_bundle_field_info_alter(), we can add constraints to configurable fields (and overridden base fields). Let's see an example of how we can add constraints to the Node ID field:
function my_module_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
if ($entity_type->id() === 'node') {
/** @var BaseFieldDefinition $nid */
$nid = $fields['nid'];
$nid->addPropertyConstraints('value', ['Range' => ['mn' => 5, 'max' => 10]]);
}
}
As you can see, we are still just working with data definitions. One thing to note, however, is that when it comes to base fields and configurable fields (which are lists of items), we also have the addPropertyConstraints() method available. This simply makes sure that whatever constraint you are adding is targeted towards the actual items in the list (specifying which property), rather than the entire list as it would have happened we had used the main addConstraint() API. Another difference with this method is that constraints get wrapped into a ComplexDataConstraint plugin. However, you don't have to worry too much about that, just be aware when you see it.
We can even inspect the constraints found on a data definition object. For example, this is how we can read the constraints found on the Node ID field:
$nid = $node->get('nid');
$constraints = $nid->getConstraints();
$item_constraints = $nid->getItemDefinition()->getConstraints();
Where the getConstraints() method returns an array of constraint plugin instances. Now let’s see, though, how we can validate entities:
$nid = $node->get('nid');
$node_violations = $node->validate();
$nid_list_violations = $nid->validate();
$nid_item_violations = $nid->get(0)->validate();
The entity level validate() method returns an instance of EntityConstraintViolationList which is a more specific version of the ConstraintViolationList we talked about earlier. The latter is, however, returned by the validate() method of the other cases given in the following. But for all, inside we have a collection of ConstraintViolationInterface instances from which we can learn some things.
The entity level validation goes through all the fields and validates them; this means that is where we will get most violations (if that's the case). Next, the list will contain violations of any of the items in the list while the item will contain only the violation on that individual item in the list. The property path is something interesting to observe the following is the result of calling getPropertyPath() on a violation found in all three of the resulting violation lists preceding:
nid.0.value
0.value
value
As you can see, this reflects the TypedData hierarchy. When we validate the entire entity, it gives us a property path all the way down to the value--field name -> delta (position in the list) -> property name. Once we validate the field, we already know what field we are validating so that is omitted. And the same goes for the individual item (we know also the delta of the item).
A word of warning about base fields that can be overridden per bundle such as the Node title field. As I mentioned earlier, the base definition for these fields use an instance of BaseFieldOverride, which allows certain changes to be made to the definition via the UI. In this respect, they are very close to configurable fields. The "problem" with this is that, if we tried to apply a constraint like we just did with the nid to, say, the Node title field, we wouldn't have gotten any violations when validating. This is because the validator performs the validation on the BaseFieldOverride definition rather than the BaseFieldDefinition.
This is no problem, though, as we can use hook_entity_bundle_field_info_alter() and do the same thing as done before which will then apply the constraint to the overridden definition. In doing so, we can also account for the bundle we want this applied to. This is the same way to apply constraints to a configurable field you create in the UI.