Completed
Push — 8.x-1.x ( a0d335...75249f )
by Janez
27:14
created

EntityReference   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 429
Duplicated Lines 3.26 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 27
Bugs 2 Features 4
Metric Value
wmc 42
c 27
b 2
f 4
lcom 1
cbo 2
dl 14
loc 429
rs 8.295

11 Methods

Rating   Name   Duplication   Size   Complexity  
A updateSettingsAjax() 0 3 1
A __construct() 0 5 1
A create() 0 12 1
A defaultSettings() 0 10 1
B settingsForm() 0 81 5
B settingsSummary() 0 24 4
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
      if ($browser = $this->entityManager->getStorage('entity_browser')->load($entity_browser_id)) {
226
        $summary[] = t('Entity browser: @browser', ['@browser' => $browser->label()]);
227
      } else {
228
        drupal_set_message(t('Missing entity browser!'), 'error');
229
        return [t('Missing entity browser!')];
230
      }
231
    }
232
233
    if (!empty($field_widget_display)) {
234
      $plugin = $this->fieldDisplayManager->getDefinition($field_widget_display);
235
      $summary[] = t('Entity display: @name', ['@name' => $plugin['label']]);
236
    }
237
238
    return $summary;
239
  }
240
241
  /**
242
   * {@inheritdoc}
243
   */
244
  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...
245
    $entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
246
    $entity_storage = $this->entityManager->getStorage($entity_type);
247
248
    $ids = [];
249
    $entities = [];
250
251
    // Determine if we're submitting and if submit came from this widget.
252
    $is_relevant_submit = FALSE;
253
    if (($trigger = $form_state->getTriggeringElement())) {
254
      // Can be triggered by hidden target_id element or "Remove" button.
255
      if (end($trigger['#parents']) === 'target_id' || (end($trigger['#parents']) === 'remove_button')) {
256
        $is_relevant_submit = TRUE;
257
258
        // In case there are more instances of this widget on the same page we
259
        // need to check if submit came from this instance.
260
        $field_name_key = end($trigger['#parents']) === 'target_id' ? 2 : static::$deleteDepth + 1;
261
        $field_name_key = sizeof($trigger['#parents']) - $field_name_key;
262
        $is_relevant_submit &= ($trigger['#parents'][$field_name_key] === $this->fieldDefinition->getName());
263
      }
264
    };
265
266
    if ($is_relevant_submit) {
267
      // Submit was triggered by hidden "target_id" element when entities were
268
      // added via entity browser.
269 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...
270
        $parents = $trigger['#parents'];
271
      }
272
      // Submit was triggered by one of the "Remove" buttons. We need to walk
273
      // few levels up to read value of "target_id" element.
274
      elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], $this->fieldDefinition->getName() . '_remove_') === 0) {
275
        $parents = array_merge(array_slice($trigger['#parents'], 0, -static::$deleteDepth), ['target_id']);
276
      }
277
278
      if (isset($parents) && $value = $form_state->getValue($parents)) {
279
        $ids = explode(' ', $value);
280
        $entities = $entity_storage->loadMultiple($ids);
281
      }
282
    }
283
    // We are loading for for the first time so we need to load any existing
284
    // values that might already exist on the entity. Also, remove any leftover
285
    // data from removed entity references.
286
    else {
287
      foreach ($items as $item) {
288
        $entity = $entity_storage->load($item->target_id);
289
        if (!empty($entity)) {
290
          $entities[$item->target_id] = $entity;
291
        }
292
      }
293
      $ids = array_keys($entities);
294
    }
295
    $ids = array_filter($ids);
296
297
    $hidden_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName() . '-target-id');
298
    $details_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName());
299
    /** @var \Drupal\entity_browser\EntityBrowserInterface $entity_browser */
300
    $entity_browser = $this->entityManager->getStorage('entity_browser')->load($this->getSetting('entity_browser'));
301
302
    $element += [
303
      '#id' => $details_id,
304
      '#type' => 'details',
305
      '#open' => !empty($ids) || $this->getSetting('open'),
306
      'target_id' => [
307
        '#type' => 'hidden',
308
        '#id' => $hidden_id,
309
        // We need to repeat ID here as it is otherwise skipped when rendering.
310
        '#attributes' => ['id' => $hidden_id],
311
        '#default_value' => $ids,
312
        // #ajax is officially not supported for hidden elements but if we
313
        // specify event manually it works.
314
        '#ajax' => [
315
          'callback' => [get_class($this), 'updateWidgetCallback'],
316
          'wrapper' => $details_id,
317
          'event' => 'entity_browser_value_updated',
318
        ],
319
      ],
320
    ];
321
322
    $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
323
    if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($ids) < $cardinality) {
324
      $element['entity_browser'] = $entity_browser->getDisplay()->displayEntityBrowser();
325
      $element['#attached']['library'][] = 'entity_browser/entity_reference';
326
      $element['#attached']['drupalSettings']['entity_browser'] = [
327
        $entity_browser->getDisplay()->getUuid() => [
328
          'cardinality' => $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(),
329
          'selector' => '#'.$element['target_id']['#attributes']['id'],
330
        ]
331
      ];
332
    }
