Completed
Push — 8.x-1.x ( 647d91...c9606a )
by Janez
02:38
created

formElementEntities()   D

Complexity

Conditions 20
Paths 79

Size

Total Lines 82
Code Lines 41

Duplication

Lines 8
Ratio 9.76 %

Importance

Changes 0
Metric Value
cc 20
eloc 41
nc 79
nop 3
dl 8
loc 82
rs 4.9176
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Drupal\entity_browser\Plugin\Field\FieldWidget;
4
5
use Drupal\Core\Entity\EntityInterface;
6
use Drupal\entity_browser\Element\EntityBrowserElement;
7
use Symfony\Component\Validator\ConstraintViolationInterface;
8
use Drupal\Component\Utility\Html;
9
use Drupal\Component\Utility\NestedArray;
10
use Drupal\Core\Entity\ContentEntityInterface;
11
use Drupal\Core\Entity\EntityTypeManagerInterface;
12
use Drupal\Core\Field\FieldDefinitionInterface;
13
use Drupal\Core\Field\FieldItemListInterface;
14
use Drupal\Core\Field\WidgetBase;
15
use Drupal\Core\Form\FormStateInterface;
16
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
17
use Drupal\Core\Url;
18
use Drupal\Core\Validation\Plugin\Validation\Constraint\NotNullConstraint;
19
use Drupal\entity_browser\FieldWidgetDisplayManager;
20
use Symfony\Component\DependencyInjection\ContainerInterface;
21
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
22
use Symfony\Component\Validator\ConstraintViolation;
23
use Symfony\Component\Validator\ConstraintViolationListInterface;
24
25
/**
26
 * Plugin implementation of the 'entity_reference' widget for entity browser.
27
 *
28
 * @FieldWidget(
29
 *   id = "entity_browser_entity_reference",
30
 *   label = @Translation("Entity browser"),
31
 *   description = @Translation("Uses entity browser to select entities."),
32
 *   multiple_values = TRUE,
33
 *   field_types = {
34
 *     "entity_reference"
35
 *   }
36
 * )
37
 */
38
class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactoryPluginInterface {
39
40
  /**
41
   * Entity type manager service.
42
   *
43
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
44
   */
45
  protected $entityTypeManager;
46
47
  /**
48
   * Field widget display plugin manager.
49
   *
50
   * @var \Drupal\entity_browser\FieldWidgetDisplayManager
51
   */
52
  protected $fieldDisplayManager;
53
54
  /**
55
   * The depth of the delete button.
56
   *
57
   * This property exists so it can be changed if subclasses.
58
   *
59
   * @var int
60
   */
61
  protected static $deleteDepth = 4;
62
63
  /**
64
   * Constructs widget plugin.
65
   *
66
   * @param string $plugin_id
67
   *   The plugin_id for the plugin instance.
68
   * @param mixed $plugin_definition
69
   *   The plugin implementation definition.
70
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
71
   *   The definition of the field to which the widget is associated.
72
   * @param array $settings
73
   *   The widget settings.
74
   * @param array $third_party_settings
75
   *   Any third party settings.
76
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
77
   *   Entity type manager service.
78
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
79
   *   Event dispatcher.
80
   * @param \Drupal\entity_browser\FieldWidgetDisplayManager $field_display_manager
81
   *   Field widget display plugin manager.
82
   */
83
  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher, FieldWidgetDisplayManager $field_display_manager) {
84
    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
85
    $this->entityTypeManager = $entity_type_manager;
86
    $this->fieldDisplayManager = $field_display_manager;
87
  }
88
89
  /**
90
   * {@inheritdoc}
91
   */
92
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
93
    return new static(
94
      $plugin_id,
95
      $plugin_definition,
96
      $configuration['field_definition'],
97
      $configuration['settings'],
98
      $configuration['third_party_settings'],
99
      $container->get('entity_type.manager'),
100
      $container->get('event_dispatcher'),
101
      $container->get('plugin.manager.entity_browser.field_widget_display')
102
    );
103
  }
