Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.55% covered (success)
93.55%
290 / 310
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RelationshipTypeForm
93.55% covered (success)
93.55%
290 / 310
62.50% covered (warning)
62.50%
5 / 8
59.93
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
97.01% covered (success)
97.01%
195 / 201
0.00% covered (danger)
0.00%
0 / 1
13
 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
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 actions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 save
77.36% covered (warning)
77.36%
41 / 53
0.00% covered (danger)
0.00%
0 / 1
30.69
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    $form['label_a_plural'] = [
113      '#title' => $this->t('Contact A label (plural)'),
114      '#type' => 'textfield',
115      '#default_value' => $entity_type->get('label_a_plural'),
116      '#description' => $this->t('The plural form of the Contact A label.'),
117      '#size' => 30,
118    ];
119
120    // Normalize contact_type_a to array for default value.
121    $contact_type_a_default = $entity_type->get('contact_type_a');
122    if (!is_array($contact_type_a_default)) {
123      $contact_type_a_default = $contact_type_a_default !== NULL && $contact_type_a_default !== '' ? [$contact_type_a_default] : [];
124    }
125
126    $form['contact_type_a'] = [
127      '#title' => $this->t('Contact A type'),
128      '#type' => 'select',
129      '#options' => $this->getContactTypeOptions(),
130      '#default_value' => $contact_type_a_default,
131      '#description' => $this->t('The contact type for the first contact in the relationship.'),
132      '#required' => TRUE,
133      '#multiple' => TRUE,
134    ];
135
136    $form['label_b'] = [
137      '#title' => $this->t('Contact B label'),
138      '#type' => 'textfield',
139      '#default_value' => $entity_type->get('label_b'),
140      '#description' => $this->t('The human-readable name of this crm relationship type from contact A to contact B.'),
141      '#required' => TRUE,
142      '#size' => 30,
143      '#states' => [
144        'visible' => [
145          ':input[name="asymmetric"]' => ['checked' => TRUE],
146        ],
147      ],
148    ];
149
150    $form['label_b_plural'] = [
151      '#title' => $this->t('Contact B label (plural)'),
152      '#type' => 'textfield',
153      '#default_value' => $entity_type->get('label_b_plural'),
154      '#description' => $this->t('The plural form of the Contact B label.'),
155      '#size' => 30,
156      '#states' => [
157        'visible' => [
158          ':input[name="asymmetric"]' => ['checked' => TRUE],
159        ],
160      ],
161    ];
162
163    // Normalize contact_type_b to array for default value.
164    $contact_type_b_default = $entity_type->get('contact_type_b');
165    if (!is_array($contact_type_b_default)) {
166      $contact_type_b_default = $contact_type_b_default !== NULL && $contact_type_b_default !== '' ? [$contact_type_b_default] : [];
167    }
168
169    $form['contact_type_b'] = [
170      '#title' => $this->t('Contact B type'),
171      '#type' => 'select',
172      '#options' => $this->getContactTypeOptions(),
173      '#default_value' => $contact_type_b_default,
174      '#description' => $this->t('The contact type for the second contact in the relationship.'),
175      '#required' => TRUE,
176      '#multiple' => TRUE,
177      '#states' => [
178        'visible' => [
179          ':input[name="asymmetric"]' => ['checked' => TRUE],
180        ],
181      ],
182    ];
183
184    // Vertical tabs for additional settings.
185    $form['additional_settings'] = [
186      '#type' => 'vertical_tabs',
187      '#weight' => 99,
188    ];
189
190    // Attach JavaScript for vertical tab summaries.
191    $form['#attached']['library'][] = 'crm/crm.relationship_type_form';
192
193    // Relationship limits section.
194    $form['limits'] = [
195      '#type' => 'details',
196      '#title' => $this->t('Relationship limits'),
197      '#group' => 'additional_settings',
198    ];
199
200    $form['limits']['limit_a'] = [
201      '#title' => $this->t('Contact A limit'),
202      '#type' => 'number',
203      '#default_value' => $entity_type->get('limit_a'),
204      '#description' => $this->t('Maximum number of relationships of this type where a contact can be in the Contact A position. Leave empty for unlimited.'),
205      '#min' => 1,
206    ];
207
208    $form['limits']['limit_b'] = [
209      '#title' => $this->t('Contact B limit'),
210      '#type' => 'number',
211      '#default_value' => $entity_type->get('limit_b'),
212      '#description' => $this->t('Maximum number of relationships of this type where a contact can be in the Contact B position. Leave empty for unlimited.'),
213      '#min' => 1,
214      '#states' => [
215        'visible' => [
216          ':input[name="asymmetric"]' => ['checked' => TRUE],
217        ],
218      ],
219    ];
220
221    $form['limits']['limit_active_only'] = [
222      '#title' => $this->t('Count active relationships only'),
223      '#type' => 'checkbox',
224      '#default_value' => $entity_type->get('limit_active_only') ?? FALSE,
225      '#description' => $this->t('If checked, only active relationships will count toward the limit. Inactive relationships will be ignored.'),
226    ];
227
228    // Valid contacts section.
229    $form['valid_contacts'] = [
230      '#type' => 'details',
231      '#title' => $this->t('Valid contacts'),
232      '#group' => 'additional_settings',
233      '#description' => $this->t('Optionally restrict which contacts can be selected for each side of the relationship. Leave empty for no restrictions.'),
234    ];
235
236    // Get valid contacts A default value.
237    $valid_contacts_a_default = [];
238    $valid_contacts_a_ids = $entity_type->getValidContactsA();
239    if (!empty($valid_contacts_a_ids)) {
240      $valid_contacts_a_default = $this->entityTypeManager
241        ->getStorage('crm_contact')
242        ->loadMultiple($valid_contacts_a_ids);
243    }
244
245    $form['valid_contacts']['valid_contacts_a'] = [
246      '#title' => $this->t('Valid contacts for Contact A'),
247      '#type' => 'entity_autocomplete',
248      '#target_type' => 'crm_contact',
249      '#default_value' => $valid_contacts_a_default,
250      '#tags' => TRUE,
251      '#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.'),
252    ];
253
254    // Get valid contacts B default value.
255    $valid_contacts_b_default = [];
256    $valid_contacts_b_ids = $entity_type->getValidContactsB();
257    if (!empty($valid_contacts_b_ids)) {
258      $valid_contacts_b_default = $this->entityTypeManager
259        ->getStorage('crm_contact')
260        ->loadMultiple($valid_contacts_b_ids);
261    }
262
263    $form['valid_contacts']['valid_contacts_b'] = [
264      '#title' => $this->t('Valid contacts for Contact B'),
265      '#type' => 'entity_autocomplete',
266      '#target_type' => 'crm_contact',
267      '#default_value' => $valid_contacts_b_default,
268      '#tags' => TRUE,
269      '#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.'),
270      '#states' => [
271        'visible' => [
272          ':input[name="asymmetric"]' => ['checked' => TRUE],
273        ],
274      ],
275    ];
276
277    // Contact readonly section.
278    $form['contact_readonly'] = [
279      '#type' => 'details',
280      '#title' => $this->t('Readonly'),
281      '#group' => 'additional_settings',
282      '#description' => $this->t('Control whether contacts can be changed after being set.'),
283    ];
284
285    $form['contact_readonly']['readonly_contact_a'] = [
286      '#title' => $this->t('Make Contact A read-only after being set'),
287      '#type' => 'checkbox',
288      '#default_value' => $entity_type->isNew() ? TRUE : $entity_type->isReadonlyContactA(),
289      '#description' => $this->t('If checked, Contact A cannot be changed after the relationship is created.'),
290    ];
291
292    $form['contact_readonly']['readonly_contact_b'] = [
293      '#title' => $this->t('Make Contact B read-only after being set'),
294      '#type' => 'checkbox',
295      '#default_value' => $entity_type->isNew() ? TRUE : $entity_type->isReadonlyContactB(),
296      '#description' => $this->t('If checked, Contact B cannot be changed after the relationship is created.'),
297      '#states' => [
298        'visible' => [
299          ':input[name="asymmetric"]' => ['checked' => TRUE],
300        ],
301      ],
302    ];
303
304    return $this->protectBundleIdElement($form);
305  }
306
307  /**
308   * Returns a list of contact types.
309   */
310  protected function getContactTypeOptions() {
311    $crm_contact_type = $this->entityTypeBundleInfo->getBundleInfo('crm_contact');
312    $options = [];
313    foreach ($crm_contact_type as $type => $contact) {
314      $options[$type] = $contact['label'];
315    }
316
317    return $options;
318  }
319
320  /**
321   * {@inheritdoc}
322   */
323  public function validateForm(array &$form, FormStateInterface $form_state) {
324    parent::validateForm($form, $form_state);
325
326    // Only validate contact type removal on edit, not add.
327    if ($this->entity->isNew()) {
328      return;
329    }
330
331    // Get old and new contact types.
332    $old_types_a = $this->entity->get('contact_type_a') ?? [];
333    $new_types_a = $form_state->getValue('contact_type_a') ?? [];
334    if (!is_array($new_types_a)) {
335      $new_types_a = $new_types_a !== NULL && $new_types_a !== '' ? [$new_types_a] : [];
336    }
337    $new_types_a = array_values(array_filter($new_types_a));
338
339    $old_types_b = $this->entity->get('contact_type_b') ?? [];
340    $new_types_b = $form_state->getValue('contact_type_b') ?? [];
341    if (!is_array($new_types_b)) {
342      $new_types_b = $new_types_b !== NULL && $new_types_b !== '' ? [$new_types_b] : [];
343    }
344    $new_types_b = array_values(array_filter($new_types_b));
345
346    // Find removed contact types.
347    $removed_types_a = array_diff($old_types_a, $new_types_a);
348    $removed_types_b = array_diff($old_types_b, $new_types_b);
349
350    // Check if any removed types are still in use.
351    foreach ($removed_types_a as $removed_type) {
352      if ($this->contactTypeInUse($removed_type, 0)) {
353        $form_state->setErrorByName('contact_type_a', $this->t('Cannot remove contact type %type from Contact A because existing relationships use it.', [
354          '%type' => $removed_type,
355        ]));
356      }
357    }
358
359    foreach ($removed_types_b as $removed_type) {
360      if ($this->contactTypeInUse($removed_type, 1)) {
361        $form_state->setErrorByName('contact_type_b', $this->t('Cannot remove contact type %type from Contact B because existing relationships use it.', [
362          '%type' => $removed_type,
363        ]));
364      }
365    }
366  }
367
368  /**
369   * Checks if a contact type is in use by existing relationships.
370   *
371   * @param string $contact_type
372   *   The contact type (bundle) to check.
373   * @param int $delta
374   *   The delta position (0 for contact_a, 1 for contact_b).
375   *
376   * @return bool
377   *   TRUE if the contact type is in use, FALSE otherwise.
378   */
379  protected function contactTypeInUse(string $contact_type, int $delta): bool {
380    $relationship_type_id = $this->entity->id();
381
382    // Query relationships of this type.
383    $relationship_ids = $this->entityTypeManager
384      ->getStorage('crm_relationship')
385      ->getQuery()
386      ->condition('bundle', $relationship_type_id)
387      ->accessCheck(FALSE)
388      ->execute();
389
390    if (empty($relationship_ids)) {
391      return FALSE;
392    }
393
394    // Load relationships and check contact types at the specified delta.
395    $relationships = $this->entityTypeManager
396      ->getStorage('crm_relationship')
397      ->loadMultiple($relationship_ids);
398
399    foreach ($relationships as $relationship) {
400      $contacts = $relationship->get('contacts')->referencedEntities();
401      if (isset($contacts[$delta]) && $contacts[$delta]->bundle() === $contact_type) {
402        return TRUE;
403      }
404    }
405
406    return FALSE;
407  }
408
409  /**
410   * {@inheritdoc}
411   */
412  protected function actions(array $form, FormStateInterface $form_state) {
413    $actions = parent::actions($form, $form_state);
414    $actions['submit']['#value'] = $this->t('Save relationship type');
415
416    return $actions;
417  }
418
419  /**
420   * {@inheritdoc}
421   */
422  public function save(array $form, FormStateInterface $form_state) {
423    $entity_type = $this->entity;
424    $entity_type
425      ->set('id', trim($entity_type->id()))
426      ->set('label', trim($entity_type->label()));
427
428    // Normalize contact_type_a to array.
429    $contact_type_a = $form_state->getValue('contact_type_a');
430    if (!is_array($contact_type_a)) {
431      $contact_type_a = $contact_type_a !== NULL && $contact_type_a !== '' ? [$contact_type_a] : [];
432    }
433    $entity_type->set('contact_type_a', array_values(array_filter($contact_type_a)));
434
435    // Normalize contact_type_b to array.
436    $contact_type_b = $form_state->getValue('contact_type_b');
437    if (!is_array($contact_type_b)) {
438      $contact_type_b = $contact_type_b !== NULL && $contact_type_b !== '' ? [$contact_type_b] : [];
439    }
440    $entity_type->set('contact_type_b', array_values(array_filter($contact_type_b)));
441
442    // Set limit values, converting empty strings to NULL.
443    $limit_a = $form_state->getValue('limit_a');
444    $entity_type->set('limit_a', $limit_a !== '' && $limit_a !== NULL ? (int) $limit_a : NULL);
445
446    $limit_b = $form_state->getValue('limit_b');
447    $entity_type->set('limit_b', $limit_b !== '' && $limit_b !== NULL ? (int) $limit_b : NULL);
448
449    $entity_type->set('limit_active_only', (bool) $form_state->getValue('limit_active_only'));
450
451    // Normalize valid_contacts_a from entity_autocomplete format.
452    $valid_contacts_a = $form_state->getValue('valid_contacts_a') ?? [];
453    $valid_contacts_a_ids = [];
454    if (!empty($valid_contacts_a)) {
455      foreach ($valid_contacts_a as $item) {
456        if (is_array($item) && isset($item['target_id'])) {
457          $valid_contacts_a_ids[] = (int) $item['target_id'];
458        }
459        elseif (is_numeric($item)) {
460          $valid_contacts_a_ids[] = (int) $item;
461        }
462      }
463    }
464    $entity_type->set('valid_contacts_a', $valid_contacts_a_ids);
465
466    // Normalize valid_contacts_b from entity_autocomplete format.
467    $valid_contacts_b = $form_state->getValue('valid_contacts_b') ?? [];
468    $valid_contacts_b_ids = [];
469    if (!empty($valid_contacts_b)) {
470      foreach ($valid_contacts_b as $item) {
471        if (is_array($item) && isset($item['target_id'])) {
472          $valid_contacts_b_ids[] = (int) $item['target_id'];
473        }
474        elseif (is_numeric($item)) {
475          $valid_contacts_b_ids[] = (int) $item;
476        }
477      }
478    }
479    $entity_type->set('valid_contacts_b', $valid_contacts_b_ids);
480
481    // Set readonly contact settings.
482    $entity_type->set('readonly_contact_a', (bool) $form_state->getValue('readonly_contact_a'));
483    $entity_type->set('readonly_contact_b', (bool) $form_state->getValue('readonly_contact_b'));
484
485    if (!$entity_type->get('asymmetric')) {
486      $entity_type->set('label_b', $entity_type->get('label_a'));
487      $entity_type->set('label_b_plural', $entity_type->get('label_a_plural'));
488      $entity_type->set('contact_type_b', $entity_type->get('contact_type_a'));
489      $entity_type->set('limit_b', $entity_type->get('limit_a'));
490      $entity_type->set('valid_contacts_b', $entity_type->get('valid_contacts_a'));
491      $entity_type->set('readonly_contact_b', $entity_type->get('readonly_contact_a'));
492    }
493
494    $status = $entity_type->save();
495
496    $t_args = ['%name' => $entity_type->label()];
497    if ($status == self::SAVED_UPDATED) {
498      $message = $this->t('The crm relationship type %name has been updated.', $t_args);
499    }
500    elseif ($status == self::SAVED_NEW) {
501      $message = $this->t('The crm relationship type %name has been added.', $t_args);
502    }
503    $this->messenger()->addStatus($message);
504
505    $form_state->setRedirectUrl($entity_type->toUrl('collection'));
506
507    return $status;
508  }
509
510}