Completed
Pull Request — 8.x-1.x (#117)
by
unknown
03:25
created

EntityReference::prepareValidators()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
3
/**
4
 * @file
5
 * Contains \Drupal\entity_browser\Plugin\Field\FieldWidget\EntityReference.
6
 */
7
8
namespace Drupal\entity_browser\Plugin\Field\FieldWidget;
9
10
use Drupal\Component\Utility\Html;
11
use Drupal\Component\Utility\NestedArray;
12
use Drupal\Core\Entity\ContentEntityInterface;
13
use Drupal\Core\Entity\EntityManagerInterface;
14
use Drupal\Core\Field\FieldDefinitionInterface;
15
use Drupal\Core\Field\FieldItemListInterface;
16
use Drupal\Core\Field\FieldStorageDefinitionInterface;
17
use Drupal\Core\Field\WidgetBase;
18
use Drupal\Core\Form\FormStateInterface;
19
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
20
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
21
use Drupal\Core\Url;
22
use Drupal\entity_browser\Events\Events;
23
use Drupal\entity_browser\Events\RegisterJSCallbacks;
24
use Drupal\entity_browser\FieldWidgetDisplayManager;
25
use Symfony\Component\DependencyInjection\ContainerInterface;
26
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
27
28
/**
29
 * Plugin implementation of the 'entity_reference' widget for entity browser.
30
31
 * @FieldWidget(
32
 *   id = "entity_browser_entity_reference",
33
 *   label = @Translation("Entity browser"),
34
 *   description = @Translation("Uses entity browser to select entities."),
35
 *   multiple_values = TRUE,
36
 *   field_types = {
37
 *     "entity_reference"
38
 *   }
39
 * )
40
 */
41
class EntityReference extends WidgetBase implements ContainerFactoryPluginInterface {
42
43
  /**
44
   * Entity manager service
45
   *
46
   * @var \Drupal\Core\Entity\EntityManagerInterface
47
   */
48
  protected $entityManager;
49
50
  /**
51
   * Field widget display plugin manager.
52
   *
53
   * @var \Drupal\entity_browser\FieldWidgetDisplayManager
54
   */
55
  protected $fieldDisplayManager;
56
57
  /**
58
   * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
59
   */
60
  protected $keyValue;
61
62
  /**
63
   * The depth of the delete button.
64
   *
65
   * This property exists so it can be changed if subclasses
66
   *
67
   * @var int
68
   */
69
  protected static $deleteDepth = 4;
70
71
  /**
72
   * Constructs widget plugin.
73
   *
74
   * @param string $plugin_id
75
   *   The plugin_id for the plugin instance.
76
   * @param mixed $plugin_definition
77
   *   The plugin implementation definition.
78
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
79
   *   The definition of the field to which the widget is associated.
80
   * @param array $settings
81
   *   The widget settings.
82
   * @param array $third_party_settings
83
   *   Any third party settings.
84
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
85
   *   Entity manager service.
86
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
87
   *   Event dispatcher.
88
   * @param \Drupal\entity_browser\FieldWidgetDisplayManager $field_display_manager
89
   *   Field widget display plugin manager.
90
   * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value
91
   *   The key value store.
92
   */
93 View Code Duplication
  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityManagerInterface $entity_manager, EventDispatcherInterface $event_dispatcher, FieldWidgetDisplayManager $field_display_manager, KeyValueStoreExpirableInterface $key_value) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
94
    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
95
    $this->entityManager = $entity_manager;
96
    $this->fieldDisplayManager = $field_display_manager;
97
    $this->keyValue = $key_value;
98
  }
99
100
  /**
101
   * {@inheritdoc}
102
   */
103
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
104
    return new static(
105
      $plugin_id,
106
      $plugin_definition,
107
      $configuration['field_definition'],
108
      $configuration['settings'],
109
      $configuration['third_party_settings'],
110
      $container->get('entity.manager'),
111
      $container->get('event_dispatcher'),
112
      $container->get('plugin.manager.entity_browser.field_widget_display'),
113
      $container->get('keyvalue.expirable')->get('entity_browser')
114
    );
115
  }
116
117
  /**
118
   * {@inheritdoc}
119
   */
120
  public static function defaultSettings() {
121
    return array(
122
      'entity_browser' => NULL,
123
      'open' => FALSE,
124
      'field_widget_display' => NULL,
125
      'field_widget_edit' => TRUE,
126
      'field_widget_remove' => TRUE,
127
      'field_widget_display_settings' => [],
128
    ) + parent::defaultSettings();
129
  }
130
131
  /**
132
   * {@inheritdoc}
133
   */