104
105
  /**
106
   * {@inheritdoc}
107
   */
108
  public static function defaultSettings() {
109
    return array(
110
      'entity_browser' => NULL,
111
      'open' => FALSE,
112
      'field_widget_display' => NULL,
113
      'field_widget_edit' => TRUE,
114
      'field_widget_remove' => TRUE,
115
      'field_widget_display_settings' => [],
116
      'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND,
117
    ) + parent::defaultSettings();
118
  }
119
120
  /**
121
   * {@inheritdoc}
122
   */
123
  public function settingsForm(array $form, FormStateInterface $form_state) {
124
    $element = parent::settingsForm($form, $form_state);
125
126
    $browsers = [];
127
    /** @var \Drupal\entity_browser\EntityBrowserInterface $browser */
128
    foreach ($this->entityTypeManager->getStorage('entity_browser')->loadMultiple() as $browser) {
129
      $browsers[$browser->id()] = $browser->label();
130
    }
131
132
    $element['entity_browser'] = [
133
      '#title' => $this->t('Entity browser'),
134
      '#type' => 'select',
135
      '#default_value' => $this->getSetting('entity_browser'),
136
      '#options' => $browsers,
137
    ];
138
139
    $target_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
140
    $entity_type = $this->entityTypeManager->getStorage($target_type)->getEntityType();
141
142
    $displays = [];
143
    foreach ($this->fieldDisplayManager->getDefinitions() as $id => $definition) {
144
      if ($this->fieldDisplayManager->createInstance($id)->isApplicable($entity_type)) {
145
        $displays[$id] = $definition['label'];
146
      }
147
    }
148
149
    $id = Html::getUniqueId('field-' . $this->fieldDefinition->getName() . '-display-settings-wrapper');
150
    $element['field_widget_display'] = [
151
      '#title' => $this->t('Entity display plugin'),
152
      '#type' => 'select',
153
      '#default_value' => $this->getSetting('field_widget_display'),
154
      '#options' => $displays,
155
      '#ajax' => [
156
        'callback' => array($this, 'updateSettingsAjax'),
157
        'wrapper' => $id,
158
      ],
159
    ];
160
161
    $element['field_widget_edit'] = [
162
      '#title' => $this->t('Display Edit button'),
163
      '#type' => 'checkbox',
164
      '#default_value' => $this->getSetting('field_widget_edit'),
165
    ];
166
167
    $element['field_widget_remove'] = [
168
      '#title' => $this->t('Display Remove button'),
169
      '#type' => 'checkbox',
170
      '#default_value' => $this->getSetting('field_widget_remove'),
171
    ];
172
173
    $element['open'] = [
174
      '#title' => $this->t('Show widget details as open by default'),
175
      '#description' => $this->t('If marked, the fieldset container that wraps the browser on the entity form will be loaded initially expanded.'),
176
      '#type' => 'checkbox',
177
      '#default_value' => $this->getSetting('open'),
178
    ];
179
180
    $element['selection_mode'] = [
181
      '#title' => $this->t('Selection mode'),
182
      '#description' => $this->t('Determines how selection in entity browser will be handled. Will selection be appended/prepended or it will be replaced in case of editing.'),
183
      '#type' => 'select',
184
      '#options' => EntityBrowserElement::getSelectionModeOptions(),
185
      '#default_value' => $this->getSetting('selection_mode'),
186
    ];
187
188
    $element['field_widget_display_settings'] = [
189
      '#type' => 'fieldset',
190
      '#title' => $this->t('Entity display plugin configuration'),
191
      '#tree' => TRUE,
192
      '#prefix' => '<div id="' . $id . '">',
193
      '#suffix' => '</div>',
194
    ];
195
196
    if ($this->getSetting('field_widget_display')) {
197
      $element['field_widget_display_settings'] += $this->fieldDisplayManager
198
        ->createInstance(
199
          $form_state->getValue(
200
            ['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display'],
201
            $this->getSetting('field_widget_display')
202
          ),
203
          $form_state->getValue(
204
            ['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display_settings'],
205
            $this->getSetting('field_widget_display_settings')
206
          ) + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
207
        )
208
        ->settingsForm($form, $form_state);
209
    }
210
211
    return $element;
212
  }
