Completed
Pull Request — 8.x-1.x (#134)
by
unknown
12:27
created

EntityReference   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 426
Duplicated Lines 3.29 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 26
Bugs 2 Features 4
Metric Value
wmc 41
c 26
b 2
f 4
lcom 1
cbo 2
dl 14
loc 426
rs 8.2769

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A create() 0 12 1
A defaultSettings() 0 10 1
B settingsForm() 0 81 5
A updateSettingsAjax() 0 3 1
A settingsSummary() 0 21 3
D formElement() 8 96 17
A massageFormValues() 0 9 3
B updateWidgetCallback() 6 15 5
B removeItemSubmit() 0 24 2
A displayCurrentSelection() 0 55 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like EntityReference often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntityReference, and based on these observations, apply Extract Interface, too.

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\Plugin\ContainerFactoryPluginInterface;
20
use Drupal\Core\Url;
21
use Drupal\entity_browser\Events\Events;
22
use Drupal\entity_browser\Events\RegisterJSCallbacks;
23
use Drupal\entity_browser\FieldWidgetDisplayManager;
24
use Symfony\Component\DependencyInjection\ContainerInterface;
25
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
26
27
/**
28
 * Plugin implementation of the 'entity_reference' widget for entity browser.
29
30
 * @FieldWidget(
31
 *   id = "entity_browser_entity_reference",
32
 *   label = @Translation("Entity browser"),
33
 *   description = @Translation("Uses entity browser to select entities."),
34
 *   multiple_values = TRUE,
35
 *   field_types = {
36
 *     "entity_reference"
37
 *   }
38
 * )
39
 */
40
class EntityReference extends WidgetBase implements ContainerFactoryPluginInterface {
41
42
  /**
43
   * Entity manager service
44
   *
45
   * @var \Drupal\Core\Entity\EntityManagerInterface
46
   */
47
  protected $entityManager;
48
49
  /**
50
   * Field widget display plugin manager.
51
   *
52
   * @var \Drupal\entity_browser\FieldWidgetDisplayManager
53
   */
54
  protected $fieldDisplayManager;
55
56
  /**
57
   * The depth of the delete button.
58
   *
59
   * This property exists so it can be changed if subclasses
60
   *
61
   * @var int
62
   */
63
  protected static $deleteDepth = 4;
64
65
  /**
66
   * Constructs widget plugin.
67
   *
68
   * @param string $plugin_id
69
   *   The plugin_id for the plugin instance.
70
   * @param mixed $plugin_definition
71
   *   The plugin implementation definition.
72
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
73
   *   The definition of the field to which the widget is associated.
74
   * @param array $settings
75
   *   The widget settings.
76
   * @param array $third_party_settings
77
   *   Any third party settings.
78
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
79
   *   Entity manager service.
80
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
81
   *   Event dispatcher.
82
   * @param \Drupal\entity_browser\FieldWidgetDisplayManager $field_display_manager
83
   *   Field widget display plugin manager.
84
   */
85
  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) {
86
    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
87
    $this->entityManager = $entity_manager;
88
    $this->fieldDisplayManager = $field_display_manager;
89
  }
90
91
  /**
92
   * {@inheritdoc}
93
   */
94
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
95
    return new static(
96
      $plugin_id,
97
      $plugin_definition,
98
      $configuration['field_definition'],
99
      $configuration['settings'],
100
      $configuration['third_party_settings'],
101
      $container->get('entity.manager'),
102
      $container->get('event_dispatcher'),
103
      $container->get('plugin.manager.entity_browser.field_widget_display')
104
    );
105
  }
106
107
  /**
108
   * {@inheritdoc}
109
   */
110
  public static function defaultSettings() {
111
    return array(
112
      'entity_browser' => NULL,
113
      'open' => FALSE,
114
      'field_widget_display' => NULL,
115
      'field_widget_edit' => TRUE,
116
      'field_widget_remove' => TRUE,
117
      'field_widget_display_settings' => [],
118
    ) + parent::defaultSettings();
119
  }
120
121
  /**
122
   * {@inheritdoc}
123
   */
