Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
69 / 69
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
RelationshipLimitConstraintValidator
100.00% covered (success)
100.00%
69 / 69
100.00% covered (success)
100.00%
4 / 4
21
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 validate
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
12
 countExistingRelationships
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3namespace Drupal\crm\Plugin\Validation\Constraint;
4
5use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
6use Drupal\Core\Entity\EntityTypeManagerInterface;
7use Symfony\Component\DependencyInjection\ContainerInterface;
8use Symfony\Component\Validator\Constraint;
9use Symfony\Component\Validator\ConstraintValidator;
10
11/**
12 * Validates that relationship limits are not exceeded.
13 */
14class RelationshipLimitConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
15
16  /**
17   * The entity type manager.
18   *
19   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
20   */
21  protected EntityTypeManagerInterface $entityTypeManager;
22
23  /**
24   * Constructs a new RelationshipLimitConstraintValidator.
25   *
26   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
27   *   The entity type manager.
28   */
29  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
30    $this->entityTypeManager = $entity_type_manager;
31  }
32
33  /**
34   * {@inheritdoc}
35   */
36  public static function create(ContainerInterface $container) {
37    return new static(
38      $container->get('entity_type.manager')
39    );
40  }
41
42  /**
43   * Validates the entity.
44   *
45   * @param \Drupal\crm\CrmRelationshipInterface $entity
46   *   The entity being validated.
47   * @param \Drupal\crm\Plugin\Validation\Constraint\RelationshipLimitConstraint $constraint
48   *   The constraint to validate against.
49   */
50  public function validate($entity, Constraint $constraint) {
51    $relationship_type = $entity->get('bundle')->entity;
52    if (!$relationship_type) {
53      return;
54    }
55
56    $limit_a = $relationship_type->getLimitA();
57    $limit_b = $relationship_type->getLimitB();
58
59    // If no limits are configured, nothing to validate.
60    if ($limit_a === NULL && $limit_b === NULL) {
61      return;
62    }
63
64    $contacts = $entity->get('contacts')->referencedEntities();
65    $contact_a = $contacts[0] ?? NULL;
66    $contact_b = $contacts[1] ?? NULL;
67
68    $limit_active_only = $relationship_type->isLimitActiveOnly();
69
70    // Check limit for Contact A.
71    if ($limit_a !== NULL && $contact_a) {
72      $count = $this->countExistingRelationships(
73        $contact_a->id(),
74        $relationship_type->id(),
75        0,
76        $limit_active_only,
77        $entity->id()
78      );
79
80      if ($count >= $limit_a) {
81        $this->context->buildViolation($constraint->limitExceededMessageA)
82          ->atPath('contact_a')
83          ->setParameter('@count', $count)
84          ->setParameter('@type', $relationship_type->label())
85          ->setParameter('@label', $relationship_type->get('label_a') ?: $relationship_type->label())
86          ->setParameter('@limit', $limit_a)
87          ->addViolation();
88      }
89    }
90
91    // Check limit for Contact B.
92    if ($limit_b !== NULL && $contact_b) {
93      $count = $this->countExistingRelationships(
94        $contact_b->id(),
95        $relationship_type->id(),
96        1,
97        $limit_active_only,
98        $entity->id()
99      );
100
101      if ($count >= $limit_b) {
102        $this->context->buildViolation($constraint->limitExceededMessageB)
103          ->atPath('contact_b')
104          ->setParameter('@count', $count)
105          ->setParameter('@type', $relationship_type->label())
106          ->setParameter('@label', $relationship_type->get('label_b') ?: $relationship_type->label())
107          ->setParameter('@limit', $limit_b)
108          ->addViolation();
109      }
110    }
111  }
112
113  /**
114   * Counts existing relationships for a contact in a specific position.
115   *
116   * @param int|string $contact_id
117   *   The contact ID.
118   * @param string $relationship_type_id
119   *   The relationship type ID.
120   * @param int $delta
121   *   The position (0 for contact_a, 1 for contact_b).
122   * @param bool $active_only
123   *   Whether to count only active relationships.
124   * @param int|string|null $exclude_id
125   *   The relationship ID to exclude (for edits).
126   *
127   * @return int
128   *   The count of existing relationships.
129   */
130  protected function countExistingRelationships(
131    int|string $contact_id,
132    string $relationship_type_id,
133    int $delta,
134    bool $active_only,
135    int|string|null $exclude_id = NULL,
136  ): int {
137    $query = $this->entityTypeManager
138      ->getStorage('crm_relationship')
139      ->getQuery()
140      ->condition('bundle', $relationship_type_id)
141      ->condition('contacts', $contact_id)
142      ->accessCheck(FALSE);
143
144    if ($active_only) {
145      $query->condition('status', 1);
146    }
147
148    if ($exclude_id !== NULL) {
149      $query->condition('id', $exclude_id, '<>');
150    }
151
152    $relationship_ids = $query->execute();
153
154    if (empty($relationship_ids)) {
155      return 0;
156    }
157
158    // Load relationships and count those where the contact is at the
159    // specified delta position.
160    $relationships = $this->entityTypeManager
161      ->getStorage('crm_relationship')
162      ->loadMultiple($relationship_ids);
163
164    $count = 0;
165    foreach ($relationships as $relationship) {
166      $contacts = $relationship->get('contacts')->getValue();
167      if (isset($contacts[$delta]) && (string) $contacts[$delta]['target_id'] === (string) $contact_id) {
168        $count++;
169      }
170    }
171
172    return $count;
173  }
174
175}