333
334
    $field_parents = $element['#field_parents'];
335
336
    $element['current'] = $this->displayCurrentSelection($details_id, $field_parents, $entities);
337
338
    return $element;
339
  }
340
341
  /**
342
   * {@inheritdoc}
343
   */
344
  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
345
    $ids = empty($values['target_id']) ? [] : explode(' ', trim($values['target_id']));
346
    $return = [];
347
    foreach ($ids as $id) {
348
      $return[]['target_id'] = $id;
349
    }
350
351
    return $return;
352
  }
353
354
  /**
355
   * AJAX form callback.
356
   */
357
  public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
358
    $trigger = $form_state->getTriggeringElement();
359
    // AJAX requests can be triggered by hidden "target_id" element when entities
360
    // are added or by one of the "Remove" buttons. Depending on that we need to
361
    // figure out where root of the widget is in the form structure and use this
362
    // information to return correct part of the form.
363 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...
364
      $parents = array_slice($trigger['#array_parents'], 0, -1);
365
    }
366
    elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_remove_')) {
367
      $parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth);
368
    }
369
370
    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...
371
  }
372
373
  /**
374
   * Submit callback for remove buttons.
375
   */
376
  public static function removeItemSubmit(&$form, FormStateInterface $form_state) {
377
    $triggering_element = $form_state->getTriggeringElement();
378
    if (!empty($triggering_element['#attributes']['data-entity-id'])) {
379
      $id = $triggering_element['#attributes']['data-entity-id'];
380
      $parents = array_slice($triggering_element['#parents'], 0, -static::$deleteDepth);
381
      $array_parents = array_slice($triggering_element['#array_parents'], 0, -static::$deleteDepth);
382
383
      // Find and remove correct entity.
384
      $values = explode(' ', $form_state->getValue(array_merge($parents, ['target_id'])));
385
      $values = array_filter(
386
        $values,
387
        function($item) use ($id) { return $item != $id; }
388
      );
389
      $values = implode(' ', $values);
390
391
      // Set new value for this widget.
392
      $target_id_element = &NestedArray::getValue($form, array_merge($array_parents, ['target_id']));
393
      $form_state->setValueForElement($target_id_element, $values);
394
      NestedArray::setValue($form_state->getUserInput(), $target_id_element['#parents'], $values);
395
396
      // Rebuild form.
397
      $form_state->setRebuild();
398
    }
399
  }
400
401
  /**
402
   * Builds the render array for displaying the current results.
403
   *
404
   * @param string $details_id
405
   *   The ID for the details element.
406
   * @param string[] $field_parents
407
   *   Field parents.
408
   * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
409
   *
410
   * @return array
411
   *   The render array for the current selection.
412
   */
413
  protected function displayCurrentSelection($details_id, $field_parents, $entities) {
414
415
    $field_widget_display = $this->fieldDisplayManager->createInstance(
416
      $this->getSetting('field_widget_display'),
417
      $this->getSetting('field_widget_display_settings') + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
418
    );
419
420
    return [
421
      '#theme_wrappers' => ['container'],
422
      '#attributes' => ['class' => ['entities-list']],
423
      'items' => array_map(
424
        function (ContentEntityInterface $entity) use ($field_widget_display, $details_id, $field_parents) {
425
          $display = $field_widget_display->view($entity);
426
          if (is_string($display)) {
427
            $display = ['#markup' => $display];
428
          }
429
          return [
430
            '#theme_wrappers' => ['container'],
431
            '#attributes' => [
432
              'class' => ['item-container', Html::getClass($field_widget_display->getPluginId())],
433
              'data-entity-id' => $entity->id()
434
            ],
435
            'display' => $display,
436
            'remove_button' => [
437
              '#type' => 'submit',
438
              '#value' => $this->t('Remove'),
439
              '#ajax' => [
440
                'callback' => [get_class($this), 'updateWidgetCallback'],
441
                'wrapper' => $details_id,
442
              ],
443
              '#submit' => [[get_class($this), 'removeItemSubmit']],
444
              '#name' => $this->fieldDefinition->getName() . '_remove_' . $entity->id(),
445
              '#limit_validation_errors' => [array_merge($field_parents, [$this->fieldDefinition->getName()])],
446
              '#attributes' => ['data-entity-id' => $entity->id()],
447
              '#access' => (bool) $this->getSetting('field_widget_remove')
448
            ],
449
            'edit_button' => [
450
              '#type' => 'submit',
451
              '#value' => $this->t('Edit'),
452
              '#ajax' => [
453
                'url' => Url::fromRoute(
454
                  'entity_browser.edit_form', [
455
                  'entity_type' => $entity->getEntityTypeId(),
456
                  'entity' => $entity->id()
457
                ]
458
                )
459
              ],
460
              '#access' => (bool) $this->getSetting('field_widget_edit')
461
            ]
462
          ];
463
        },
464
        $entities
465
      ),
466
    ];
467
  }
468
}
469