124
  public function settingsForm(array $form, FormStateInterface $form_state) {
125
    $element = parent::settingsForm($form, $form_state);
126
127
    $browsers = [];
128
    /** @var \Drupal\entity_browser\EntityBrowserInterface $browser */
129
    foreach ($this->entityManager->getStorage('entity_browser')->loadMultiple() as $browser) {
130
      $browsers[$browser->id()] = $browser->label();
131
    }
132
133
    $element['entity_browser'] = [
134
      '#title' => t('Entity browser'),
135
      '#type' => 'select',
136
      '#default_value' => $this->getSetting('entity_browser'),
137
      '#options' => $browsers,
138
    ];
139
140
    $target_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
141
    $entity_type = \Drupal::entityTypeManager()->getStorage($target_type)->getEntityType();
142
143
    $displays = [];
144
    foreach ($this->fieldDisplayManager->getDefinitions() as $id => $definition) {
145
      if ($this->fieldDisplayManager->createInstance($id)->isApplicable($entity_type)) {
146
        $displays[$id] = $definition['label'];
147
      }
148
    }
149
150
    $id = Html::getUniqueId('field-' . $this->fieldDefinition->getName() . '-display-settings-wrapper');
151
    $element['field_widget_display'] = [
152
      '#title' => t('Entity display plugin'),
153
      '#type' => 'select',
154
      '#default_value' => $this->getSetting('field_widget_display'),
155
      '#options' => $displays,
156
      '#ajax' => [
157
        'callback' => array($this, 'updateSettingsAjax'),
158
        'wrapper' => $id,
159
      ],
160
    ];
161
162
    $element['field_widget_edit'] = [
163
      '#title' => t('Display Edit button'),
164
      '#type' => 'checkbox',
165
      '#default_value' => $this->getSetting('field_widget_edit')
166
    ];
167
168
    $element['field_widget_remove'] = [
169
      '#title' => t('Display Remove button'),
170
      '#type' => 'checkbox',
171
      '#default_value' => $this->getSetting('field_widget_remove')
172
    ];
173
174
    $element['open'] = [
175
      '#title' => t('Show widget details as open by default'),
176
      '#type' => 'checkbox',
177
      '#default_value' => $this->getSetting('open')
178
    ];
179
180
    $element['field_widget_display_settings'] = [
181
      '#type' => 'fieldset',
182
      '#title' => t('Entity display plugin configuration'),
183
      '#tree' => TRUE,
184
      '#prefix' => '<div id="' . $id . '">',
185
      '#suffix' => '</div>',
186
    ];
187
188
    if ($this->getSetting('field_widget_display')) {
189
      $element['field_widget_display_settings'] += $this->fieldDisplayManager
190
        ->createInstance(
191
          $form_state->getValue(
192
            ['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display'],
193
            $this->getSetting('field_widget_display')
194
          ),
195
          $form_state->getValue(
196
            ['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display_settings'],
197
            $this->getSetting('field_widget_display_settings')
198
          ) + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
199
        )
200
        ->settingsForm($form, $form_state);
201
    }
202
203
    return $element;
204
  }
205
206
  /**
207
   * Ajax callback that updates field widget display settings fieldset.
208
   */
209
  public function updateSettingsAjax(array $form, FormStateInterface $form_state) {
210
    return $form['fields'][$this->fieldDefinition->getName()]['plugin']['settings_edit_form']['settings']['field_widget_display_settings'];
211
  }
212
213
  /**
214
   * {@inheritdoc}
215
   */
216
  public function settingsSummary() {
217
    $summary = [];
218
    $entity_browser_id = $this->getSetting('entity_browser');
219
    $field_widget_display = $this->getSetting('field_widget_display');
220
221
    if (empty($entity_browser_id)) {
222
      return [t('No entity browser selected.')];
223
    }
224
    else {
225
      $browser = $this->entityManager->getStorage('entity_browser')
226
        ->load($entity_browser_id);
227
      $summary[] = t('Entity browser: @browser', ['@browser' => $browser->label()]);
228
    }
229
230
    if (!empty($field_widget_display)) {
231
      $plugin = $this->fieldDisplayManager->getDefinition($field_widget_display);
232
      $summary[] = t('Entity display: @name', ['@name' => $plugin['label']]);
233
    }
234
235
    return $summary;
236
  }
237
238
  /**
239
   * {@inheritdoc}
240
   */
241
  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...
242
    $entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
243
    $entity_storage = $this->entityManager->getStorage($entity_type);
244
245
    $ids = [];
