Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.67% covered (warning)
56.67%
68 / 120
72.73% covered (warning)
72.73%
8 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
PrimaryEntityReferenceInlineFormWidget
56.78% covered (warning)
56.78%
67 / 118
72.73% covered (warning)
72.73%
8 / 11
176.72
0.00% covered (danger)
0.00%
0 / 1
 defaultSettings
100.00% covered (success)
100.00%
5 / 5
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%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 formElement
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
132
 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%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 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    ];
41
42    return $defaults;
43  }
44
45  /**
46   * {@inheritdoc}
47   */
48  public function settingsForm(array $form, FormStateInterface $form_state) {
49    $element = parent::settingsForm($form, $form_state);
50
51    $element['primary_selection_settings'] = [
52      '#type'          => 'details',
53      '#title'         => $this->t('Primary selection settings'),
54      '#open'          => TRUE,
55      '#weight'        => 10,
56    ];
57
58    $element['primary_selection_settings']['primary_label'] = [
59      '#type'          => 'textfield',
60      '#title'         => $this->t('Primary label'),
61      '#description'   => $this->t('The label to display for the primary selection.'),
62      '#default_value' => $this->getSetting('primary_label'),
63      '#required'      => TRUE,
64    ];
65
66    return $element;
67  }
68
69  /**
70   * {@inheritdoc}
71   */
72  public function settingsSummary() {
73    $summary = parent::settingsSummary();
74
75    $summary[] = $this->t('Primary selection is required.');
76
77    $primary_label = $this->getSetting('primary_label');
78    $summary[] = $this->t('Primary label: @label', ['@label' => $primary_label]);
79
80    return $summary;
81  }
82
83  /**
84   * {@inheritdoc}
85   */
86  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
87    // Get the parent form element.
88    $element = parent::formElement($items, $delta, $element, $form, $form_state);
89
90    // Add the primary selection library.
91    $element['#attached']['library'][] = 'primary_entity_reference/primary_selection';
92
93    // Add a wrapper class for styling.
94    $class = 'primary-entity-reference-inline-form';
95    $element['#attributes']['class'][] = $class;
96
97    // Get the IEF ID from the element (set by parent).
98    $ief_id = $element['#ief_id'] ?? NULL;
99
100    if (!$ief_id) {
101      return $element;
102    }
103
104    // Get the IEF widget state to check for entities.
105    $widget_state = $form_state->get(['inline_entity_form', $ief_id]);
106    $entities = $widget_state['entities'] ?? [];
107
108    // Add primary column to table fields.
109    $primary_label = $this->getSetting('primary_label');
110    if (!isset($element['entities']['#table_fields']['primary'])) {
111      $element['entities']['#table_fields'] = [
112        'primary' => [
113          'type' => 'callback',
114          'label' => $primary_label,
115          'weight' => -100,
116          'callback' => [get_class($this), 'renderPrimaryField'],
117        ],
118      ] + ($element['entities']['#table_fields'] ?? []);
119    }
120
121    // Determine the current primary selection.
122    $current_primary = NULL;
123    foreach ($items as $item_delta => $item) {
124      if ($item->get('primary')->getValue()) {
125        $current_primary = $item_delta;
126        break;
127      }
128    }
129
130    // If no primary is set and we have entities, default to the first one.
131    if ($current_primary === NULL && !empty($entities)) {
132      $current_primary = 0;
133    }
134
135    $field_name = $this->fieldDefinition->getName();
136
137    // Add primary radio to each entity row.
138    foreach ($entities as $key => $value) {
139      if (!isset($element['entities'][$key])) {
140        continue;
141      }
142
143      // Add primary radio button for this row.
144      $element['entities'][$key]['primary'] = [
145        '#type' => 'radio',
146        '#title' => $this->t('Primary'),
147        '#title_display' => 'invisible',
148        '#return_value' => $key,
149        '#default_value' => ($current_primary == $key) ? $key : NULL,
150        '#parents' => [$field_name, 'primary'],
151        '#required' => FALSE,
152        '#attributes' => [
153          'class' => ['primary-radio'],
154        ],
155      ];
156    }
157
158    // Add validation.
159    if (!empty($entities)) {
160      $element['#element_validate'][] = [get_class($this), 'validatePrimarySelection'];
161    }
162
163    return $element;
164  }
165
166  /**
167   * Renders the primary radio button for a table cell.
168   *
169   * This callback is used by the IEF table rendering to display custom columns.
170   *
171   * @param \Drupal\Core\Entity\EntityInterface $entity
172   *   The entity being rendered in this row.
173   * @param array $variables
174   *   The template variables including the form array.
175   *
176   * @return array
177   *   A render array for the primary radio button.
178   */
179  public static function renderPrimaryField($entity, array $variables) {
180    $form = $variables['form'];
181
182    // Find the delta (key) for this entity in the form.
183    foreach ($form as $key => $item) {
184      if (is_array($item) && isset($item['#entity']) && $item['#entity'] === $entity) {
185        // Return the radio button if it exists.
186        if (isset($form[$key]['primary'])) {
187          return $form[$key]['primary'];
188        }
189        break;
190      }
191    }
192
193    // Fallback if radio not found.
194    return ['#markup' => ''];
195  }
196
197  /**
198   * Builds options for the primary selection radios.
199   *
200   * @param \Drupal\Core\Field\FieldItemListInterface $items
201   *   The field items.
202   * @param array $entities
203   *   The entities array from the widget state.
204   *
205   * @return array
206   *   An array of options for the radios.
207   */
208  public function buildPrimaryOptions(FieldItemListInterface $items, array $entities = []) {
209    $options = [];
210
211    // If we have entities in the widget state, use those.
212    if (!empty($entities)) {
213      foreach ($entities as $delta => $value) {
214        $entity = $value['entity'];
215        $label = $entity->label() ?: $this->t('Item @number', ['@number' => $delta + 1]);
216        $options[$delta] = $label;
217      }
218    }
219    else {
220      // Fallback: build options from field values.
221      $values = $items->getValue();
222      if (empty($values)) {
223        $options[0] = $this->t('First item');
224        return $options;
225      }
226
227      $target_type = $this->getTargetEntityType();
228      $storage = $this->entityTypeManager->getStorage($target_type);
229
230      foreach ($values as $delta => $value) {
231        if (isset($value['target_id'])) {
232          $entity = $storage->load($value['target_id']);
233          if ($entity) {
234            $options[$delta] = $entity->label();
235          }
236          else {
237            $options[$delta] = $this->t('Item @number', ['@number' => $delta + 1]);
238          }
239        }
240        else {
241          $options[$delta] = $this->t('Item @number', ['@number' => $delta + 1]);
242        }
243      }
244    }
245
246    return $options;
247  }
248
249  /**
250   * Validates the primary selection.
251   *
252   * @param array $element
253   *   The form element.
254   * @param \Drupal\Core\Form\FormStateInterface $form_state
255   *   The form state.
256   * @param array $form
257   *   The complete form.
258   */
259  public static function validatePrimarySelection(array $element, FormStateInterface $form_state, array $form) {
260    // Get the widget value.
261    $value = $form_state->getValue($element['#parents']);
262
263    // Check if primary is selected.
264    $primary = $value['primary'] ?? NULL;
265
266    if ($primary === NULL || $primary === '') {
267      $form_state->setError($element, t('Please select a primary item.'));
268    }
269  }
270
271  /**
272   * {@inheritdoc}
273   */
274  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
275    // Get the field name and retrieve the primary selection.
276    $field_name = $this->fieldDefinition->getName();
277    $field_parents = $form['#parents'] ?? [];
278    $parents = array_merge($field_parents, [$field_name, 'primary']);
279
280    $primary_delta = $form_state->getValue($parents);
281
282    // Let the parent massage the values first.
283    $values = parent::massageFormValues($values, $form, $form_state);
284
285    // Reset all primary flags to 0.
286    foreach ($values as $delta => $value) {
287      $values[$delta]['primary'] = 0;
288    }
289
290    // Set the selected primary.
291    if ($primary_delta !== NULL && isset($values[$primary_delta])) {
292      $values[$primary_delta]['primary'] = 1;
293    }
294
295    return $values;
296  }
297
298  /**
299   * {@inheritdoc}
300   */
301  public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
302    parent::extractFormValues($items, $form, $form_state);
303
304    // Get the field name and retrieve the primary selection.
305    $field_name = $this->fieldDefinition->getName();
306    $field_parents = $form['#parents'] ?? [];
307    $parents = array_merge($field_parents, [$field_name, 'primary']);
308
309    $primary_delta = $form_state->getValue($parents);
310
311    // Update the primary flag on the field items.
312    if ($primary_delta !== NULL) {
313      foreach ($items as $delta => $item) {
314        $item->set('primary', $delta == $primary_delta ? 1 : 0);
315      }
316    }
317  }
318
319  /**
320   * Gets the target entity type ID.
321   *
322   * @return string
323   *   The target entity type ID.
324   */
325  public function getTargetEntityType(): string {
326    return $this->fieldDefinition->getSetting('target_type');
327  }
328
329  /**
330   * Gets the target entity type label.
331   *
332   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
333   *   The target entity type label.
334   */
335  public function getTargetEntityTypeLabel() {
336    $entity_type = $this->entityTypeManager->getDefinition($this->getTargetEntityType());
337    return $entity_type->getLabel();
338  }
339
340}