As good developers, we might add helpful guidelines in forms, for example, “Please enter a 10 digit phone number”. But this doesn’t guarantee that the user will actually follow them. Drupal’s form API helps validate user input, but form validations are for “validating forms” – they don’t work when entities are saved programatically or through web services.
Thus, the Entity Validation API ensures that all entities pass the validation criteria no matter how they’re being saved – it’s not limited to form submissions. Now, let’s see how to implement custom entity validation.
Two Minute Version
- Refer to the sample code provided with this module.
- Plan your constraint and think of a short machine-name for it in
PascalCase
.- For this tutorial, we use the name
NotEmptyWhenPublished
.
- For this tutorial, we use the name
- Create a custom module to contain custom code.
- For this tutorial, we use a module named
custom_validation
.
- For this tutorial, we use a module named
- Create the following classes in the
custom_validation\Plugin\Validation\Constraint
namespace as per the sample code:NotEmptyWhenPublished
– Contains error messages for the constraint.NotEmptyWhenPublishedValidator
– Contains code for handling the validation. Especially, thevalidate()
method.
- Add the constraint to an entity or a field using one of the following hooks:
hook_entity_base_info_alter()
hook_entity_bundle_field_info_alter()
hook_entity_type_alter()
Prerequisites
- Know the fundamentals of OOP with PHP – namespaces and classes.
- Know some basic concepts of Drupal – entities, fields and plugins.
- Know how to create a custom module in Drupal 8.
Step 1: Getting started
Let’s take the example of a constraint that I wrote for this website. Editors can create articles without cover images, but they cannot publish without one.
Start by creating a module named custom_validation
– you can name the module as you like.
Step 2: Defining a constraint
First, we define a constraint by creating the following class in a custom module.
## Filename: custom_validation/src/Plugin/Validation/Constraint/NotEmptyWhenPublished.php
namespace Drupal\custom_validation\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Requires a field to have a value when the entity is published.
*
* @Constraint(
* id = "NotEmptyWhenPublished",
* label = @Translation("Not empty when published", context = "Validation"),
* type = "string"
* )
*/
class NotEmptyWhenPublished extends Constraint {
public $needsValue = '%field-name field cannot be empty at the time of publication.';
}
The constraint simply contains metadata and error messages. Drupal detects this constraint because of the @Constraint
annotation. The constraint may also contain more than one messages.
Note: See the section about constraint options to learn how to use them.
Step 3: Defining a validator
Next, we define a validator – a class responsible for enforcing the validation.
## Filename: custom_validation/src/Plugin/Validation/Constraint/NotEmptyWhenPublishedValidator.php
namespace Drupal\custom_validation\Plugin\Validation\Constraint;
use Drupal\Core\Entity\EntityPublishedInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the NotEmptyWhenPublished constraint.
*/
class NotEmptyWhenPublishedValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->context->getRoot()->getValue();
if (
// If the entity can be published.
$entity instanceof EntityPublishedInterface &&
$entity->isPublished() &&
$value->isEmpty()
) {
$this->context->addViolation($constraint->needsValue, [
'%field' => $value->getFieldDefinition()->getLabel(),
]);
}
}
}
Depending on the scope of your constraint, the $value
will contain one of the following:
- An instance of
FieldItemListInterface
if you add your constraint to a field of an entity. It will contain a list of values present in the field. - An instance of
EntityInterface
if you add your constraint to an entity type. It will contain the entire entity being validated. - A scalar value, say, a string or a number depending on the property to which you add the constraint.
Now, we’re left with the last step, i.e. applying the constraint to the field which we want to validate.
Step 4: Applying the constraint
Drupal allows us to apply constraints in multiple ways, depending on the use case.
Scenario A: Applying constraints to base fields of entities you have defined
If you’ve created your own entity type, you can apply constraints to base fields in the baseFieldDefinitions()
method.
// See FieldableEntityInterface::baseFieldDefinitions().
public static function baseFieldDefinitions(EntityTypeInterface $entityType) {
// ...
$fields['subtitle'] = BaseFieldDefinition::create('string')
->setLabel(t('Subtitle'))
->setDescription(t('A text that accompanies the primary title.'));
->addConstraint('NotEmptyWhenPublished');
// ...
}
Scenario B: Applying constraints to fields of entities you’ve not defined
Even though you didn’t define an entity, you can still add your constraints to it. To do this, you need to implement one of the following hooks, depending on the field you want to validate.
Entity base fields
Use hook_entity_base_field_info_alter() to add constraints to base fields defined by the entity. These fields are available for all bundles of the entity. For example: title
and path
are available for all nodes, irrespective of their bundle.
Entity bundle fields
Use hook_entity_bundle_field_info_alter() to add constraints to fields added to an entity bundle. These are mostly the fields added using the field module. For example: an image field – field_image
– which is only available for article nodes.
Here’s how a constraint was added to article nodes for this website:
## See hook_entity_bundle_field_info_alter().
function custom_validation_entity_bundle_field_info_alter(
array &$fields,
EntityTypeInterface $entity_type,
$bundle
) {
if (
$entity_type->id() === 'node' &&
$bundle === 'article' &&
isset($fields['field_image'])
) {
$fields['field_image']->addConstraint('NotEmptyWhenPublished', []);
}
}
Scenario C: Applying constraints to an entity type as a whole (not fields)
Use hook_entity_type_alter() to add constraints to an entity as a whole. This helps add constraints which span over multiple fields. For example, for a user profile either field_phone_fixed
or field_phone_mobile
must have a value. This can be achieved by adding a constraint to an entity type instead of a field.
## See hook_entity_type_alter().
function custom_validation_entity_type_alter(array &$entity_types) {
if (isset($entity_types['user'])) {
$entity_types['user']->addConstraint('PhoneNumberRequired');
}
}
The PhoneNumberRequiredValidator::validate()
method will receive the user entity being validated. It can then ensure that at least one phone number is present.
Constraint options
It is also possible to “options” for constraints. Here’s a quick example of a AgeRange
constraint which takes options min
and max
to check if the value lies in a given range.
## In class AgeRangeConstraint.
class AgeRangeConstraint {
// ...
public $min;
public $max;
public $mustRespectAgeRange = 'Age must be between @min and @max.';
// ...
}
## In class AgeRangeConstraintValidator.
class AgeRangeConstraintValidator {
public function validated($value, Constraint $constraint) {
// ...
// Here's how you access the constraint options.
if ($age < $constraint->min || $age > $constraint->max) {
$this->context->addViolation(
$constraint->mustRespectAgeRange,
['min' => $constraint->min, 'max' => $constraint->max]
);
}
}
}
## While adding the constraint.
$fields['field_age']->addConstraint(
'AgeRange',
['min' => 18, 'max' => 65]
);
This increases the reusability of the constraint – you can apply it to different fields with different options instead of having to create multiple similar constraints.
Singular and plural messages
Sometimes it might be necessary to show a singular or a plural error message depending on the context. This can be achieved as follows:
# In class WordCount Constraint.
class WordCount {
public $min;
// Format: "singlular message|plural message"
public $errorMessage = '%field must have at least %count word.|%field must have at least %count words.'
}
# In class WordCountValidator.
if (count($words) < $constraint->min) {
$this->context
->buildViolation($constraint->errorMessage)
->setParameter('%field', $items->getFieldDefinition()->label())
->setParameter('%count', $constraint->count)
// This sets the number that determines if the error message should be
// singular or plural using ConstraintViolationBuilderInterface::setPlural().
->setPlural((int) $constraint->count)
->addViolation();
}
Conclusion
The Drupal Entity Validation API allows us to define constraints which can then be applied to one or more fields across multiple entities in a uniform manner. This allows for consistent validation of entities irrespective of the method used to create / update them.
What's Next?
- See the source code for Entity Validation API example on GitHub.
- Read the official Drupal Entity Validation API documentation.
- Read about the Drupal Entity API.
- Use the Entity Validation API and let us know how it goes.