246
    $entities = [];
247
248
    // Determine if we're submitting and if submit came from this widget.
249
    $is_relevant_submit = FALSE;
250
    if (($trigger = $form_state->getTriggeringElement())) {
251
      // Can be triggered by hidden target_id element or "Remove" button.
252
      if (end($trigger['#parents']) === 'target_id' || (end($trigger['#parents']) === 'remove_button')) {
253
        $is_relevant_submit = TRUE;
254
255
        // In case there are more instances of this widget on the same page we
256
        // need to check if submit came from this instance.
257
        $field_name_key = end($trigger['#parents']) === 'target_id' ? 2 : static::$deleteDepth + 1;
258
        $field_name_key = sizeof($trigger['#parents']) - $field_name_key;
259
        $is_relevant_submit &= ($trigger['#parents'][$field_name_key] === $this->fieldDefinition->getName());
260
      }
261
    };
262
263
    if ($is_relevant_submit) {
264
      // Submit was triggered by hidden "target_id" element when entities were
265
      // added via entity browser.
266 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...
267
        $parents = $trigger['#parents'];
268
      }
269
      // Submit was triggered by one of the "Remove" buttons. We need to walk
270
      // few levels up to read value of "target_id" element.
271
      elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], $this->fieldDefinition->getName() . '_remove_') === 0) {
272
        $parents = array_merge(array_slice($trigger['#parents'], 0, -static::$deleteDepth), ['target_id']);
273
      }
274
275
      if (isset($parents) && $value = $form_state->getValue($parents)) {
276
        $ids = explode(' ', $value);
277
        $entities = $entity_storage->loadMultiple($ids);
278
      }
279
    }
280
    // We are loading for for the first time so we need to load any existing
281
    // values that might already exist on the entity. Also, remove any leftover
282
    // data from removed entity references.
283
    else {
284
      foreach ($items as $item) {
285
        $entity = $entity_storage->load($item->target_id);
286
        if (!empty($entity)) {
287
          $entities[$item->target_id] = $entity;
288
        }
289
      }
290
      $ids = array_keys($entities);
291
    }
292
    $ids = array_filter($ids);
293
294
    $hidden_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName() . '-target-id');
295
    $details_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName());
296
    /** @var \Drupal\entity_browser\EntityBrowserInterface $entity_browser */
297
    $entity_browser = $this->entityManager->getStorage('entity_browser')->load($this->getSetting('entity_browser'));
298
299
    $element += [
300
      '#id' => $details_id,
301
      '#type' => 'details',
302
      '#open' => !empty($ids) || $this->getSetting('open'),
303
      'target_id' => [
304
        '#type' => 'hidden',
305
        '#id' => $hidden_id,
306
        // We need to repeat ID here as it is otherwise skipped when rendering.
307
        '#attributes' => ['id' => $hidden_id],
308
        '#default_value' => $ids,
309
        // #ajax is officially not supported for hidden elements but if we
310
        // specify event manually it works.
311
        '#ajax' => [
312
          'callback' => [get_class($this), 'updateWidgetCallback'],
313
          'wrapper' => $details_id,
314
          'event' => 'entity_browser_value_updated',
315
        ],
316
      ],
317
    ];
318
319
    $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
320
    if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($ids) < $cardinality) {
321
      $element['entity_browser'] = $entity_browser->getDisplay()->displayEntityBrowser();
322
      $element['#attached']['library'][] = 'entity_browser/entity_reference';
323
      $element['#attached']['drupalSettings']['entity_browser'] = [
324
        $entity_browser->getDisplay()->getUuid() => [
325
          'cardinality' => $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(),
326
          'selector' => '#'.$element['target_id']['#attributes']['id'],
327
        ]
328
      ];
329
    }
330
331
    $field_parents = $element['#field_parents'];
332
333
    $element['current'] = $this->displayCurrentSelection($details_id, $field_parents, $entities);
334
335
    return $element;
336
  }
337
338
  /**
339
   * {@inheritdoc}
340
   */
341
  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
342
    $ids = empty($values['target_id']) ? [] : explode(' ', trim($values['target_id']));
343
    $return = [];
344
    foreach ($ids as $id) {
345
      $return[]['target_id'] = $id;
346
    }
347
348
    return $return;
349
  }
350
351
  /**
352
   * AJAX form callback.
353
   */
