Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.98% covered (warning)
85.98%
227 / 264
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RelationshipTypeForm
85.98% covered (warning)
85.98%
227 / 264
50.00% covered (danger)
50.00%
4 / 8
65.94
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 form
96.23% covered (success)
96.23%
153 / 159
0.00% covered (danger)
0.00%
0 / 1
11
 getContactTypeOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 validateForm
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
12.07
 contactTypeInUse
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 actions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 save
75.51% covered (warning)
75.51%
37 / 49
0.00% covered (danger)
0.00%
0 / 1
32.46
1<?php
2
3namespace Drupal\crm\Form;
4
5use Drupal\Core\Entity\BundleEntityFormBase;
6use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
7use Drupal\Core\Entity\EntityTypeInterface;
8use Drupal\Core\Entity\EntityTypeManagerInterface;
9use Drupal\Core\Form\FormStateInterface;
10use Symfony\Component\DependencyInjection\ContainerInterface;
11
12/**
13 * Form handler for crm relationship type forms.
14 */
15class RelationshipTypeForm extends BundleEntityFormBase {
16
17  const SAVED_NEW = 1;
18
19  const SAVED_UPDATED = 2;
20
21  /**
22   * The entity type bundle info service.
23   *
24   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
25   */
26  protected $entityTypeBundleInfo;
27
28  /**
29   * The entity type manager.
30   *
31   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
32   */
33  protected $entityTypeManager;
34
35  /**
36   * Constructs a RelationshipTypeForm object.
37   *
38   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
39   *   The entity type bundle info service.
40   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
41   *   The entity type manager.
42   */
43  public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager) {
44    $this->entityTypeBundleInfo = $entity_type_bundle_info;
45    $this->entityTypeManager = $entity_type_manager;
46  }
47
48  /**
49   * {@inheritdoc}
50   */
51  final public static function create(ContainerInterface $container) {
52    return new self(
53      $container->get('entity_type.bundle.info'),
54      $container->get('entity_type.manager')
55    );
56  }
57
58  /**
59   * {@inheritdoc}
60   */
61  public function form(array $form, FormStateInterface $form_state) {
62    $form = parent::form($form, $form_state);
63
64    $entity_type = $this->entity;
65    if ($this->operation == 'edit') {
66      $form['#title'] = $this->t('Edit %label crm relationship type', ['%label' => $entity_type->label()]);
67    }
68
69    $form['label'] = [
70      '#title' => $this->t('Label'),
71      '#type' => 'textfield',
72      '#default_value' => $entity_type->label(),
73      '#description' => $this->t('The human-readable name of this crm relationship type.'),
74      '#required' => TRUE,
75      '#size' => 30,
76    ];
77
78    $form['id'] = [
79      '#type' => 'machine_name',
80      '#default_value' => $entity_type->id(),
81      '#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
82      '#machine_name' => [
83        'exists' => ['Drupal\crm\Entity\RelationshipType', 'load'],
84        'source' => ['label'],
85      ],
86      '#description' => $this->t('A unique machine-readable name for this crm relationship type. It must only contain lowercase letters, numbers, and underscores.'),
87    ];
88
89    $form['description'] = [
90      '#title' => $this->t('Description'),
91      '#type' => 'textarea',
92      '#default_value' => $entity_type->get('description'),
93      '#description' => $this->t('A description of this crm relationship type.'),
94    ];
95
96    $form['asymmetric'] = [
97      '#title' => $this->t('Asymmetric'),
98      '#type' => 'checkbox',
99      '#default_value' => $entity_type->isNew() ? 1 : $entity_type->get('asymmetric'),
100      '#description' => $this->t('Check this box if the relationship is asymmetric.'),
101    ];
102
103    $form['label_a'] = [
104      '#title' => $this->t('Contact A label'),
105      '#type' => 'textfield',
106      '#default_value' => $entity_type->get('label_a'),
107      '#description' => $this->t('The human-readable name of this crm relationship type from contact A to contact B.'),
108      '#required' => TRUE,
109      '#size' => 30,
110    ];
111
112    // Normalize contact_type_a to array for default value.
113    $contact_type_a_default = $entity_type->get('contact_type_a');
114    if (!is_array($contact_type_a_default)) {
115      $contact_type_a_default = $contact_type_a_default !== NULL && $contact_type_a_default !== '' ? [$contact_type_a_default] : [];
116    }
117
118    $form['contact_type_a'] = [
119      '#title' => $this->t('Contact A type'),
120      '#type' => 'select',
121      '#options' => $this->getContactTypeOptions(),
122      '#default_value' => $contact_type_a_default,
123      '#description' => $this->t('The contact type for the first contact in the relationship.'),
124      '#required' => TRUE,
125      '#multiple' => TRUE,
126    ];
127
128    $form['label_b'] = [
129      '#title' => $this->t('Contact B label'),
130      '#type' => 'textfield',
131      '#default_value' => $entity_type->get('label_b'),
132      '#description' => $this->t('The human-readable name of this crm relationship type from contact A to contact B.'),
133      '#required' => TRUE,
134      '#size' => 30,
135      '#states' => [
136        'visible' => [
137          ':input[name="asymmetric"]' => ['checked' => TRUE],
138        ],
139      ],
140    ];
141
142    // Normalize contact_type_b to array for default value.
143    $contact_type_b_default = $entity_type->get('contact_type_b');
144    if (!is_array($contact_type_b_default)) {
145      $contact_type_b_default = $contact_type_b_default !== NULL && $contact_type_b_default !== '' ? [$contact_type_b_default] : [];
146    }
147
148    $form['contact_type_b'] = [
149      '#title' => $this->t('Contact B type'),
150      '#type' => 'select',
151      '#options' => $this->getContactTypeOptions(),
152      '#default_value' => $contact_type_b_default,
153      '#description' => $this->t('The contact type for the second contact in the relationship.'),
154      '#required' => TRUE,
155      '#multiple' => TRUE,
156      '#states' => [
157        'visible' => [
158          ':input[name="asymmetric"]' => ['checked' => TRUE],
159        ],
160      ],
161    ];
162
163    // Vertical tabs for additional settings.
164    $form['additional_settings'] = [
165      '#type' => 'vertical_tabs',
166      '#weight' => 99,
167    ];
168
169    // Attach JavaScript for vertical tab summaries.
170    $form['#attached']['library'][] = 'crm/crm.relationship_type_form';
171
172    // Relationship limits section.
173    $form['limits'] = [
174      '#type' => 'details',
175      '#title' => $this->t('Relationship limits'),
176      '#group' => 'additional_settings',
177    ];
178
179    $form['limits']['limit_a'] = [
180      '#title' => $this->t('Contact A limit'),
181      '#type' => 'number',
182      '#default_value' => $entity_type->get('limit_a'),
183      '#description' => $this->t('Maximum number of relationships of this type where a contact can be in the Contact A position. Leave empty for unlimited.'),
184      '#min' => 1,
185    ];
186
187    $form['limits']['limit_b'] = [
188      '#title' => $this->t('Contact B limit'),
189      '#type' => 'number',
190      '#default_value' => $entity_type->get('limit_b'),
191      '#description' => $this->t('Maximum number of relationships of this type where a contact can be in the Contact B position. Leave empty for unlimited.'),
192      '#min' => 1,
193      '#states' => [
194        'visible' => [
195          ':input[name="asymmetric"]' => ['checked' => TRUE],
196        ],
197      ],
198    ];
199
200    $form['limits']['limit_active_only'] = [
201      '#title' => $this->t('Count active relationships only'),
202      '#type' => 'checkbox',
203      '#default_value' => $entity_type->get('limit_active_only') ?? FALSE,
204      '#description' => $this->t('If checked, only active relationships will count toward the limit. Inactive relationships will be ignored.'),
205    ];
206
207    // Valid contacts section.
208    $form['valid_contacts'] = [
209      '#type' => 'details',
210      '#title' => $this->t('Valid contacts'),
211      '#group' => 'additional_settings',
212      '#description' => $this->t('Optionally restrict which contacts can be selected for each side of the relationship. Leave empty for no restrictions.'),
213    ];
214
215    // Get valid contacts A default value.
216    $valid_contacts_a_default = [];
217    $valid_contacts_a_ids = $entity_type->getValidContactsA();
218    if (!empty($valid_contacts_a_ids)) {
219      $valid_contacts_a_default = $this->entityTypeManager
220        ->getStorage('crm_contact')
221        ->loadMultiple($valid_contacts_a_ids);
222    }
223
224    $form['valid_contacts']['valid_contacts_a'] = [
225      '#title' => $this->t('Valid contacts for Contact A'),
226      '#type' => 'entity_autocomplete',
227      '#target_type' => 'crm_contact',
228      '#default_value' => $valid_contacts_a_default,
229      '#tags' => TRUE,
230      '#description' => $this->t('Select the contacts that are valid for Contact A. If only one contact is selected, it will be automatically selected and locked when creating relationships.'),
231    ];
232
233    // Get valid contacts B default value.
234    $valid_contacts_b_default = [];
235    $valid_contacts_b_ids = $entity_type->getValidContactsB();
236    if (!empty($valid_contacts_b_ids)) {
237      $valid_contacts_b_default = $this->entityTypeManager
238        ->getStorage('crm_contact')
239        ->loadMultiple($valid_contacts_b_ids);
240    }
241
242    $form['valid_contacts']['valid_contacts_b'] = [
243      '#title' => $this->t('Valid contacts for Contact B'),
244      '#type' => 'entity_autocomplete',
245      '#target_type' => 'crm_contact',
246      '#default_value' => $valid_contacts_b_default,
247      '#tags' => TRUE,
248      '#description' => $this->t('Select the contacts that are valid for Contact B. If only one contact is selected, it will be automatically selected and locked when creating relationships.'),
249      '#states' => [
250        'visible' => [
251          ':input[name="asymmetric"]' => ['checked' => TRUE],
252        ],
253      ],
254    ];
255
256    return $this->protectBundleIdElement($form);
257  }
258
259  /**
260   * Returns a list of contact types.
261   */
262  protected function getContactTypeOptions() {
263    $crm_contact_type = $this->entityTypeBundleInfo->getBundleInfo('crm_contact');
264    $options = [];
265    foreach ($crm_contact_type as $type => $contact) {
266      $options[$type] = $contact['label'];
267    }
268
269    return $options;
270  }
271
272  /**
273   * {@inheritdoc}
274   */
275  public function validateForm(array &$form, FormStateInterface $form_state) {
276    parent::validateForm($form, $form_state);
277
278    // Only validate contact type removal on edit, not add.
279    if ($this->entity->isNew()) {
280      return;
281    }
282
283    // Get old and new contact types.
284    $old_types_a = $this->entity->get('contact_type_a') ?? [];
285    $new_types_a = $form_state->getValue('contact_type_a') ?? [];
286    if (!is_array($new_types_a)) {
287      $new_types_a = $new_types_a !== NULL && $new_types_a !== '' ? [$new_types_a] : [];
288    }
289    $new_types_a = array_values(array_filter($new_types_a));
290
291    $old_types_b = $this->entity->get('contact_type_b') ?? [];
292    $new_types_b = $form_state->getValue('contact_type_b') ?? [];
293    if (!is_array($new_types_b)) {
294      $new_types_b = $new_types_b !== NULL && $new_types_b !== '' ? [$new_types_b] : [];
295    }
296    $new_types_b = array_values(array_filter($new_types_b));
297
298    // Find removed contact types.
299    $removed_types_a = array_diff($old_types_a, $new_types_a);
300    $removed_types_b = array_diff($old_types_b, $new_types_b);
301
302    // Check if any removed types are still in use.
303    foreach ($removed_types_a as $removed_type) {
304      if ($this->contactTypeInUse($removed_type, 0)) {
305        $form_state->setErrorByName('contact_type_a', $this->t('Cannot remove contact type %type from Contact A because existing relationships use it.', [
306          '%type' => $removed_type,
307        ]));
308      }
309    }
310
311    foreach ($removed_types_b as $removed_type) {
312      if ($this->contactTypeInUse($removed_type, 1)) {
313        $form_state->setErrorByName('contact_type_b', $this->t('Cannot remove contact type %type from Contact B because existing relationships use it.', [
314          '%type' => $removed_type,
315        ]));
316      }
317    }
318  }
319
320  /**
321   * Checks if a contact type is in use by existing relationships.
322   *
323   * @param string $contact_type
324   *   The contact type (bundle) to check.
325   * @param int $delta
326   *   The delta position (0 for contact_a, 1 for contact_b).
327   *
328   * @return bool
329   *   TRUE if the contact type is in use, FALSE otherwise.
330   */
331  protected function contactTypeInUse(string $contact_type, int $delta): bool {
332    $relationship_type_id = $this->entity->id();
333
334    // Query relationships of this type.
335    $relationship_ids = $this->entityTypeManager
336      ->getStorage('crm_relationship')
337      ->getQuery()
338      ->condition('bundle', $relationship_type_id)
339      ->accessCheck(FALSE)
340      ->execute();
341
342    if (empty($relationship_ids)) {
343      return FALSE;
344    }
345
346    // Load relationships and check contact types at the specified delta.
347    $relationships = $this->entityTypeManager
348      ->getStorage('crm_relationship')
349      ->loadMultiple($relationship_ids);
350
351    foreach ($relationships as $relationship) {
352      $contacts = $relationship->get('contacts')->referencedEntities();
353      if (isset($contacts[$delta]) && $contacts[$delta]->bundle() === $contact_type) {
354        return TRUE;
355      }
356    }
357
358    return FALSE;
359  }
360
361  /**
362   * {@inheritdoc}
363   */
364  protected function actions(array $form, FormStateInterface $form_state) {
365    $actions = parent::actions($form, $form_state);
366    $actions['submit']['#value'] = $this->t('Save relationship type');
367
368    return $actions;
369  }
370
371  /**
372   * {@inheritdoc}
373   */
374  public function save(array $form, FormStateInterface $form_state) {
375    $entity_type = $this->entity;
376    $entity_type
377      ->set('id', trim($entity_type->id()))
378      ->set('label', trim($entity_type->label()));
379
380    // Normalize contact_type_a to array.
381    $contact_type_a = $form_state->getValue('contact_type_a');
382    if (!is_array($contact_type_a)) {
383      $contact_type_a = $contact_type_a !== NULL && $contact_type_a !== '' ? [$contact_type_a] : [];
384    }
385    $entity_type->set('contact_type_a', array_values(array_filter($contact_type_a)));
386
387    // Normalize contact_type_b to array.
388    $contact_type_b = $form_state->getValue('contact_type_b');
389    if (!is_array($contact_type_b)) {
390      $contact_type_b = $contact_type_b !== NULL && $contact_type_b !== '' ? [$contact_type_b] : [];
391    }
392    $entity_type->set('contact_type_b', array_values(array_filter($contact_type_b)));
393
394    // Set limit values, converting empty strings to NULL.
395    $limit_a = $form_state->getValue('limit_a');
396    $entity_type->set('limit_a', $limit_a !== '' && $limit_a !== NULL ? (int) $limit_a : NULL);
397
398    $limit_b = $form_state->getValue('limit_b');
399    $entity_type->set('limit_b', $limit_b !== '' && $limit_b !== NULL ? (int) $limit_b : NULL);
400
401    $entity_type->set('limit_active_only', (bool) $form_state->getValue('limit_active_only'));
402
403    // Normalize valid_contacts_a from entity_autocomplete format.
404    $valid_contacts_a = $form_state->getValue('valid_contacts_a') ?? [];
405    $valid_contacts_a_ids = [];
406    if (!empty($valid_contacts_a)) {
407      foreach ($valid_contacts_a as $item) {
408        if (is_array($item) && isset($item['target_id'])) {
409          $valid_contacts_a_ids[] = (int) $item['target_id'];
410        }
411        elseif (is_numeric($item)) {
412          $valid_contacts_a_ids[] = (int) $item;
413        }
414      }
415    }
416    $entity_type->set('valid_contacts_a', $valid_contacts_a_ids);
417
418    // Normalize valid_contacts_b from entity_autocomplete format.
419    $valid_contacts_b = $form_state->getValue('valid_contacts_b') ?? [];
420    $valid_contacts_b_ids = [];
421    if (!empty($valid_contacts_b)) {
422      foreach ($valid_contacts_b as $item) {
423        if (is_array($item) && isset($item['target_id'])) {
424          $valid_contacts_b_ids[] = (int) $item['target_id'];
425        }
426        elseif (is_numeric($item)) {
427          $valid_contacts_b_ids[] = (int) $item;
428        }
429      }
430    }
431    $entity_type->set('valid_contacts_b', $valid_contacts_b_ids);
432
433    if (!$entity_type->get('asymmetric')) {
434      $entity_type->set('label_b', $entity_type->get('label_a'));
435      $entity_type->set('contact_type_b', $entity_type->get('contact_type_a'));
436      $entity_type->set('limit_b', $entity_type->get('limit_a'));
437      $entity_type->set('valid_contacts_b', $entity_type->get('valid_contacts_a'));
438    }
439
440    $status = $entity_type->save();
441
442    $t_args = ['%name' => $entity_type->label()];
443    if ($status == self::SAVED_UPDATED) {
444      $message = $this->t('The crm relationship type %name has been updated.', $t_args);
445    }
446    elseif ($status == self::SAVED_NEW) {
447      $message = $this->t('The crm relationship type %name has been added.', $t_args);
448    }
449    $this->messenger()->addStatus($message);
450
451    $form_state->setRedirectUrl($entity_type->toUrl('collection'));
452
453    return $status;
454  }
455
456}