134
  public function settingsForm(array $form, FormStateInterface $form_state) {
135
    $element = parent::settingsForm($form, $form_state);
136
137
    $browsers = [];
138
    /** @var \Drupal\entity_browser\EntityBrowserInterface $browser */
139
    foreach ($this->entityManager->getStorage('entity_browser')->loadMultiple() as $browser) {
140
      $browsers[$browser->id()] = $browser->label();
141
    }
142
143
    $element['entity_browser'] = [
144
      '#title' => t('Entity browser'),
145
      '#type' => 'select',
146
      '#default_value' => $this->getSetting('entity_browser'),
147
      '#options' => $browsers,
148
    ];
149
150
    $target_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
151
    $entity_type = \Drupal::entityTypeManager()->getStorage($target_type)->getEntityType();
152
153
    $displays = [];
154
    foreach ($this->fieldDisplayManager->getDefinitions() as $id => $definition) {
155
      if ($this->fieldDisplayManager->createInstance($id)->isApplicable($entity_type)) {
156
        $displays[$id] = $definition['label'];
157
      }
158
    }
159
160
    $id = Html::getUniqueId('field-' . $this->fieldDefinition->getName() . '-display-settings-wrapper');
161
    $element['field_widget_display'] = [
162
      '#title' => t('Entity display plugin'),
163
      '#type' => 'select',
164
      '#default_value' => $this->getSetting('field_widget_display'),
165
      '#options' => $displays,
166
      '#ajax' => [
167
        'callback' => array($this, 'updateSettingsAjax'),
168
        'wrapper' => $id,
169
      ],
170
    ];
171
172
    $element['field_widget_edit'] = [
173
      '#title' => t('Display Edit button'),
174
      '#type' => 'checkbox',
175
      '#default_value' => $this->getSetting('field_widget_edit')
176
    ];
177
178
    $element['field_widget_remove'] = [
179
      '#title' => t('Display Remove button'),
180
      '#type' => 'checkbox',
181
      '#default_value' => $this->getSetting('field_widget_remove')
182
    ];
183
184
    $element['open'] = [
185
      '#title' => t('Show widget details as open by default'),
186
      '#type' => 'checkbox',
187
      '#default_value' => $this->getSetting('open')
188
    ];
189
190
    $element['field_widget_display_settings'] = [
191
      '#type' => 'fieldset',
192
      '#title' => t('Entity display plugin configuration'),
193
      '#tree' => TRUE,
194
      '#prefix' => '<div id="' . $id . '">',
195
      '#suffix' => '</div>',
196
    ];
197
198
    if ($this->getSetting('field_widget_display')) {
199
      $element['field_widget_display_settings'] += $this->fieldDisplayManager
200
        ->createInstance(
201
          $form_state->getValue(
202
            ['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display'],
203
            $this->getSetting('field_widget_display')
204
          ),
205
          $form_state->getValue(
206
            ['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display_settings'],
207
            $this->getSetting('field_widget_display_settings')
208
          ) + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
209
        )
210
        ->settingsForm($form, $form_state);
211
    }
212
213
    return $element;
214
  }
215
216
  /**
217
   * Ajax callback that updates field widget display settings fieldset.
218
   */
219
  public function updateSettingsAjax(array $form, FormStateInterface $form_state) {
220
    return $form['fields'][$this->fieldDefinition->getName()]['plugin']['settings_edit_form']['settings']['field_widget_display_settings'];
221
  }
222
223
  /**
224
   * {@inheritdoc}
225
   */
226
  public function settingsSummary() {
227
    $summary = [];
228
    $entity_browser_id = $this->getSetting('entity_browser');
229
    $field_widget_display = $this->getSetting('field_widget_display');
230
231
    if (empty($entity_browser_id)) {
232
      return [t('No entity browser selected.')];
233
    }
234
    else {
235
      $browser = $this->entityManager->getStorage('entity_browser')
236
        ->load($entity_browser_id);
237
      $summary[] = t('Entity browser: @browser', ['@browser' => $browser->label()]);
238
    }
239
240
    if (!empty($field_widget_display)) {
241
      $plugin = $this->fieldDisplayManager->getDefinition($field_widget_display);
242
      $summary[] = t('Entity display: @name', ['@name' => $plugin['label']]);
243
    }
244
245
    return $summary;
246
  }
247
248
  /**
249
   * {@inheritdoc}
250
   */
251
  function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
252
    $entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
253
    $entity_storage = $this->entityManager->getStorage($entity_type);
254
255
    $ids = [];
256
    $entities = [];
257
258
    // Determine if we're submitting and if submit came from this widget.
259
    $is_relevant_submit = FALSE;