354
  public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
355
    $trigger = $form_state->getTriggeringElement();
356
    // AJAX requests can be triggered by hidden "target_id" element when entities
357
    // are added or by one of the "Remove" buttons. Depending on that we need to
358
    // figure out where root of the widget is in the form structure and use this
359
    // information to return correct part of the form.
360 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...
361
      $parents = array_slice($trigger['#array_parents'], 0, -2);
362
    }
363
    elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_remove_')) {
364
      $parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth);
365
    }
366
367
    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...
368
  }
369
370
  /**
371
   * Submit callback for remove buttons.
372
   */
373
  public static function removeItemSubmit(&$form, FormStateInterface $form_state) {
374
    $triggering_element = $form_state->getTriggeringElement();
375
    if (!empty($triggering_element['#attributes']['data-entity-id'])) {
376
      $id = $triggering_element['#attributes']['data-entity-id'];
377
      $parents = array_slice($triggering_element['#parents'], 0, -static::$deleteDepth);
378
      $array_parents = array_slice($triggering_element['#array_parents'], 0, -static::$deleteDepth);
379
380
      // Find and remove correct entity.
381
      $values = explode(' ', $form_state->getValue(array_merge($parents, ['target_id'])));
382
      $values = array_filter(
383
        $values,
384
        function($item) use ($id) { return $item != $id; }
385
      );
386
      $values = implode(' ', $values);
387
388
      // Set new value for this widget.
389
      $target_id_element = &NestedArray::getValue($form, array_merge($array_parents, ['target_id']));
390
      $form_state->setValueForElement($target_id_element, $values);
391
      NestedArray::setValue($form_state->getUserInput(), $target_id_element['#parents'], $values);
392
393
      // Rebuild form.
394
      $form_state->setRebuild();
395
    }
396
  }
397
398
  /**
399
   * Builds the render array for displaying the current results.
400
   *
401
   * @param string $details_id
402
   *   The ID for the details element.
403
   * @param string[] $field_parents
404
   *   Field parents.
405
   * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
406
   *
407
   * @return array
408
   *   The render array for the current selection.
409
   */
410
  protected function displayCurrentSelection($details_id, $field_parents, $entities) {
411
412
    $field_widget_display = $this->fieldDisplayManager->createInstance(
413
      $this->getSetting('field_widget_display'),
414
      $this->getSetting('field_widget_display_settings') + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
415
    );
416
417
    return [
418
      '#theme_wrappers' => ['container'],
419
      '#attributes' => ['class' => ['entities-list']],
420
      'items' => array_map(
421
        function (ContentEntityInterface $entity) use ($field_widget_display, $details_id, $field_parents) {
422
          $display = $field_widget_display->view($entity);
423
          if (is_string($display)) {
424
            $display = ['#markup' => $display];
425
          }
426
          return [
427
            '#theme_wrappers' => ['container'],
428
            '#attributes' => [
429
              'class' => ['item-container'],
430
              'data-entity-id' => $entity->id()
431
            ],
432
            'display' => $display,
433
            'remove_button' => [
434
              '#type' => 'submit',
435
              '#value' => $this->t('Remove'),
436
              '#ajax' => [
437
                'callback' => [get_class($this), 'updateWidgetCallback'],
438
                'wrapper' => $details_id,
439
              ],
440
              '#submit' => [[get_class($this), 'removeItemSubmit']],
441
              '#name' => $this->fieldDefinition->getName() . '_remove_' . $entity->id(),
442
              '#limit_validation_errors' => [array_merge($field_parents, [$this->fieldDefinition->getName()])],
443
              '#attributes' => ['data-entity-id' => $entity->id()],
444
              '#access' => (bool) $this->getSetting('field_widget_remove')
445
            ],
446
            'edit_button' => [
447
              '#type' => 'submit',
448
              '#value' => $this->t('Edit'),
449
              '#ajax' => [
450
                'url' => Url::fromRoute(
451
                  'entity_browser.edit_form', [
452
                  'entity_type' => $entity->getEntityTypeId(),
453
                  'entity' => $entity->id()
454
                ]
455
                )
456
              ],
457
              '#access' => (bool) $this->getSetting('field_widget_edit')
458
            ]
459
          ];
460
        },
461
        $entities
462
      ),
463
    ];
464
  }
465
}
466