213
214
  /**
215
   * Ajax callback that updates field widget display settings fieldset.
216
   */
217
  public function updateSettingsAjax(array $form, FormStateInterface $form_state) {
218
    return $form['fields'][$this->fieldDefinition->getName()]['plugin']['settings_edit_form']['settings']['field_widget_display_settings'];
219
  }
220
221
  /**
222
   * {@inheritdoc}
223
   */
224
  public function settingsSummary() {
225
    $summary = $this->summaryBase();
226
    $field_widget_display = $this->getSetting('field_widget_display');
227
228
    if (!empty($field_widget_display)) {
229
      $plugin = $this->fieldDisplayManager->getDefinition($field_widget_display);
230
      $summary[] = $this->t('Entity display: @name', ['@name' => $plugin['label']]);
231
    }
232
    return $summary;
233
  }
234
235
  /**
236
   * {@inheritdoc}
237
   */
238
  public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
239
    if ($violations->count() > 0) {
240
      /** @var \Symfony\Component\Validator\ConstraintViolation $violation */
241
      foreach ($violations as $offset => $violation) {
242
        // The value of the required field is checked through the "not null"
243
        // constraint, whose message is not very useful. We override it here for
244
        // better UX.
245
        if ($violation->getConstraint() instanceof NotNullConstraint) {
0 ignored issues
show
Bug introduced by
The class Drupal\Core\Validation\P...raint\NotNullConstraint does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
246
          $violations->set($offset, new ConstraintViolation(
247
            $this->t('@name field is required.', ['@name' => $items->getFieldDefinition()->getLabel()]),
248
            '',
249
            [],
250
            $violation->getRoot(),
251
            $violation->getPropertyPath(),
252
            $violation->getInvalidValue(),
253
            $violation->getPlural(),
254
            $violation->getCode(),
255
            $violation->getConstraint(),
256
            $violation->getCause()
257
          ));
258
        }
259
      }
260
    }
261
262
    parent::flagErrors($items, $violations, $form, $form_state);
263
  }
264
265
  /**
266
   * Returns a key used to store the previously loaded entity.
267
   *
268
   * @param \Drupal\Core\Field\FieldItemListInterface $items
269
   *   The field items.
270
   *
271
   * @return string
272
   *   A key for form state storage.
273
   */
274
  protected function getFormStateKey(FieldItemListInterface $items) {
275
    return $items->getEntity()->uuid() . ':' . $items->getFieldDefinition()->getName();
276
  }
277
278
  /**
279
   * {@inheritdoc}
280
   */
281
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
282
    $entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
283
    $entities = $this->formElementEntities($items, $element, $form_state);
284
285
    // Get correct ordered list of entity IDs.
286
    $ids = array_map(
287
      function (EntityInterface $entity) {
288
        return $entity->id();
289
      },
290
      $entities
291
    );
292
293
    // We store current entity IDs as we might need them in future requests. If
294
    // some other part of the form triggers an AJAX request with
295
    // #limit_validation_errors we won't have access to the value of the
296
    // target_id element and won't be able to build the form as a result of
297
    // that. This will cause missing submit (Remove, Edit, ...) elements, which
298
    // might result in unpredictable results.
299
    $form_state->set(['entity_browser_widget', $this->getFormStateKey($items)], $ids);
300
301
    $hidden_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName() . '-target-id');
302
    $details_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName());