260
    if (($trigger = $form_state->getTriggeringElement())) {
261
      // Can be triggered by hidden target_id element or "Remove" button.
262
      if (end($trigger['#parents']) === 'target_id' || (end($trigger['#parents']) === 'remove_button')) {
263
        $is_relevant_submit = TRUE;
264
265
        // In case there are more instances of this widget on the same page we
266
        // need to check if submit came from this instance.
267
        $field_name_key = end($trigger['#parents']) === 'target_id' ? 2 : static::$deleteDepth + 1;
268
        $field_name_key = sizeof($trigger['#parents']) - $field_name_key;
269
        $is_relevant_submit &= ($trigger['#parents'][$field_name_key] === $this->fieldDefinition->getName());
270
      }
271
    };
272
273
    if ($is_relevant_submit) {
274
      // Submit was triggered by hidden "target_id" element when entities were
275
      // added via entity browser.
276 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...
277
        $parents = $trigger['#parents'];
278
      }
279
      // Submit was triggered by one of the "Remove" buttons. We need to walk
280
      // few levels up to read value of "target_id" element.
281
      elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], $this->fieldDefinition->getName() . '_remove_') === 0) {
282
        $parents = array_merge(array_slice($trigger['#parents'], 0, -static::$deleteDepth), ['target_id']);
283
      }
284
285
      if (isset($parents) && $value = $form_state->getValue($parents)) {
286
        $ids = explode(' ', $value);
287
        $entities = $entity_storage->loadMultiple($ids);
288
      }
289
    }
290
    // We are loading for for the first time so we need to load any existing
291
    // values that might already exist on the entity. Also, remove any leftover
292
    // data from removed entity references.
293
    else {
294
      foreach ($items as $item) {
295
        $entity = $entity_storage->load($item->target_id);
296
        if (!empty($entity)) {
297
          $entities[$item->target_id] = $entity;
298
        }
299
      }
300
      $ids = array_keys($entities);
301
    }
302
    $ids = array_filter($ids);
303
304
    $hidden_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName() . '-target-id');
305
    $details_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName());
306
    /** @var \Drupal\entity_browser\EntityBrowserInterface $entity_browser */
307
    $entity_browser = $this->entityManager->getStorage('entity_browser')->load($this->getSetting('entity_browser'));
308
309
    $element += [
310
      '#id' => $details_id,
311
      '#type' => 'details',
312
      '#open' => !empty($ids) || $this->getSetting('open'),
313
      'target_id' => [
314
        '#type' => 'hidden',
315
        '#id' => $hidden_id,
316
        // We need to repeat ID here as it is otherwise skipped when rendering.
317
        '#attributes' => ['id' => $hidden_id],
318
        '#default_value' => $ids,
319
        // #ajax is officially not supported for hidden elements but if we
320
        // specify event manually it works.
321
        '#ajax' => [
322
          'callback' => [get_class($this), 'updateWidgetCallback'],
323
          'wrapper' => $details_id,
324
          'event' => 'entity_browser_value_updated',
325
        ],
326
      ],
327
    ];
328
329
330
    // Gather and set validators.
331
    // @todo Is there a better place to do that?
332
    $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
333
    $validators = $this->prepareValidators(['cardinality' => ['min' => $cardinality]]);
334
335
    if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($ids) < $cardinality) {
336
      $element['entity_browser'] = $entity_browser->getDisplay()->displayEntityBrowser($validators);
337
      $element['#attached']['library'][] = 'entity_browser/entity_reference';
338
      $element['#attached']['drupalSettings']['entity_browser'] = [
339
        $entity_browser->getDisplay()->getUuid() => [
340
          'cardinality' => $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(),
341
          'selector' => '#'.$element['target_id']['#attributes']['id'],
342
        ]
343
      ];
344
    }
345
346
    $field_parents = $element['#field_parents'];
347
348
    $element['current'] = $this->displayCurrentSelection($details_id, $field_parents, $entities);
349
350
    return $element;
351
  }
352
353
  /**
354
   * {@inheritdoc}
355
   */
356
  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
357
    $ids = empty($values['target_id']) ? [] : explode(' ', trim($values['target_id']));
358
    $return = [];
359
    foreach ($ids as $id) {
360
      $return[]['target_id'] = $id;
361
    }
362
363
    return $return;
364
  }
365
366
  /**
367
   * AJAX form callback.
368
   */
369
  public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
370
    $trigger = $form_state->getTriggeringElement();
371
    // AJAX requests can be triggered by hidden "target_id" element when entities
372
    // are added or by one of the "Remove" buttons. Depending on that we need to
373
    // figure out where root of the widget is in the form structure and use this
374
    // information to return correct part of the form.
375 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...
376
      $parents = array_slice($trigger['#array_parents'], 0, -1);
377
    }
378
    elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_remove_')) {
379
      $parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth);
380
    }
381
382
    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...
383
  }
384
385
  /**
386
   * Submit callback for remove buttons.
387
   */
