Completed
Push — 8.x-1.x ( da8d51...96c705 )
by Janez
07:38 queued 03:01
created

Modal::propagateSelection()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 16

Duplication

Lines 20
Ratio 83.33 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 16
c 3
b 0
f 0
nc 1
nop 1
dl 20
loc 24
rs 8.9713
1
<?php
2
3
namespace Drupal\entity_browser\Plugin\EntityBrowser\Display;
4
5
use Drupal\Component\Utility\Html;
6
use Drupal\Component\Utility\NestedArray;
7
use Drupal\Component\Uuid\UuidInterface;
8
use Drupal\Core\Ajax\OpenDialogCommand;
9
use Drupal\Core\Entity\EntityInterface;
10
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
11
use Drupal\Core\Routing\RouteMatchInterface;
12
use Drupal\Core\Url;
13
use Drupal\entity_browser\DisplayBase;
14
use Drupal\entity_browser\DisplayRouterInterface;
15
use Drupal\entity_browser\Events\Events;
16
use Drupal\entity_browser\Events\RegisterJSCallbacks;
17
use Symfony\Component\DependencyInjection\ContainerInterface;
18
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
19
use Drupal\Core\Path\CurrentPathStack;
20
use Drupal\Core\Ajax\AjaxResponse;
21
use Drupal\entity_browser\Ajax\SelectEntitiesCommand;
22
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
23
use Drupal\Core\Form\FormStateInterface;
24
use Symfony\Component\HttpFoundation\Request;
25
use Symfony\Component\HttpFoundation\Response;
26
use Drupal\entity_browser\Events\AlterEntityBrowserDisplayData;
27
28
/**
29
 * Presents entity browser in an Modal.
30
 *
31
 * @EntityBrowserDisplay(
32
 *   id = "modal",
33
 *   label = @Translation("Modal"),
34
 *   description = @Translation("Displays entity browser in a Modal."),
35
 *   uses_route = TRUE
36
 * )
37
 */
38
class Modal extends DisplayBase implements DisplayRouterInterface {
39
40
  /**
41
   * Current route match service.
42
   *
43
   * @var \Drupal\Core\Routing\RouteMatchInterface
44
   */
45
  protected $currentRouteMatch;
46
47
  /**
48
   * Current path.
49
   *
50
   * @var \Drupal\Core\Path\CurrentPathStack
51
   */
52
  protected $currentPath;
53
54
  /**
55
   * Current request.
56
   *
57
   * @var \Symfony\Component\HttpFoundation\Request
58
   */
59
  protected $request;
60
61
  /**
62
   * Constructs display plugin.
63
   *
64
   * @param array $configuration
65
   *   A configuration array containing information about the plugin instance.
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 \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
71
   *   Event dispatcher service.
72
   * @param \Drupal\Component\Uuid\UuidInterface $uuid
73
   *   UUID generator interface.
74
   * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $selection_storage
75
   *   The selection storage.
76
   * @param \Drupal\Core\Routing\RouteMatchInterface $current_route_match
77
   *   The currently active route match object.
78
   * @param \Drupal\Core\Path\CurrentPathStack $current_path
79
   *   The current path.
80
   * @param \Symfony\Component\HttpFoundation\Request $request
81
   *   Current request.
82
   */
83 View Code Duplication
  public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid, KeyValueStoreExpirableInterface $selection_storage, RouteMatchInterface $current_route_match, CurrentPathStack $current_path, Request $request) {
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...
84
    parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $uuid, $selection_storage);
85
    $this->currentRouteMatch = $current_route_match;
86
    $this->currentPath = $current_path;
87
    $this->request = $request;
88
  }
89
90
  /**
91
   * {@inheritdoc}
92
   */
93 View Code Duplication
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
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
    return new static(
95
      $configuration,
96
      $plugin_id,
97
      $plugin_definition,
98
      $container->get('event_dispatcher'),
99
      $container->get('uuid'),
100
      $container->get('entity_browser.selection_storage'),
101
      $container->get('current_route_match'),
102
      $container->get('path.current'),
103
      $container->get('request_stack')->getCurrentRequest()
104
    );
105
  }
106
107
  /**
108
   * {@inheritdoc}
109
   */
110 View Code Duplication
  public function defaultConfiguration() {
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...
111
    return [
112
      'width' => '650',
113
      'height' => '500',
114
      'link_text' => t('Select entities'),
115
    ] + parent::defaultConfiguration();
116
  }
117
118
  /**
119
   * {@inheritdoc}
120
   */
