Skip to main content

Custom validation of multiple, dependent entity fields in Drupal 8

Drupal 8 custom validation of multiple entity fields

In a recent Drupal 8.6 project, I was using the media entity type to add custom fields to pdf documents. These pdf files contain two fields: a select list and a text field. When a specific select option is chosen, the text field becomes required. I could have used a form alter function to add my custom validation for these fields, but this would limit my validation to just one form. This wouldn't work for entities that were created with JSON API or other methods.

I began by reading a few articles online that nudged me in the right direction:

- https://www.drupal.org/docs/8/api/entity-validation-api/providing-a-cus…
- https://www.bluestatedigital.com/ideas/drupal8-data-validation/

These were great in helping me to get my constraints setup within my custom module. However, they were a bit limiting because they were written for validating individual fields and didn't have the multiple field dependency that I needed.

My search continued and I came upon this posting, https://drupal.stackexchange.com/a/215280, which led me to the comment module in core. Here, I found the CommentNameConstraint.php and CommentNameConstraintValidator.php files. A working example of dependent fields being validated.

Using this as a model, I wrote my constraint and validator classes within my custom module.

  1. namespace Drupal\mymodule\Plugin\Validation\Constraint;
  2.  
  3. use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
  4.  
  5. /**
  6.  * Verify that the order number is valid.
  7.  *
  8.  * @Constraint(
  9.  *   id = "OrderNumber",
  10.  *   label = @Translation("Order Number", context = "Validation"),
  11.  *   type = "entity:media"
  12.  * )
  13.  */
  14. class OrderNumberConstraint extends CompositeConstraintBase {
  15.   /**
  16.    * @var string
  17.    */
  18.   public $orderNumberRequired = 'The order number is required when the resolution is entered.';
  19.  
  20.   /**
  21.    * @var string
  22.    */
  23.   public $orderNumberEmpty = 'The order number should not contain a value.';
  24.  
  25.   /**
  26.    * {@inheritdoc}
  27.    */
  28.   public function coversFields() {
  29.     return ['field_order_number', 'field_resolution'];
  30.   }
  31.  
  32. }

  1. <?php
  2.  
  3. namespace Drupal\mymodule\Plugin\Validation\Constraint;
  4.  
  5. use Symfony\Component\Validator\Constraint;
  6. use Symfony\Component\Validator\ConstraintValidator;
  7. use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
  8. use Symfony\Component\DependencyInjection\ContainerInterface;
  9.  
  10. /**
  11.  * Validates the Order Number constraint.
  12.  */
  13. class OrderNumberConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
  14.  
  15.   /**
  16.    * Validator 2.5 and upwards compatible execution context.
  17.    *
  18.    * @var \Symfony\Component\Validator\Context\ExecutionContextInterface
  19.    */
  20.   protected $context;
  21.  
  22.   /**
  23.    * {@inheritdoc}
  24.    */
  25.   public static function create(ContainerInterface $container) {
  26.     return new static($container->get('entity.manager')->getStorage('media'));
  27.   }
  28.  
  29.   /**
  30.    * {@inheritdoc}
  31.    */
  32.   public function validate($entity, Constraint $constraint) {
  33.     if ($entity->hasField('field_resolution') && $entity->hasField('field_order_number')) {
  34.       $resolution = $entity->get('field_resolution')->getValue();
  35.       $resolution = (count($resolution) == 1 ? $resolution[0]['value'] : FALSE);
  36.  
  37.       $ordernumber = $entity->get('field_order_number')->getValue();
  38.       $ordernumber = (count($ordernumber) == 1 ? $ordernumber[0]['value'] : FALSE);
  39.  
  40.       // If the resolution is other, require the order number.
  41.       if ($resolution == 'other') {
  42.         // If there is nothing in the order number, add violation.
  43.         if (!strlen($ordernumber)) {
  44.           $this->context->buildViolation($constraint->orderNumberRequired)
  45.             ->atPath('field_order_number')
  46.             ->addViolation();
  47.         }
  48.       }
  49.       else {
  50.         // If the resolution is not other, clear the order number.
  51.         if (strlen($ordernumber)) {
  52.           $this->context->buildViolation($constraint->orderNumberEmpty)
  53.             ->atPath('field_order_number')
  54.             ->addViolation();
  55.         }
  56.       }
  57.     }
  58.  
  59.     // If the fields do not exist, do not validate anything.
  60.     return NULL;
  61.   }
  62.  
  63. }

So, my constraints have been created. However, we need to attach the custom constraint to the entity. To do this, we needed to implement hook_entity_type_build().

  1. /**
  2.  * Implements hook_entity_type_build().
  3.  */
  4. function mymodule_entity_type_build(array &$entity_types) {
  5.   // Add our custom validation to the order number.
  6.   $entity_types['media']->addConstraint('OrderNumber');
  7. }

There it is, a working constraint based on two fields that will work with both using the GUI to create the entity or programmatically adding the entity.

Next step, writing automated tests to validate this is working properly.


Comments