388
  public static function removeItemSubmit(&$form, FormStateInterface $form_state) {
389
    $triggering_element = $form_state->getTriggeringElement();
390
    if (!empty($triggering_element['#attributes']['data-entity-id'])) {
391
      $id = $triggering_element['#attributes']['data-entity-id'];
392
      $parents = array_slice($triggering_element['#parents'], 0, -static::$deleteDepth);
393
      $array_parents = array_slice($triggering_element['#array_parents'], 0, -static::$deleteDepth);
394
395
      // Find and remove correct entity.
396
      $values = explode(' ', $form_state->getValue(array_merge($parents, ['target_id'])));
397
      $values = array_filter(
398
        $values,
399
        function($item) use ($id) { return $item != $id; }
400
      );
401
      $values = implode(' ', $values);
402
403
      // Set new value for this widget.
404
      $target_id_element = &NestedArray::getValue($form, array_merge($array_parents, ['target_id']));
405
      $form_state->setValueForElement($target_id_element, $values);
406
      NestedArray::setValue($form_state->getUserInput(), $target_id_element['#parents'], $values);
407
408
      // Rebuild form.
409
      $form_state->setRebuild();
410
    }
411
  }
412
413
  /**
414
   * Builds the render array for displaying the current results.
415
   *
416
   * @param string $details_id
417
   *   The ID for the details element.
418
   * @param string[] $field_parents
419
   *   Field parents.
420
   * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
421
   *
422
   * @return array
423
   *   The render array for the current selection.
424
   */
425
  protected function displayCurrentSelection($details_id, $field_parents, $entities) {
426
427
    $field_widget_display = $this->fieldDisplayManager->createInstance(
428
      $this->getSetting('field_widget_display'),
429
      $this->getSetting('field_widget_display_settings') + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
430
    );
431
432
    return [
433
      '#theme_wrappers' => ['container'],
434
      '#attributes' => ['class' => ['entities-list']],
435
      'items' => array_map(
436
        function (ContentEntityInterface $entity) use ($field_widget_display, $details_id, $field_parents) {
437
          $display = $field_widget_display->view($entity);
438
          if (is_string($display)) {
439
            $display = ['#markup' => $display];
440
          }
441
          return [
442
            '#theme_wrappers' => ['container'],
443
            '#attributes' => [
444
              'class' => ['item-container', Html::getClass($field_widget_display->getPluginId())],
445
              'data-entity-id' => $entity->id()
446
            ],
447
            'display' => $display,
448
            'remove_button' => [
449
              '#type' => 'submit',
450
              '#value' => $this->t('Remove'),
451
              '#ajax' => [
452
                'callback' => [get_class($this), 'updateWidgetCallback'],
453
                'wrapper' => $details_id,
454
              ],
455
              '#submit' => [[get_class($this), 'removeItemSubmit']],
456
              '#name' => $this->fieldDefinition->getName() . '_remove_' . $entity->id(),
457
              '#limit_validation_errors' => [array_merge($field_parents, [$this->fieldDefinition->getName()])],
458
              '#attributes' => ['data-entity-id' => $entity->id()],
459
              '#access' => (bool) $this->getSetting('field_widget_remove')
460
            ],
461
            'edit_button' => [
462
              '#type' => 'submit',
463
              '#value' => $this->t('Edit'),
464
              '#ajax' => [
465
                'url' => Url::fromRoute(
466
                  'entity_browser.edit_form', [
467
                  'entity_type' => $entity->getEntityTypeId(),
468
                  'entity' => $entity->id()
469
                ]
470
                )
471
              ],
472
              '#access' => (bool) $this->getSetting('field_widget_edit')
473
            ]
474
          ];
475
        },
476
        $entities
477
      ),
478
    ];
479
  }
480
481
  /**
482
   * Prepare validators.
483
   *
484
   * Saves Entity Browser Widget validators in key/value storage if an identical
485
   * set of constraints is not already stored there.
486
   *
487
   * @param array $validators
488
   *   An array where keys are validator ids and values configurations for them.
489
   *
490
   * @return string
491
   *   The hash generated from hashing the validators array.
492
   */
493
  public function prepareValidators(array $validators) {
494
    // Generate the hash that we use as key for the key/value.
495
    $hash = md5(serialize($validators));
496
497
    if (!$this->keyValue->has($hash)) {
498
      $this->keyValue->set($hash, $validators);
499
    }
500
501
    return $hash;
502
  }
503
504
  /**
505
   * Get validators.
506
   *
507
   * @param \Drupal\entity_browser\string $hash
508
   *   The hash generated from hashing the validators array.
509
   *
510
   * @return mixed
511
   *   An array where keys are validator ids and values configurations for them
512
   *   or empty array if no validators are stored.
513
   */
514
  public function getValidators($hash) {
515
    return $this->keyValue->get($hash, []);
516
  }
517
}
518