Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.92% covered (warning)
89.92%
107 / 119
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
RelationshipForm
89.92% covered (warning)
89.92%
107 / 119
60.00% covered (warning)
60.00%
3 / 5
35.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 form
84.06% covered (warning)
84.06%
58 / 69
0.00% covered (danger)
0.00%
0 / 1
23.96
 save
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 applyValidContactConstraints
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
7.01
1<?php
2
3namespace Drupal\crm\Form;
4
5use Drupal\Component\Datetime\TimeInterface;
6use Drupal\Core\Datetime\DateFormatterInterface;
7use Drupal\Core\Entity\ContentEntityForm;
8use Drupal\Core\Entity\EntityRepositoryInterface;
9use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
10use Drupal\Core\Form\FormStateInterface;
11use Drupal\Core\Entity\EntityTypeManagerInterface;
12use Drupal\Core\Session\AccountInterface;
13use Symfony\Component\DependencyInjection\ContainerInterface;
14use Symfony\Component\HttpFoundation\RequestStack;
15
16/**
17 * Form controller for the crm relationship entity edit forms.
18 */
19class RelationshipForm extends ContentEntityForm {
20
21  /**
22   * The date formatter service.
23   *
24   * @var \Drupal\Core\Datetime\DateFormatterInterface
25   */
26  protected $dateFormatter;
27
28  /**
29   * The current user.
30   *
31   * @var \Drupal\Core\Session\AccountInterface
32   */
33  protected $currentUser;
34
35  /**
36   * The entity type manager.
37   *
38   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
39   */
40  protected $entityTypeManager;
41
42  /**
43   * The request stack.
44   *
45   * @var \Symfony\Component\HttpFoundation\RequestStack
46   */
47  protected $requestStack;
48
49  /**
50   * Constructs a RelationshipForm object.
51   *
52   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
53   *   The entity repository.
54   * @param \Drupal\Component\Datetime\TimeInterface $time
55   *   The time service.
56   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
57   *   The date formatter service.
58   * @param \Drupal\Core\Session\AccountInterface $current_user
59   *   The current user.
60   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
61   *   The entity type manager.
62   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface|null $entity_type_bundle_info
63   *   The entity type bundle service.
64   * @param \Symfony\Component\HttpFoundation\RequestStack|null $request_stack
65   *   The request stack.
66   */
67  public function __construct(
68    EntityRepositoryInterface $entity_repository,
69    TimeInterface $time,
70    DateFormatterInterface $date_formatter,
71    AccountInterface $current_user,
72    EntityTypeManagerInterface $entity_type_manager,
73    ?EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL,
74    ?RequestStack $request_stack = NULL,
75  ) {
76
77    parent::__construct($entity_repository, $entity_type_bundle_info, $time);
78    $this->dateFormatter = $date_formatter;
79    $this->currentUser = $current_user;
80    $this->entityTypeManager = $entity_type_manager;
81    $this->requestStack = $request_stack;
82  }
83
84  /**
85   * {@inheritdoc}
86   */
87  final public static function create(ContainerInterface $container) {
88    return new self(
89      $container->get('entity.repository'),
90      $container->get('datetime.time'),
91      $container->get('date.formatter'),
92      $container->get('current_user'),
93      $container->get('entity_type.manager'),
94      $container->get('entity_type.bundle.info'),
95      $container->get('request_stack'),
96    );
97  }
98
99  /**
100   * {@inheritdoc}
101   */
102  public function form(array $form, FormStateInterface $form_state) {
103    $form = parent::form($form, $form_state);
104    $relationship = $this->getEntity();
105    $bundle = $relationship->bundle->entity;
106
107    // Normalize contact_type_a to array and convert to target_bundles format.
108    $contact_type_a = $bundle->get('contact_type_a');
109    if (!is_array($contact_type_a)) {
110      $contact_type_a = $contact_type_a !== NULL && $contact_type_a !== '' ? [$contact_type_a] : [];
111    }
112    $target_bundles_a = array_combine($contact_type_a, $contact_type_a);
113    $form['contact_a']['widget'][0]['target_id']['#selection_settings']['target_bundles'] = $target_bundles_a;
114    $form['contact_a']['widget'][0]['target_id']['#title'] = $bundle->get('label_a');
115
116    // Apply valid contacts restriction for Contact A.
117    $this->applyValidContactConstraints($form, $relationship, $bundle, 'contact_a');
118
119    // Normalize contact_type_b to array and convert to target_bundles format.
120    $contact_type_b = $bundle->get('contact_type_b');
121    if (!is_array($contact_type_b)) {
122      $contact_type_b = $contact_type_b !== NULL && $contact_type_b !== '' ? [$contact_type_b] : [];
123    }
124    $target_bundles_b = array_combine($contact_type_b, $contact_type_b);
125    $form['contact_b']['widget'][0]['target_id']['#selection_settings']['target_bundles'] = $target_bundles_b;
126    $form['contact_b']['widget'][0]['target_id']['#title'] = $bundle->get('label_b');
127
128    // Apply valid contacts restriction for Contact B.
129    $this->applyValidContactConstraints($form, $relationship, $bundle, 'contact_b');
130
131    // Hide contact fields if read-only and the contact is already set.
132    if (!$relationship->isNew()) {
133      if ($bundle->isReadonlyContactA() && $relationship->get('contact_a')->entity) {
134        $form['contact_a']['#access'] = FALSE;
135      }
136      if ($bundle->isReadonlyContactB() && $relationship->get('contact_b')->entity) {
137        $form['contact_b']['#access'] = FALSE;
138      }
139    }
140
141    // Pre-populate contact fields from query parameters if creating new.
142    if ($relationship->isNew() && $this->requestStack) {
143      $request = $this->requestStack->getCurrentRequest();
144      $contact_storage = $this->entityTypeManager->getStorage('crm_contact');
145
146      // Check for contact_a query parameter.
147      $contact_a_id = $request->query->get('contact_a');
148      if ($contact_a_id) {
149        $contact = $contact_storage->load($contact_a_id);
150        if ($contact) {
151          $form['contact_a']['widget'][0]['target_id']['#default_value'] = $contact;
152          $form['contact_a']['widget']['#disabled'] = TRUE;
153        }
154      }
155
156      // Check for contact_b query parameter.
157      $contact_b_id = $request->query->get('contact_b');
158      if ($contact_b_id) {
159        $contact = $contact_storage->load($contact_b_id);
160        if ($contact) {
161          $form['contact_b']['widget'][0]['target_id']['#default_value'] = $contact;
162          $form['contact_b']['widget']['#disabled'] = TRUE;
163        }
164      }
165    }
166
167    $form['advanced']['#attributes']['class'][] = 'entity-meta';
168
169    $form['meta'] = [
170      '#type' => 'details',
171      '#group' => 'advanced',
172      '#weight' => -10,
173      '#title' => $this->t('Status'),
174      '#attributes' => ['class' => ['entity-meta__header']],
175      '#tree' => TRUE,
176      '#access' => $this->currentUser->hasPermission('administer crm'),
177    ];
178    $form['meta']['published'] = [
179      '#type' => 'item',
180      '#markup' => $relationship->get('status')->value ? $this->t('Active') : $this->t('Inactive'),
181      '#access' => !$relationship->isNew(),
182      '#wrapper_attributes' => ['class' => ['entity-meta__title']],
183    ];
184    $form['meta']['changed'] = [
185      '#type' => 'item',
186      '#title' => $this->t('Last saved'),
187      '#markup' => !$relationship->isNew() ? $this->dateFormatter->format($relationship->getChangedTime(), 'short') : $this->t('Not saved yet'),
188      '#wrapper_attributes' => ['class' => ['entity-meta__last-saved']],
189    ];
190
191    $form['meta']['status'] = &$form['status'];
192    $form['meta']['status']['#weight'] = 100;
193    unset($form['status']);
194
195    if (isset($form['uid'])) {
196      unset($form['uid']);
197    }
198
199    if (isset($form['created'])) {
200      $form['created']['#weight'] = 200;
201      $form['meta']['created'] = &$form['created'];
202      unset($form['created']);
203    }
204
205    return $form;
206  }
207
208  /**
209   * {@inheritdoc}
210   */
211  public function save(array $form, FormStateInterface $form_state) {
212    $result = parent::save($form, $form_state);
213
214    $relationship = $this->getEntity();
215
216    $label = $relationship->label() ?? 'No label';
217    $message_arguments = ['%label' => $label];
218    $logger_arguments = [
219      '%label' => $label,
220      'link' => $relationship->toLink($this->t('View'))->toString(),
221    ];
222
223    switch ($result) {
224      case SAVED_NEW:
225        $this->messenger()->addStatus($this->t('New crm relationship %label has been created.', $message_arguments));
226        $this->logger('crm')->notice('Created new crm relationship %label', $logger_arguments);
227        break;
228
229      case SAVED_UPDATED:
230        $this->messenger()->addStatus($this->t('The crm relationship %label has been updated.', $message_arguments));
231        $this->logger('crm')->notice('Updated crm relationship %label.', $logger_arguments);
232        break;
233    }
234
235    $form_state->setRedirect('entity.crm_relationship.canonical', ['crm_relationship' => $relationship->id()]);
236
237    return $result;
238  }
239
240  /**
241   * Applies valid contact constraints to a contact field.
242   *
243   * @param array &$form
244   *   The form array.
245   * @param \Drupal\crm\RelationshipInterface $relationship
246   *   The relationship entity.
247   * @param \Drupal\crm\RelationshipTypeInterface $bundle
248   *   The relationship type entity.
249   * @param string $field_name
250   *   The field name ('contact_a' or 'contact_b').
251   */
252  protected function applyValidContactConstraints(array &$form, $relationship, $bundle, string $field_name): void {
253    // Get valid contacts based on field name.
254    $valid_contact_ids = $field_name === 'contact_a'
255      ? $bundle->getValidContactsA()
256      : $bundle->getValidContactsB();
257
258    // If no valid contacts are configured, no restrictions apply.
259    if (empty($valid_contact_ids)) {
260      return;
261    }
262
263    // Load the valid contacts to verify they exist.
264    $valid_contacts = $this->entityTypeManager
265      ->getStorage('crm_contact')
266      ->loadMultiple($valid_contact_ids);
267
268    // Filter to only existing contacts.
269    $existing_valid_ids = array_keys($valid_contacts);
270
271    if (empty($existing_valid_ids)) {
272      return;
273    }
274
275    // If exactly one valid contact exists, auto-select and lock the field.
276    if (count($existing_valid_ids) === 1) {
277      $single_contact = reset($valid_contacts);
278
279      // Set the default value if not already set.
280      if ($relationship->isNew() || empty($form[$field_name]['widget'][0]['target_id']['#default_value'])) {
281        $form[$field_name]['widget'][0]['target_id']['#default_value'] = $single_contact;
282      }
283
284      // Disable the field to prevent changes.
285      $form[$field_name]['widget']['#disabled'] = TRUE;
286
287      // Add description explaining why it's locked.
288      $form[$field_name]['widget'][0]['target_id']['#description'] = $this->t('This contact is automatically selected because it is the only valid option for this relationship type.');
289    }
290    else {
291      // Multiple valid contacts, use custom selection handler to restrict.
292      $form[$field_name]['widget'][0]['target_id']['#selection_handler'] = 'valid_contacts:crm_contact';
293      $form[$field_name]['widget'][0]['target_id']['#selection_settings']['valid_contact_ids'] = $existing_valid_ids;
294    }
295  }
296
297}