Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.46% covered (warning)
56.46%
83 / 147
72.73% covered (warning)
72.73%
8 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
PrimaryEntityReferenceInlineFormWidget
56.55% covered (warning)
56.55%
82 / 145
72.73% covered (warning)
72.73%
8 / 11
313.21
0.00% covered (danger)
0.00%
0 / 1
 defaultSettings
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 settingsForm
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 settingsSummary
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 formElement
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
380
 renderPrimaryField
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 buildPrimaryOptions
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
8
 validatePrimarySelection
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 massageFormValues
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
10
 extractFormValues
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getTargetEntityType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTargetEntityTypeLabel
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\primary_entity_reference\Plugin\Field\FieldWidget;
6
7use Drupal\Core\Field\FieldItemListInterface;
8use Drupal\Core\Form\FormStateInterface;
9use Drupal\Core\Field\Attribute\FieldWidget;
10use Drupal\Core\StringTranslation\TranslatableMarkup;
11
12// Only define this widget if Inline Entity Form is installed.
13if (!class_exists('Drupal\inline_entity_form\Plugin\Field\FieldWidget\InlineEntityFormComplex')) {
14  return;
15}
16
17use Drupal\inline_entity_form\Plugin\Field\FieldWidget\InlineEntityFormComplex;
18
19/**
20 * Plugin implementation of the 'primary_entity_reference_inline_form' widget.
21 *
22 * This widget requires the Inline Entity Form module to be installed.
23 */
24#[FieldWidget(
25  id: 'primary_entity_reference_inline_form',
26  label: new TranslatableMarkup('Inline entity form - Primary'),
27  description: new TranslatableMarkup('An inline entity form widget with primary selection.'),
28  field_types: ['primary_entity_reference'],
29  multiple_values: TRUE,
30)]
31class PrimaryEntityReferenceInlineFormWidget extends InlineEntityFormComplex {
32
33  /**
34   * {@inheritdoc}
35   */
36  public static function defaultSettings() {
37    $defaults = parent::defaultSettings();
38    $defaults += [
39      'primary_label'     => 'Primary',
40      'show_primary_only' => FALSE,
41    ];
42
43    return $defaults;
44  }
45
46  /**
47   * {@inheritdoc}
48   */
49  public function settingsForm(array $form, FormStateInterface $form_state) {
50    $element = parent::settingsForm($form, $form_state);
51
52    $element['primary_label'] = [
53      '#type'          => 'textfield',
54      '#title'         => $this->t('Primary label'),
55      '#description'   => $this->t('The label to display for the primary selection.'),
56      '#default_value' => $this->getSetting('primary_label'),
57      '#required'      => TRUE,
58    ];
59
60    $element['show_primary_only'] = [
61      '#type'          => 'checkbox',
62      '#title'         => $this->t('Show primary only'),
63      '#description'   => $this->t('Only display the primary item in the form. All non-primary items will be preserved but not shown.'),
64      '#default_value' => $this->getSetting('show_primary_only'),
65    ];
66
67    return $element;
68  }
69
70  /**
71   * {@inheritdoc}
72   */
73  public function settingsSummary() {
74    $summary = parent::settingsSummary();
75
76    $summary[] = $this->t('Primary selection is required.');
77
78    $primary_label = $this->getSetting('primary_label');
79    $summary[] = $this->t('Primary label: @label', ['@label' => $primary_label]);
80
81    // Always show the show_primary_only setting status.
82    if ($this->getSetting('show_primary_only')) {
83      $summary[] = $this->t('Show primary only: Yes');
84    }
85    else {
86      $summary[] = $this->t('Show primary only: No');
87    }
88
89    return $summary;
90  }
91
92  /**
93   * {@inheritdoc}
94   */
95  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
96    // Get the parent form element.
97    $element = parent::formElement($items, $delta, $element, $form, $form_state);
98
99    // Add the primary selection library.
100    $element['#attached']['library'][] = 'primary_entity_reference/primary_selection';
101
102    // Add a wrapper class for styling.
103    $class = 'primary-entity-reference-inline-form';
104    $element['#attributes']['class'][] = $class;
105
106    // Get the IEF ID from the element (set by parent).
107    $ief_id = $element['#ief_id'] ?? NULL;
108
109    if (!$ief_id) {
110      return $element;
111    }
112
113    // Get the IEF widget state to check for entities.
114    $widget_state = $form_state->get(['inline_entity_form', $ief_id]);
115    $entities = $widget_state['entities'] ?? [];
116
117    // Determine the current primary selection.
118    $current_primary = NULL;
119    foreach ($items as $item_delta => $item) {
120      if ($item->get('primary')->getValue()) {
121        $current_primary = $item_delta;
122        break;
123      }
124    }
125
126    // If no primary is set and we have entities, default to the first one.
127    if ($current_primary === NULL && !empty($entities)) {
128      $current_primary = 0;
129    }
130
131    // Handle show_primary_only setting.
132    $show_primary_only = $this->getSetting('show_primary_only');
133
134    if ($show_primary_only) {
135      // Always re-fetch entities from widget state when setting is enabled.
136      $widget_state = $form_state->get(['inline_entity_form', $ief_id]);
137      $fresh_entities = $widget_state['entities'] ?? [];
138
139      // Now check if we have entities to process.
140      if (!empty($fresh_entities)) {
141        // Remove all non-primary entity rows from the already-built form.
142        foreach ($fresh_entities as $key => $value) {
143          if ($key !== $current_primary && isset($element['entities'][$key])) {
144            // Completely remove this entity row from rendering.
145            unset($element['entities'][$key]);
146          }
147        }
148
149        // Disable "add more" functionality.
150        if (isset($element['actions'])) {
151          $element['actions']['#access'] = FALSE;
152        }
153      }
154    }
155
156    // Add primary column to table fields.
157    $primary_label = $this->getSetting('primary_label');
158    // Hide primary column if show_primary_only is enabled.
159    if (!$show_primary_only && !isset($element['entities']['#table_fields']['primary'])) {
160      $element['entities']['#table_fields'] = [
161        'primary' => [
162          'type'     => 'callback',
163          'label'    => $primary_label,
164          'weight'   => -100,
165          'callback' => [get_class($this), 'renderPrimaryField'],
166        ],
167      ] + ($element['entities']['#table_fields'] ?? []);
168    }
169
170    $field_name = $this->fieldDefinition->getName();
171    // Build the correct parents path for the primary radio buttons.
172    // This must include the form's parent path to work in nested forms
173    // (e.g., when the form is embedded into another form).
174    $field_parents = array_merge($form['#parents'] ?? [], [$field_name, 'primary']);
175
176    // Add primary radio to each entity row (skip if show_primary_only).
177    if (!$show_primary_only) {
178      foreach ($entities as $key => $value) {
179        if (!isset($element['entities'][$key])) {
180          continue;
181        }
182
183        // Add primary radio button for this row.
184        $element['entities'][$key]['primary'] = [
185          '#type'           => 'radio',
186          '#title'          => $this->t('Primary'),
187          '#title_display'  => 'invisible',
188          '#return_value'   => $key,
189          '#default_value'  => ($current_primary == $key) ? $key : NULL,
190          '#parents'        => $field_parents,
191          '#required'       => FALSE,
192          '#attributes'     => [
193            'class' => ['primary-radio'],
194          ],
195        ];
196      }
197    }
198
199    // Add validation.
200    if (!empty($entities)) {
201      $element['#element_validate'][] = [get_class($this), 'validatePrimarySelection'];
202    }
203
204    return $element;
205  }
206
207  /**
208   * Renders the primary radio button for a table cell.
209   *
210   * This callback is used by the IEF table rendering to display custom columns.
211   *
212   * @param \Drupal\Core\Entity\EntityInterface $entity
213   *   The entity being rendered in this row.
214   * @param array $variables
215   *   The template variables including the form array.
216   *
217   * @return array
218   *   A render array for the primary radio button.
219   */
220  public static function renderPrimaryField($entity, array $variables) {
221    $form = $variables['form'];
222
223    // Find the delta (key) for this entity in the form.
224    foreach ($form as $key => $item) {
225      if (is_array($item) && isset($item['#entity']) && $item['#entity'] === $entity) {
226        // Return the radio button if it exists.
227        if (isset($form[$key]['primary'])) {
228          return $form[$key]['primary'];
229        }
230        break;
231      }
232    }
233
234    // Fallback if radio not found.
235    return ['#markup' => ''];
236  }
237
238  /**
239   * Builds options for the primary selection radios.
240   *
241   * @param \Drupal\Core\Field\FieldItemListInterface $items
242   *   The field items.
243   * @param array $entities
244   *   The entities array from the widget state.
245   *
246   * @return array
247   *   An array of options for the radios.
248   */
249  public function buildPrimaryOptions(FieldItemListInterface $items, array $entities = []) {
250    $options = [];
251
252    // If we have entities in the widget state, use those.
253    if (!empty($entities)) {
254      foreach ($entities as $delta => $value) {
255        $entity = $value['entity'];
256        $label = $entity->label() ?: $this->t('Item @number', ['@number' => $delta + 1]);
257        $options[$delta] = $label;
258      }
259    }
260    else {
261      // Fallback: build options from field values.
262      $values = $items->getValue();
263      if (empty($values)) {
264        $options[0] = $this->t('First item');
265        return $options;
266      }
267
268      $target_type = $this->getTargetEntityType();
269      $storage = $this->entityTypeManager->getStorage($target_type);
270
271      foreach ($values as $delta => $value) {
272        if (isset($value['target_id'])) {
273          $entity = $storage->load($value['target_id']);
274          if ($entity) {
275            $options[$delta] = $entity->label();
276          }
277          else {
278            $options[$delta] = $this->t('Item @number', ['@number' => $delta + 1]);
279          }
280        }
281        else {
282          $options[$delta] = $this->t('Item @number', ['@number' => $delta + 1]);
283        }
284      }
285    }
286
287    return $options;
288  }
289
290  /**
291   * Validates the primary selection.
292   *
293   * @param array $element
294   *   The form element.
295   * @param \Drupal\Core\Form\FormStateInterface $form_state
296   *   The form state.
297   * @param array $form
298   *   The complete form.
299   */
300  public static function validatePrimarySelection(array $element, FormStateInterface $form_state, array $form) {
301    // Get the widget value.
302    $value = $form_state->getValue($element['#parents']);
303
304    // Check if primary is selected.
305    $primary = $value['primary'] ?? NULL;
306
307    if ($primary === NULL || $primary === '') {
308      $form_state->setError($element, t('Please select a primary item.'));
309    }
310  }
311
312  /**
313   * {@inheritdoc}
314   */
315  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
316    // Get the field name and retrieve the primary selection.
317    $field_name = $this->fieldDefinition->getName();
318    $field_parents = $form['#parents'] ?? [];
319    $parents = array_merge($field_parents, [$field_name, 'primary']);
320
321    $primary_delta = $form_state->getValue($parents);
322
323    // If show_primary_only is enabled, preserve non-primary items.
324    $show_primary_only = $this->getSetting('show_primary_only');
325    $original_items = [];
326    if ($show_primary_only) {
327      // Get the original entity from form state.
328      $form_object = $form_state->getFormObject();
329      if (method_exists($form_object, 'getEntity')) {
330        $entity = $form_object->getEntity();
331        if ($entity && $entity->hasField($field_name)) {
332          $original_items = $entity->get($field_name)->getValue();
333        }
334      }
335    }
336
337    // Let the parent massage the values first.
338    $values = parent::massageFormValues($values, $form, $form_state);
339
340    // Reset all primary flags to 0.
341    foreach ($values as $delta => $value) {
342      $values[$delta]['primary'] = 0;
343    }
344
345    // Set the selected primary.
346    if ($primary_delta !== NULL && isset($values[$primary_delta])) {
347      $values[$primary_delta]['primary'] = 1;
348    }
349
350    // If show_primary_only is enabled, merge back non-primary items.
351    if ($show_primary_only && !empty($original_items)) {
352      // Filter out the original primary item.
353      $non_primary_items = array_filter($original_items, fn($item) => empty($item['primary']));
354
355      // Merge processed values with non-primary items.
356      $values = array_merge($values, $non_primary_items);
357    }
358
359    return $values;
360  }
361
362  /**
363   * {@inheritdoc}
364   */
365  public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
366    parent::extractFormValues($items, $form, $form_state);
367
368    // Get the field name and retrieve the primary selection.
369    $field_name = $this->fieldDefinition->getName();
370    $field_parents = $form['#parents'] ?? [];
371    $parents = array_merge($field_parents, [$field_name, 'primary']);
372
373    $primary_delta = $form_state->getValue($parents);
374
375    // Update the primary flag on the field items.
376    if ($primary_delta !== NULL) {
377      foreach ($items as $delta => $item) {
378        $item->set('primary', $delta == $primary_delta ? 1 : 0);
379      }
380    }
381  }
382
383  /**
384   * Gets the target entity type ID.
385   *
386   * @return string
387   *   The target entity type ID.
388   */
389  public function getTargetEntityType(): string {
390    return $this->fieldDefinition->getSetting('target_type');
391  }
392
393  /**
394   * Gets the target entity type label.
395   *
396   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
397   *   The target entity type label.
398   */
399  public function getTargetEntityTypeLabel() {
400    $entity_type = $this->entityTypeManager->getDefinition($this->getTargetEntityType());
401    return $entity_type->getLabel();
402  }
403
404}