Score:1

ConstraintValidator fails to validate constraint on Taxonomy Term reference field

nr flag

Client has a simple and consistent hierarchical taxonomy.

Cities at the top level and Schools at the second level.

.
├── City1
│   ├── School1
│   ├── School2
│   └── School5
└── City2
    ├── School3
    └── School4

The User account entity bundle contains an Entity Reference field to the School taxonomy terms.

I am trying to enforce a constraint to prevent creating or saving a user account if the term selected is at the top level of the hierarchy (and hence a City, not a School).

Here's some example code, skipping the dependency injection of $this->entityTypeManager for brevity.

TermParentConstraint.php:

<?php

namespace Drupal\entity_validation_examples\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * Prevent account creation if School taxonomy term has no parent.
 *
 * @Constraint(
 *   id = "TermParent",
 *   label = @Translation("Prevent account creation if term has no parent", context = "Validation"),
 *   type = "entity"
 * )
 */
class TermParentConstraint extends Constraint {

  /**
   * Message shown when trying to create account if School has no parent City.
   *
   * @var string
   *   School message.
   */
  public $schoolMessage = 'You must select both a City and a School.';

}

TermParentConstraintValidator.php:

<?php

namespace Drupal\entity_validation_examples\Plugin\Validation\Constraint;

use Drupal\user\Entity\User;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/**
 * Validates the TermParent constraint.
 */
class TermParentConstraintValidator extends ConstraintValidator {

  /**
   * {@inheritdoc}
   */
  public function validate($entity, Constraint $constraint) {
    if (!isset($entity)) {
      return;
    }

    if ($entity->getEntityTypeId() == 'user') {

      $school = $entity->get('field_select_a_school')->getValue();

      // Orphaned school taxonomy terms must be Cities, not Schools.
      $parent = \Drupal::entityTypeManager()
        ->getStorage('taxonomy_term')
        ->loadParents($school[0]['target_id']);
      if (empty($parent)) {
        $this->context->addViolation($constraint->schoolMessage);
      }
    }
  }

}

Expected behavior: When I create a new user account and select City1 as the value of School, account creation should fail and an error message should be displayed.

Actual behavior: When I create a new user account and select City1 as the value of School, account creation succeeds and no error message is displayed.