121
  public function displayEntityBrowser(array $element, FormStateInterface $form_state, array &$complete_form, array $persistent_data = []) {
122
    parent::displayEntityBrowser($element, $form_state, $complete_form, $persistent_data);
123
    $js_event_object = new RegisterJSCallbacks($this->configuration['entity_browser_id'], $this->getUuid());
124
    $js_event_object->registerCallback('Drupal.entityBrowser.selectionCompleted');
125
    $js_event = $this->eventDispatcher->dispatch(Events::REGISTER_JS_CALLBACKS, $js_event_object);
126
    $original_path = $this->currentPath->getPath();
127
128
    $data = [
129
      'query_parameters' => [
130
        'query' => [
131
          'uuid' => $this->getUuid(),
132
          'original_path' => $original_path,
133
        ],
134
      ],
135
      'attributes' => [
136
        'data-uuid' => $this->getUuid(),
137
      ],
138
    ];
139
    $event_object = new AlterEntityBrowserDisplayData($this->configuration['entity_browser_id'], $this->getUuid(), $this->getPluginDefinition(), $form_state, $data);
140
    $event = $this->eventDispatcher->dispatch(Events::ALTER_BROWSER_DISPLAY_DATA, $event_object);
141
    $data = $event->getData();
142
    return [
143
      '#theme_wrappers' => ['container'],
144
      'path' => [
145
        '#type' => 'hidden',
146
        '#value' => Url::fromRoute('entity_browser.' . $this->configuration['entity_browser_id'], [], $data['query_parameters'])->toString(),
147
      ],
148
      'open_modal' => [
149
        '#type' => 'submit',
150
        '#value' => $this->configuration['link_text'],
151
        '#limit_validation_errors' => [],
152
        '#submit' => [],
153
        '#name' => implode('_', $element['#eb_parents']),
154
        '#ajax' => [
155
          'callback' => [$this, 'openModal'],
156
          'event' => 'click',
157
        ],
158
        '#executes_submit_callback' => FALSE,
159
        '#attributes' => $data['attributes'],
160
        '#attached' => [
161
          'library' => ['core/drupal.dialog.ajax', 'entity_browser/modal'],
162
          'drupalSettings' => [
163
            'entity_browser' => [
164
              'modal' => [
165
                $this->getUuid() => [
166
                  'uuid' => $this->getUuid(),
167
                  'js_callbacks' => $js_event->getCallbacks(),
168
                  'original_path' => $original_path,
169
                ],
170
              ],
171
            ],
172
          ],
173
        ],
174
      ],
175
    ];
176
  }
177
178
  /**
179
   * Generates the content and opens the modal.
180
   *
181
   * @param array $form
182
   *   The form array.
183
   * @param \Drupal\Core\Form\FormStateInterface $form_state
184
   *   The form state object.
185
   *
186
   * @return \Drupal\Core\Ajax\AjaxResponse
187
   *   An ajax response.
188
   */
189
  public function openModal(array &$form, FormStateInterface $form_state) {
190
    $triggering_element = $form_state->getTriggeringElement();
191
    $parents = $triggering_element['#parents'];
192
    array_pop($parents);
193
    $parents = array_merge($parents, ['path']);
194
    $input = $form_state->getUserInput();
195
    $src = NestedArray::getValue($input, $parents);
196
197
    $field_name = $triggering_element['#parents'][0];
198
    $element_name = $this->configuration['entity_browser_id'];
199
    $content = [
200
      '#type' => 'html_tag',
201
      '#tag' => 'iframe',
202
      '#attributes' => [
203
        'src' => $src,
204
        'class' => 'entity-browser-modal-iframe',
205
        'width' => '100%',
206
        'height' => $this->configuration['height'] - 90,
207
        'frameborder' => 0,
208
        'style' => 'padding:0',
209
        'name' => 'entity_browser_iframe_' . $element_name,
210
      ],
211
    ];
212
    $html = drupal_render($content);
213
214
    $response = new AjaxResponse();
215
    $response->addCommand(new OpenDialogCommand('#' . Html::getUniqueId($field_name . '-' . $element_name . '-dialog'), $this->configuration['link_text'], $html, [
216
      'width' => 'auto',
217
      'height' => 'auto',
218
      'modal' => TRUE,
219
      'maxWidth' => $this->configuration['width'],
220
      'maxHeight' => $this->configuration['height'],
221
      'fluid' => 1,
222
      'autoResize' => 0,
223
      'resizable' => 0,
224
    ]));
225
    return $response;
226
  }
227
228
  /**
229
   * {@inheritdoc}
230
   */
