A trait for always validating entities on save
Drupal features a powerful entity (more broadly, typed data) entity validation API, powered by the Symfony Validation component. Problem is, Drupal hardly ever uses it.
One might assume that entities are valdiated on save, but by-in-large they are not. This
behavior is opt-in, by calling FieldableEntityInterface::setValidationRequired()
,
but core only “sort of” uses it in ContentEntityForm. I say “sort of,” because the validation
is performed in the context of validating the form, and the flag only appears to be set
in the spirit of completeness during that operation.
But why not validate more? I can understand how this would be a major change in Drupal core, but there’s nothing stopping you from implementing a fail-earlier approach in your own codebase. In fact, if you’re utilizing JSON:API module, posting new content already validates the full entity, not just provided fields.
A trait to always validate an entity on save
<?php
declare(strict_types=1);
namespace Kinksters;
use Drupal\Core\Entity\EntityStorageInterface;
use Symfony\Component\Validator\ConstraintViolation;
/**
* Trait for content entities requiring validation prior to save.
*/
trait ContentEntityRequiringValidationTrait {
/**
* Pre-save method for performing validation.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* Entity storage.
*/
public function preSave(EntityStorageInterface $storage): void {
$this->setValidationRequired(TRUE);
if (!$this->validated) {
$validation = $this->validate();
if (count($validation) > 0) {
$message = sprintf(
'%s failed validation: %s',
$this->getEntityType()->getLabel(),
implode(', ', array_map(
fn (ConstraintViolation $violation) => $violation->getMessage(),
iterator_to_array($validation)
))
);
throw new \LogicException($message);
}
}
parent::preSave($storage);
}
}
Now, simply use ContentEntityRequiringValidationTrait;
in your content entity class,
and the preSave()
method will validate the entity. The parent method will further
ensure the validation actually occurred, or throw its own LogicException
.
Wait, what about PATCH
ing an invalid entity with JSON:API?
It’s important to be able to still PATCH
fields on an otherwise invalid entity over
JSON:API, otherwise you’d be stuck with no way to remedy the incompleteness.
This trait won’t block this because JSON:API performs validation of the entity
in EntityResource::patchIndividual()
,
but only throws exceptions if the explicitly patched fields fail. The entity’s
internal validated
property is set, meaning our ::preSave()
implementation
above will not perform a second, full entity validation.