Skip to main content
We work with
PlumbersRestaurantsBoutiquesDentists Law OfficesGymsHair SalonsFood Trucks ContractorsAccountantsPet GroomersFlorists RealtorsChiropractorsAuto Shops PlumbersRestaurantsBoutiquesDentists Law OfficesGymsHair SalonsFood Trucks ContractorsAccountantsPet GroomersFlorists RealtorsChiropractorsAuto Shops

Add field constraint

By Dave

Sometimes you'll want to do some sort of validation on a field, but form_alter hooks may not work for you. (i.e. when creating a new entity.)

Here we will be checking for a unique value. Though this is already in core, we are creating our own validation classes so we can customize the output.

You'll start with a hook_entity_bundle_field_info_alter in your .module file.

/**
 * Implements hook_entity_bundle_field_info_alter().
 */
function YOUR_MODULE_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
  // Create a constraint to prevent duplicate field values.
  if ($entity_type->id() === 'node' && $bundle === 'CONTENT_TYPE') {
    $fields['field_my_field']->addConstraint('MyUniqueFieldConstraint', []);
  }
}

From there you will add two files to your modules /src/Plugin/Validation/Constraint folder.

MyCustomFieldConstraint.php

<?php

namespace Drupal\YOUR_MODULE\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * Checks if the field has a unique value.
 *
 * @Constraint(
 *   id = "MyUniqueField",
 *   label = @Translation("My Unique Field constraint", context = "Validation"),
 * )
 */
class MyUniqueFieldConstraint extends Constraint {

  public $message = 'A @entity_type with @field_name %value already exists. See <a target="_blank" href="@existing_url">@existing_name</a>';

  /**
   * {@inheritdoc}
   */
  public function validatedBy() {
    return '\Drupal\YOUR_MODULE\Plugin\Validation\Constraint\MyCustomFieldValidator';
  }

}

and

MyCustomFieldValidator

<?php

namespace Drupal\YOUR_MODULE\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/**
 * Validates that a field is unique for any given node.
 */
class MyCustomFieldValidator extends ConstraintValidator {

  /**
   * {@inheritdoc}
   */
  public function validate($items, Constraint $constraint) {
    if (!$item = $items->first()) {
      return;
    }
    $field_name = $items->getFieldDefinition()->getName();
    /** @var \Drupal\Core\Entity\EntityInterface $entity */
    $entity = $items->getEntity();
    $entity_type_id = $entity->getEntityTypeId();
    $id_key = $entity->getEntityType()->getKey('id');

    $query = \Drupal::entityQuery($entity_type_id);

    // @todo Don't check access. http://www.drupal.org/node/3171047
    $query->accessCheck(TRUE);

    $entity_id = $entity->id();
    // Using isset() instead of !empty() as 0 and '0' are valid ID values for
    // entity types using string IDs.
    if (isset($entity_id)) {
      $query->condition($id_key, $entity_id, '<>');
    }

    $value_taken = (array) $query
      ->condition($field_name, $item->value)
      ->range(0, 1)
      ->execute();

    if(!empty($value_taken)) {
      // We do this so we can add some extra information to the message
      $entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
      $existing = $entity_storage->load(reset($value_taken));
      $this->context->addViolation($constraint->message, [
        '%value' => $item->value,
        '@entity_type' => 'Person',
        '@field_name' => mb_strtolower($items->getFieldDefinition()->getLabel()),
        '@existing_url' => $existing->toLink()->getUrl()->toString(),
        '@existing_name' => $existing->label(),
      ]);
    }

  }

}

Clear your cache and try to create 2 nodes with the same values for your field designated in the .module file.