303
304
    $element += [
305
      '#id' => $details_id,
306
      '#type' => 'details',
307
      '#open' => !empty($entities) || $this->getSetting('open'),
308
      '#required' => $this->fieldDefinition->isRequired(),
309
      // We are not using Entity browser's hidden element since we maintain
310
      // selected entities in it during entire process.
311
      'target_id' => [
312
        '#type' => 'hidden',
313
        '#id' => $hidden_id,
314
        // We need to repeat ID here as it is otherwise skipped when rendering.
315
        '#attributes' => ['id' => $hidden_id],
316
        '#default_value' => implode(' ', array_map(
317
            function (EntityInterface $item) {
318
              return $item->getEntityTypeId() . ':' . $item->id();
319
            },
320
            $entities
321
        )),
322
        // #ajax is officially not supported for hidden elements but if we
323
        // specify event manually it works.
324
        '#ajax' => [
325
          'callback' => [get_class($this), 'updateWidgetCallback'],
326
          'wrapper' => $details_id,
327
          'event' => 'entity_browser_value_updated',
328
        ],
329
      ],
330
    ];
331
332
    // Get configuration required to check entity browser availability.
333
    $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
334
    $selection_mode = $this->getSetting('selection_mode');
335
336
    // Enable entity browser if requirements for that are fulfilled.
337
    if (EntityBrowserElement::isEntityBrowserAvailable($selection_mode, $cardinality, count($ids))) {
338
      $element['entity_browser'] = [
339
        '#type' => 'entity_browser',
340
        '#entity_browser' => $this->getSetting('entity_browser'),
341
        '#cardinality' => $cardinality,
342
        '#selection_mode' => $selection_mode,
343
        '#default_value' => $entities,
344
        '#entity_browser_validators' => ['entity_type' => ['type' => $entity_type]],
345
        '#custom_hidden_id' => $hidden_id,
346
        '#process' => [
347
          ['\Drupal\entity_browser\Element\EntityBrowserElement', 'processEntityBrowser'],
348
          [get_called_class(), 'processEntityBrowser'],
349
        ],
350
      ];
351
352
      $element['#attached']['library'][] = 'entity_browser/entity_reference';
353
    }
354
355
    $field_parents = $element['#field_parents'];
356
357
    $element['current'] = $this->displayCurrentSelection($details_id, $field_parents, $entities);
358
359
    return $element;
360
  }
361
362
  /**
363
   * Render API callback: Processes the entity browser element.
364
   */
365
  public static function processEntityBrowser(&$element, FormStateInterface $form_state, &$complete_form) {
366
    $uuid = key($element['#attached']['drupalSettings']['entity_browser']);
367
    $element['#attached']['drupalSettings']['entity_browser'][$uuid]['selector'] = '#' . $element['#custom_hidden_id'];
368
    return $element;
369
  }
370
371
  /**
372
   * {@inheritdoc}
373
   */
374
  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
375
    $entities = empty($values['target_id']) ? [] : explode(' ', trim($values['target_id']));
376
    $return = [];
377
    foreach ($entities as $entity) {
378
      $return[]['target_id'] = explode(':', $entity)[1];
379
    }
380
381
    return $return;
382
  }
383
384
  /**
385
   * AJAX form callback.
386
   */
387
  public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
388
    $trigger = $form_state->getTriggeringElement();
389
    // AJAX requests can be triggered by hidden "target_id" element when
390
    // entities are added or by one of the "Remove" buttons. Depending on that
391
    // we need to figure out where root of the widget is in the form structure
392
    // and use this information to return correct part of the form.
393 View Code Duplication
    if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
394
      $parents = array_slice($trigger['#array_parents'], 0, -1);
395
    }
396
    elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_remove_')) {
397
      $parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth);
398
    }
399
400
    return NestedArray::getValue($form, $parents);
0 ignored issues
show
Bug introduced by
The variable $parents does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
401
  }
402
403
  /**
404
   * {@inheritdoc}
405
   */
406
  public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
407
    if (($trigger = $form_state->getTriggeringElement())) {
408
      // Can be triggered by "Remove" button.
409
      if (end($trigger['#parents']) === 'remove_button') {
410
        return FALSE;
411
      }
412
    }
413
    return parent::errorElement($element, $violation, $form, $form_state);
414
  }
415
416
  /**
417
   * Submit callback for remove buttons.
418
   */