231
  public function addAjax(array &$form) {
232
    // Set a wrapper container to replace the form on ajax callback.
233
    $form['#prefix'] = '<div id="entity-browser-form">';
234
    $form['#suffix'] = '</div>';
235
236
    // Add the browser id to use in the FormAjaxController.
237
    $form['browser_id'] = [
238
      '#type' => 'hidden',
239
      '#value' => $this->configuration['entity_browser_id'],
240
    ];
241
242
    $form['actions']['submit']['#ajax'] = [
243
      'callback' => [$this, 'widgetAjaxCallback'],
244
      'wrapper' => 'entity-browser-form',
245
    ];
246
  }
247
248
  /**
249
   * Ajax callback for entity browser form.
250
   *
251
   * Allows the entity browser form to submit the form via ajax.
252
   *
253
   * @param array $form
254
   *   The form array.
255
   * @param FormStateInterface $form_state
256
   *   The form state object.
257
   *
258
   * @return \Drupal\Core\Ajax\AjaxResponse
259
   *   Response.
260
   */
261
  public function widgetAjaxCallback(array &$form, FormStateInterface $form_state) {
262
    // If we've got any validation error, print out the form again.
263
    if ($form_state->hasAnyErrors()) {
264
      return $form;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $form; (array) is incompatible with the return type documented by Drupal\entity_browser\Pl...dal::widgetAjaxCallback of type Drupal\Core\Ajax\AjaxResponse.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
265
    }
266
267
    $commands = $this->getAjaxCommands($form_state);
268
    $response = new AjaxResponse();
269
    foreach ($commands as $command) {
270
      $response->addCommand($command);
271
    }
272
273
    return $response;
274
  }
275
276
  /**
277
   * Helper function to return commands to return in AjaxResponse.
278
   *
279
   * @return array
280
   *   An array of ajax commands.
281
   */
282
  public function getAjaxCommands(FormStateInterface $form_state) {
283
    $entities = array_map(function(EntityInterface $item) {
284
      return [$item->id(), $item->uuid(), $item->getEntityTypeId()];
285
    }, $form_state->get(['entity_browser', 'selected_entities']));
286
287
    $commands = [];
288
    $commands[] = new SelectEntitiesCommand($this->uuid, $entities);
289
290
    return $commands;
291
  }
292
293
  /**
294
   * KernelEvents::RESPONSE listener.
295
   *
296
   * Intercepts default response and injects
297
   * response that will trigger JS to propagate selected entities upstream.
298
   *
299
   * @param FilterResponseEvent $event
300
   *   Response event.
301
   */
302 View Code Duplication
  public function propagateSelection(FilterResponseEvent $event) {
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...
303
    $render = [
304
      'labels' => [
305
        '#markup' => 'Labels: ' . implode(', ', array_map(function (EntityInterface $item) {
306
          return $item->label();
307
        }, $this->entities)),
308
        '#attached' => [
309
          'library' => ['entity_browser/modal_selection'],
310
          'drupalSettings' => [
311
            'entity_browser' => [
312
              'modal' => [
313
                'entities' => array_map(function (EntityInterface $item) {
314
                  return [$item->id(), $item->uuid(), $item->getEntityTypeId()];
315
                }, $this->entities),
316
                'uuid' => $this->request->query->get('uuid'),
317
              ],
318
            ],
319
          ],
320
        ],
321
      ],
322
    ];
323
324
    $event->setResponse(new Response(\Drupal::service('bare_html_page_renderer')->renderBarePage($render, 'Entity browser', 'page')));
325
  }
326
327
  /**
328
   * {@inheritdoc}
329
   */
330
  public function path() {
331
    return '/entity-browser/modal/' . $this->configuration['entity_browser_id'];
332
  }
333
334
  /**
335
   * {@inheritdoc}
336
   */
337
  public function __sleep() {
338
    return ['configuration'];
339
  }
340
341
  /**
342
   * {@inheritdoc}
343
   */
344
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
345
    $configuration = $this->getConfiguration();
346
    $form['width'] = [
347
      '#type' => 'number',
348
      '#title' => $this->t('Width of the modal'),
349
      '#default_value' => $configuration['width'],
350
      '#description' => t('Empty value for responsive width.'),
351
    ];
352
    $form['height'] = [
353
      '#type' => 'number',
354
      '#title' => $this->t('Height of the modal'),
355
      '#default_value' => $configuration['height'],
356
      '#description' => t('Empty value for responsive height.'),
357
    ];
358
    $form['link_text'] = [
359
      '#type' => 'textfield',
360
      '#title' => $this->t('Link text'),
361
      '#default_value' => $configuration['link_text'],
362
    ];
363
    return $form;
364
  }
365
366
}
367