PHP
Article

Drupal 8 Entity Validation and Typed Data Explained

By Daniel Sipos

Drupal 8 Entity Validation

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.

Drupal 8 logo

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 the FieldItemListInterface implementation (the list)
  • to the individual FieldItemDataDefinition items, which are, as we saw, a type of complex data definition for the FieldItemInterface 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!

Daniel Sipos
Meet the author
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.
  • http://exclusive-paper.com JanySmool

    Hello, iot is very cool artical! I want to know everything abou Drupal! Is it try, that Drupal is more better than PHP or WordPress?! Maybe you can advice my something real cool for beginners! I would like to learn something interesting about it) Thank you

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.