419
  public static function removeItemSubmit(&$form, FormStateInterface $form_state) {
420
    $triggering_element = $form_state->getTriggeringElement();
421
    if (!empty($triggering_element['#attributes']['data-entity-id']) && isset($triggering_element['#attributes']['data-row-id'])) {
422
      $id = $triggering_element['#attributes']['data-entity-id'];
423
      $row_id = $triggering_element['#attributes']['data-row-id'];
424
      $parents = array_slice($triggering_element['#parents'], 0, -static::$deleteDepth);
425
      $array_parents = array_slice($triggering_element['#array_parents'], 0, -static::$deleteDepth);
426
427
      // Find and remove correct entity.
428
      $values = explode(' ', $form_state->getValue(array_merge($parents, ['target_id'])));
429
      foreach ($values as $index => $item) {
430
        if ($item == $id && $index == $row_id) {
431
          array_splice($values, $index, 1);
432
433
          break;
434
        }
435
      }
436
      $target_id_value = implode(' ', $values);
437
438
      // Set new value for this widget.
439
      $target_id_element = &NestedArray::getValue($form, array_merge($array_parents, ['target_id']));
440
      $form_state->setValueForElement($target_id_element, $target_id_value);
441
      NestedArray::setValue($form_state->getUserInput(), $target_id_element['#parents'], $target_id_value);
442
443
      // Rebuild form.
444
      $form_state->setRebuild();
445
    }
446
  }
447
448
  /**
449
   * Builds the render array for displaying the current results.
450
   *
451
   * @param string $details_id
452
   *   The ID for the details element.
453
   * @param string[] $field_parents
454
   *   Field parents.
455
   * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
456
   *   Array of referenced entities.
457
   *
458
   * @return array
459
   *   The render array for the current selection.
460
   */
461
  protected function displayCurrentSelection($details_id, $field_parents, $entities) {
462
463
    $field_widget_display = $this->fieldDisplayManager->createInstance(
464
      $this->getSetting('field_widget_display'),
465
      $this->getSetting('field_widget_display_settings') + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
466
    );
467
468
    return [
469
      '#theme_wrappers' => ['container'],
470
      '#attributes' => ['class' => ['entities-list']],
471
      'items' => array_map(
472
        function (ContentEntityInterface $entity, $row_id) use ($field_widget_display, $details_id, $field_parents) {
473
          $display = $field_widget_display->view($entity);
474
          if (is_string($display)) {
475
            $display = ['#markup' => $display];
476
          }
477
          return [
478
            '#theme_wrappers' => ['container'],
479
            '#attributes' => [
480
              'class' => ['item-container', Html::getClass($field_widget_display->getPluginId())],
481
              'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(),
482
              'data-row-id' => $row_id,
483
            ],
484
            'display' => $display,
485
            'remove_button' => [
486
              '#type' => 'submit',
487
              '#value' => $this->t('Remove'),
488
              '#ajax' => [
489
                'callback' => [get_class($this), 'updateWidgetCallback'],
490
                'wrapper' => $details_id,
491
              ],
492
              '#submit' => [[get_class($this), 'removeItemSubmit']],
493
              '#name' => $this->fieldDefinition->getName() . '_remove_' . $entity->id() . '_' . $row_id,
494
              '#limit_validation_errors' => [array_merge($field_parents, [$this->fieldDefinition->getName()])],
495
              '#attributes' => [
496
                'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(),
497
                'data-row-id' => $row_id,
498
              ],
499
              '#access' => (bool) $this->getSetting('field_widget_remove'),
500
            ],
501
            'edit_button' => [
502
              '#type' => 'submit',
503
              '#value' => $this->t('Edit'),
504
              '#ajax' => [
505
                'url' => Url::fromRoute(
506
                  'entity_browser.edit_form', [
507
                    'entity_type' => $entity->getEntityTypeId(),
508
                    'entity' => $entity->id(),
509
                  ]
510
                ),
511
                'options' => [
512
                  'query' => [
513
                    'details_id' => $details_id,
514
                  ],
515
                ],
516
              ],
517
              '#access' => (bool) $this->getSetting('field_widget_edit'),
518
            ],
519
          ];
520
        },
521
        $entities,
522
        empty($entities) ? [] : range(0, count($entities) - 1)
523
      ),
524
    ];
525
  }
526
527
  /**
528
   * Gets data that should persist across Entity Browser renders.
529
   *
530
   * @return array
531
   *   Data that should persist after the Entity Browser is rendered.
532
   */
533
  protected function getPersistentData() {
534
    return [
535
      'validators' => [
536
        'entity_type' => ['type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')],
537
      ],
538
    ];
539
  }
540
541
  /**
542
   * Gets options that define where newly added entities are inserted.
543
   *
544
   * @return array
545
   *   Mode labels indexed by key.
546
   */
547
  protected function selectionModeOptions() {
548
    return ['append' => $this->t('Append'), 'prepend' => $this->t('Prepend')];
549
  }
550
551
  /**
552
   * Provides base for settings summary shared by all EB widgets.
553
   *
554
   * @return array
555
   *   A short summary of the widget settings.
556
   */
557
  protected function summaryBase() {
558
    $summary = [];
559
560
    $entity_browser_id = $this->getSetting('entity_browser');
561
    if (empty($entity_browser_id)) {
562
      return [$this->t('No entity browser selected.')];
563
    }
564
    else {
565
      if ($browser = $this->entityTypeManager->getStorage('entity_browser')->load($entity_browser_id)) {
566
        $summary[] = $this->t('Entity browser: @browser', ['@browser' => $browser->label()]);
567
      }
568
      else {
569
        drupal_set_message($this->t('Missing entity browser!'), 'error');
570
        return [$this->t('Missing entity browser!')];
571
      }
572
    }
573
574
    $selection_mode = $this->getSetting('selection_mode');
575
    $selection_mode_options = EntityBrowserElement::getSelectionModeOptions();
576
    if (isset($selection_mode_options[$selection_mode])) {
577
      $summary[] = $this->t('Selection mode: @selection_mode', ['@selection_mode' => $selection_mode_options[$selection_mode]]);
578
    }
579
    else {
580
      $summary[] = $this->t('Undefined selection mode.');
581
    }
582
583
    return $summary;
584
  }
585
586
  /**
587
   * Determines the entities used for the form element.
588
   *
589
   * @param \Drupal\Core\Field\FieldItemListInterface $items
590
   *   The field item to extract the entities from.
591
   * @param array $element
592
   *   The form element.
593
   * @param \Drupal\Core\Form\FormStateInterface $form_state
594
   *   The form state.
595
   *
596
   * @return \Drupal\Core\Entity\EntityInterface[]
597
   *   The list of entities for the form element.
598
   */
599
  protected function formElementEntities(FieldItemListInterface $items, array $element, FormStateInterface $form_state) {
600
    $entities = [];
601
    $entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
602
    $entity_storage = $this->entityTypeManager->getStorage($entity_type);
603
604
    // Find IDs from target_id element (it stores selected entities in form).
605
    // This was added to help solve a really edge casey bug in IEF.
606
    if (($target_id_entities = $this->getEntitiesByTargetId($element, $form_state)) !== FALSE) {
607
      return $target_id_entities;
608
    }
609
610
    // Determine if we're submitting and if submit came from this widget.
611
    $is_relevant_submit = FALSE;
612
    if (($trigger = $form_state->getTriggeringElement())) {
613
      // Can be triggered by hidden target_id element or "Remove" button.
614
      if (end($trigger['#parents']) === 'target_id' || (end($trigger['#parents']) === 'remove_button')) {
615
        $is_relevant_submit = TRUE;
616
617
        // In case there are more instances of this widget on the same page we
618
        // need to check if submit came from this instance.
619
        $field_name_key = end($trigger['#parents']) === 'target_id' ? 2 : static::$deleteDepth + 1;
620
        $field_name_key = count($trigger['#parents']) - $field_name_key;
621
        $is_relevant_submit &= ($trigger['#parents'][$field_name_key] === $this->fieldDefinition->getName()) &&
622
          (array_slice($trigger['#parents'], 0, count($element['#field_parents'])) == $element['#field_parents']);
623
      }
624
    };
625
626
    if ($is_relevant_submit) {
627
      // Submit was triggered by hidden "target_id" element when entities were
628
      // added via entity browser.
629 View Code Duplication
      if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
630
        $parents = $trigger['#parents'];
631
      }
632
      // Submit was triggered by one of the "Remove" buttons. We need to walk
633
      // few levels up to read value of "target_id" element.
634
      elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], $this->fieldDefinition->getName() . '_remove_') === 0) {
635
        $parents = array_merge(array_slice($trigger['#parents'], 0, -static::$deleteDepth), ['target_id']);
636
      }