Drupal Watchdog logs contain some errors, but I am not sure they are relevant:

 drush ws
 --------- -------------- ------ ---------- --------------------------------------------- 
  ID        Date           Type   Severity   Message                                                                                                                                                                                       
 --------- -------------- ------ ---------- --------------------------------------------- 
  5298867   18/Oct 22:57   php    Warning    Warning: Illegal offset type in isset or empty in Drupal\Core\Entity\EntityStorageBase->load() (line 246 of /app/docroot/core/lib/Drupal/Core/Entity/EntityStorageBase.php) #0 /app/docroot/  
  5298866   18/Oct 22:57   php    Notice     Notice: Array to string conversion in Drupal\Core\Entity\EntityStorageBase->buildCacheId() (line 126 of /app/docroot/core/lib/Drupal/Core/Entity/EntityStorageBase.php) #0 /app/docroot/core  
  5298865   18/Oct 22:57   php    Warning    Warning: array_flip(): Can only flip STRING and INTEGER values! in Drupal\Core\Entity\EntityStorageBase->loadMultiple() (line 261 of /app/docroot/core/lib/Drupal/Core/Entity/EntityStorageB  
  5298864   18/Oct 22:57   php    Notice     Notice: iconv(): Wrong charset, conversion from `HTML-ENTITIES' to `UTF-8' is not allowed in twig_convert_encoding() (line 1009 of /app/vendor/twig/twig/src/Extension/CoreExtension.php) #0  
  5298863   18/Oct 22:57   php    Notice     Notice: iconv(): Wrong charset, conversion from `HTML-ENTITIES' to `UTF-8' is not allowed in twig_convert_encoding() (line 1009 of /app/vendor/twig/twig/src/Extension/CoreExtension.php) #0  
  5298862   18/Oct 22:57   php    Notice     Notice: iconv(): Wrong charset, conversion from `HTML-ENTITIES' to `UTF-8' is not allowed in twig_convert_encoding() (line 1009 of /app/vendor/twig/twig/src/Extension/CoreExtension.php) #0  
  5298861   18/Oct 22:57   php    Notice     Notice: iconv(): Wrong charset, conversion from `HTML-ENTITIES' to `UTF-8' is not allowed in twig_convert_encoding() (line 1009 of /app/vendor/twig/twig/src/Extension/CoreExtension.php) #0  
  5298860   18/Oct 22:57   php    Notice     Notice: iconv(): Wrong charset, conversion from `HTML-ENTITIES' to `UTF-8' is not allowed in twig_convert_encoding() (line 1009 of /app/vendor/twig/twig/src/Extension/CoreExtension.php) #0  
  5298859   18/Oct 22:57   php    Notice     Notice: iconv(): Wrong charset, conversion from `HTML-ENTITIES' to `UTF-8' is not allowed in twig_convert_encoding() (line 1009 of /app/vendor/twig/twig/src/Extension/CoreExtension.php) #0  
  5298858   18/Oct 22:57   php    Notice     Notice: iconv(): Wrong charset, conversion from `HTML-ENTITIES' to `UTF-8' is not allowed in twig_convert_encoding() (line 1009 of /app/vendor/twig/twig/src/Extension/CoreExtension.php) #0  
 --------- -------------- ------ ---------- --------------------------------------------- 

I've been seeing the iconv() errors since upgrading this project from Drupal 8 to Drupal 9, so I do not think they are relevant. The top three error messages here may be related to the constraint validation, but they are also rather common errors in this project (and debugging them is a much lower priority than essential functionality due very soon).

Any ideas? Am I at least on the right track?

Score:1
ph flag

The loadParents() method calls loadMultiple(), and that is where the array_flip() error is happening:

Warning: array_flip(): Can only flip STRING and INTEGER values! in Drupal\Core\Entity\EntityStorageBase->loadMultiple() (line 261 of /app/docroot/core/lib/Drupal/Core/Entity/EntityStorageB

So most likely something going on there.

The loadParents() method is different depending on your Drupal version.

hotwebmatter avatar
nr flag
Thanks for the tip! Here's partial `drush status`: Drupal version: `9.2.7` Database: `Connected` Drupal bootstrap: `Successful` Default theme: `projectname` Admin theme: `projectname_admin` Drush version: `10.6.0` Install profile: `standard` Drupal root: `/app/docroot`
Score:1
cn flag

The problem is you are loading parents in an array and then check for an empty value. Terms at the top level can have a root parent (ID=0), so this would be an item in the array as well, even if not pointing at an existing term.

You can try to simplify the code, since D8.6 you don't need to use the term storage anymore to handle the parent field. empty() on the target ID catches NULL for an empty field and 0 for the root parent.

 public function validate($entity, Constraint $constraint) {
    if (!isset($entity)) {
      return;
    }

    if ($entity->getEntityTypeId() == 'user') {
      $term = $entity->get('field_select_a_school')->entity;
      if ($term) {
        if (empty($term->parent->target_id)) {
          $this->context->addViolation($constraint->schoolMessage);
        }
      }
    }

  }
Score:0
nr flag

In this case, it turns out that the quickest solution is to configure the Simple Hierarchical Select widget correctly. (The SHS module is already installed, so I may as well use it!)

For the record, this is the problematic widget configuration.

BEFORE:

Checkbox labeled "Force selection of deepest level" unchecked

AFTER:

Checkbox labeled "Force selection of deepest level" checked

Thanks for all the answers, everybody! I learned quite a bit about constraint validation and hierarchical taxonomy, which will help me next time I have to deal with taxonomy trees or implement a ConstraintValidator class.

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.