Content entities

Let's now 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 plus any of the entity-level constraints. The latter can be added via the EntityType plugin definition (the annotation). 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) {
$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') {
$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 we are adding is targeted toward the actual items in the list (specifying which property), rather than the entire list as it would have happened had we 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 how we can validate entities:

$node_violations = $node->validate(); 
$nid = $node->get('nid');
$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 above. But for all of them, inside we have a collection of ConstraintViolationInterface instances from which we can learn some things about what did not validate.

The entity-level validation goes through all the fields and validates them all. Next, the list will contain violations of any of the items in the list, while the item will contain only the violations 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 from the example above:

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 uses 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 we did 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 you apply constraints to a configurable field you create in the UI.