637
638
      if (isset($parents) && $value = $form_state->getValue($parents)) {
639
        $entities = EntityBrowserElement::processEntityIds($value);
640
        return $entities;
641
      }
642
      return $entities;
643
    }
644
    // IDs from a previous request might be saved in the form state.
645
    elseif ($form_state->has([
646
      'entity_browser_widget',
647
      $this->getFormStateKey($items),
648
    ])
649
    ) {
650
      $stored_ids = $form_state->get([
651
        'entity_browser_widget',
652
        $this->getFormStateKey($items),
653
      ]);
654
      $indexed_entities = $entity_storage->loadMultiple($stored_ids);
655
656
      // Selection can contain same entity multiple times. Since loadMultiple()
657
      // returns unique list of entities, it's necessary to recreate list of
658
      // entities in order to preserve selection of duplicated entities.
659
      foreach ($stored_ids as $entity_id) {
660
        if (isset($indexed_entities[$entity_id])) {
661
          $entities[] = $indexed_entities[$entity_id];
662
        }
663
      }
664
      return $entities;
665
    }
666
    // We are loading for for the first time so we need to load any existing
667
    // values that might already exist on the entity. Also, remove any leftover
668
    // data from removed entity references.
669
    else {
670
      foreach ($items as $item) {
671
        if (isset($item->target_id)) {
672
          $entity = $entity_storage->load($item->target_id);
673
          if (!empty($entity)) {
674
            $entities[] = $entity;
675
          }
676
        }
677
      }
678
      return $entities;
679
    }
680
  }
681
682
  /**
683
   * {@inheritdoc}
684
   */
685
  public function calculateDependencies() {
686
    $dependencies = parent::calculateDependencies();
687
688
    // If an entity browser is being used in this widget, add it as a config
689
    // dependency.
690
    if ($browser_name = $this->getSetting('entity_browser')) {
691
      $dependencies['config'][] = 'entity_browser.browser.' . $browser_name;
692
    }
693
694
    return $dependencies;
695
  }
696
697
  /**
698
   * Get selected elements from target_id element on form.
699
   *
700
   * @param array $element
701
   *   The form element.
702
   * @param \Drupal\Core\Form\FormStateInterface $form_state
703
   *   The form state.
704
   *
705
   * @return \Drupal\Core\Entity\EntityInterface[]|false
706
   *   Return list of entities if they are available or false.
707
   */
708
  protected function getEntitiesByTargetId(array $element, FormStateInterface $form_state) {
709
    $target_id_element_path = array_merge(
710
      $element['#field_parents'],
711
      [$this->fieldDefinition->getName(), 'target_id']
712
    );
713
714
    if (!NestedArray::keyExists($form_state->getUserInput(), $target_id_element_path)) {
715
      return FALSE;
716
    }
717
718
    // TODO Figure out how to avoid using raw user input.
719
    $current_user_input = NestedArray::getValue($form_state->getUserInput(), $target_id_element_path);
720
    if (!is_array($current_user_input)) {
721
      $entities = EntityBrowserElement::processEntityIds($current_user_input);
722
      return $entities;
723
    }
724
725
    return FALSE;
726
  }
727
728
}
729