A trait for always validating entities on save

1 minute read

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 PATCHing 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.

Updated: