Completed
Pull Request — 8.x-1.x (#126)
by Janez
02:06
created

Modal::buildConfigurationForm()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 24
rs 8.9713
c 1
b 0
f 0
cc 1
eloc 17
nc 1
nop 2
1
<?php
2
3
/**
4
 * Contains \Drupal\entity_browser\Plugin\EntityBrowser\Display\Modal.
5
 */
6
7
namespace Drupal\entity_browser\Plugin\EntityBrowser\Display;
8
9
use Drupal\Component\Utility\NestedArray;
10
use Drupal\Component\Uuid\UuidInterface;
11
use Drupal\Core\Ajax\OpenModalDialogCommand;
12
use Drupal\Core\Entity\EntityInterface;
13
use Drupal\Core\Routing\RouteMatchInterface;
14
use Drupal\Core\Url;
15
use Drupal\entity_browser\DisplayAjaxInterface;
16
use Drupal\entity_browser\DisplayBase;
17
use Drupal\entity_browser\DisplayRouterInterface;
18
use Drupal\entity_browser\Events\Events;
19
use Drupal\entity_browser\Events\RegisterJSCallbacks;
20
use Symfony\Component\DependencyInjection\ContainerInterface;
21
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
22
use Drupal\Core\Path\CurrentPathStack;
23
use Drupal\Core\Ajax\AjaxResponse;
24
use Drupal\Core\Ajax\CloseDialogCommand;
25
use Drupal\entity_browser\Ajax\SelectEntitiesCommand;
26
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
27
use Symfony\Component\HttpKernel\KernelEvents;
28
use Drupal\Core\Form\FormStateInterface;
29
use Symfony\Component\HttpFoundation\Request;
30
use Symfony\Component\HttpFoundation\Response;
31
32
/**
33
 * Presents entity browser in an Modal.
34
 *
35
 * @EntityBrowserDisplay(
36
 *   id = "modal",
37
 *   label = @Translation("Modal"),
38
 *   description = @Translation("Displays entity browser in a Modal."),
39
 *   uses_route = TRUE
40
 * )
41
 */
42
class Modal extends DisplayBase implements DisplayRouterInterface {
43
44
  /**
45
   * Current route match service.
46
   *
47
   * @var \Drupal\Core\Routing\RouteMatchInterface
48
   */
49
  protected $currentRouteMatch;
50
51
  /**
52
   * UUID generator interface.
53
   *
54
   * @var \Drupal\Component\Uuid\UuidInterface
55
   */
56
  protected $uuidGenerator;
57
58
  /**
59
   * Current path.
60
   *
61
   * @var \Drupal\Core\Path\CurrentPathStack
62
   */
63
  protected $currentPath;
64
65
  /**
66
   * UIID string.
67
   *
68
   * @var string
69
   */
70
  protected $uuid = NULL;
71
72
 /**
73
   * Current request.
74
   *
75
   * @var \Symfony\Component\HttpFoundation\Request
76
   */
77
  protected $request;
78
79
  /**
80
   * Constructs display plugin.
81
   *
82
   * @param array $configuration
83
   *   A configuration array containing information about the plugin instance.
84
   * @param string $plugin_id
85
   *   The plugin_id for the plugin instance.
86
   * @param mixed $plugin_definition
87
   *   The plugin implementation definition.
88
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
89
   *   Event dispatcher service.
90
   * @param \Drupal\Core\Routing\RouteMatchInterface
91
   *   The currently active route match object.
92
   * @param \Drupal\Component\Uuid\UuidInterface
93
   *   UUID generator interface.
94
   */
95 View Code Duplication
  public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, RouteMatchInterface $current_route_match, UuidInterface $uuid, 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...
96
    parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher);
97
    $this->currentRouteMatch = $current_route_match;
98
    $this->uuidGenerator = $uuid;
99
    $this->currentPath = $current_path;
100
    $this->request = $request;
101
  }
102
103
  /**
104
   * {@inheritdoc}
105
   */
106 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...
107
    return new static(
108
      $configuration,
109
      $plugin_id,
110
      $plugin_definition,
111
      $container->get('event_dispatcher'),
112
      $container->get('current_route_match'),
113
      $container->get('uuid'),
114
      $container->get('path.current'),
115
      $container->get('request_stack')->getCurrentRequest()
116
    );
117
  }
118
119
  /**
120
   * {@inheritdoc}
121
   */
122 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...
123
    return array(
124
      'width' => '650',
125
      'height' => '500',
126
      'link_text' => t('Select entities'),
127
    ) + parent::defaultConfiguration();
128
  }
129
130
  /**
131
   * {@inheritdoc}
132
   */
133
  public function displayEntityBrowser() {
134
    $uuid = $this->getUuid();
135
    /** @var \Drupal\entity_browser\Events\RegisterJSCallbacks $event */
136
    // TODO - $uuid is unused in this event but we need to pass it as
137
    // constructor expects it. See https://www.drupal.org/node/2600706 for more
138
    // info.
139
    $event_object = new RegisterJSCallbacks($this->configuration['entity_browser_id'], $uuid);
140
    $event_object->registerCallback('Drupal.entityBrowser.selectionCompleted');
141
    $event = $this->eventDispatcher->dispatch(Events::REGISTER_JS_CALLBACKS, $event_object );
142
    $original_path = $this->currentPath->getPath();
143
    return [
144
      '#theme_wrappers' => ['container'],
145
      'path' => [
146
        '#type' => 'hidden',
147
        '#value' => Url::fromRoute('entity_browser.' . $this->configuration['entity_browser_id'], [], [
148
          'query' => [
149
            'uuid' => $uuid,
150
            'original_path' => $original_path,
151
          ],
152
        ])->toString(),
153
      ],
154
      'open_modal' => [
155
        '#type' => 'submit',
156
        '#value' => $this->configuration['link_text'],
157
        '#limit_validation_errors' => [],
158
        '#submit' => [],
159
        '#name' => 'op_' . $this->configuration['entity_browser_id'],
160
        '#ajax' => [
161
          'callback' => [$this, 'openModal'],
162
          'event' => 'click',
163
        ],
164
        '#attributes' => [
165
          'data-uuid' => $uuid,
166
        ],
167
        '#attached' => [
168
          'library' => ['core/drupal.dialog.ajax',  'entity_browser/modal'],
169
          'drupalSettings' => [
170
            'entity_browser' => [
171
              'modal' => [
172
                $uuid => [
173
                  'uuid' => $uuid,
174
                  'js_callbacks' => $event->getCallbacks(),
175
                  'original_path' => $original_path,
176
                ],
177
              ],
178
            ],
179
          ],
180
        ],
181
      ],
182
    ];
183
  }
184
185
  /**
186
   * Generates the content and opens the modal.
187
   *
188
   * @param array $form
189
   *   The form array.
190
   * @param \Drupal\Core\Form\FormStateInterface $form_state
191
   *   The form state object.
192
   *
193
   * @return \Drupal\Core\Ajax\AjaxResponse
194
   *   An ajax response.
195
   */
196
  public function openModal(array &$form, FormStateInterface $form_state) {
197
    $triggering_element = $form_state->getTriggeringElement();
198
    $parents = $triggering_element['#parents'];
199
    array_pop($parents);
200
    $parents = array_merge($parents, ['path']);
201
    $input = $form_state->getUserInput();
202
    $src = NestedArray::getValue($input, $parents);
203
204
    $content = [
205
      '#type' => 'html_tag',
206
      '#tag' => 'iframe',
207
      '#attributes' => [
208
        'src' => $src,
209
        'width' => '100%',
210
        'height' => $this->configuration['height'] - 90,
211
        'frameborder' => 0,
212
        'style' => 'padding:'
213
      ],
214
    ];
215
    $html = drupal_render($content);
216
217
    $response = new AjaxResponse();
218
    $response->addCommand(new OpenModalDialogCommand($this->configuration['link_text'], $html, [
219
      'width' => $this->configuration['width'],
220
      'height' => $this->configuration['height'],
221
    ]));
222
    return $response;
223
  }
224
225
  /**
226
   * {@inheritdoc}
227
   */
228
  public function selectionCompleted(array $entities) {
229
    $this->entities = $entities;
230
    $this->eventDispatcher->addListener(KernelEvents::RESPONSE, [$this, 'propagateSelection']);
231
  }
232
233
  /**
234
   * {@inheritdoc}
235
   */
236
  public function addAjax(array &$form) {
237
    // Set a wrapper container to replace the form on ajax callback.
238
    $form['#prefix'] = '<div id="entity-browser-form">';
239
    $form['#suffix'] = '</div>';
240
241
    // Add the browser id to use in the FormAjaxController.
242
    $form['browser_id'] = array(
243
      '#type' => 'hidden',
244
      '#value' => $this->configuration['entity_browser_id'],
245
    );
246
247
    $form['actions']['submit']['#ajax'] = array(
248
      'callback' => array($this, 'widgetAjaxCallback'),
249
      'wrapper' => 'entity-browser-form',
250
    );
251
  }
252
253
  /**
254
   * Ajax callback for entity browser form.
255
   *
256
   * Allows the entity browser form to submit the form via ajax.
257
   *
258
   * @param array $form
259
   *   The form array.
260
   * @param FormStateInterface $form_state
261
   *   The form state object.
262
   *
263
   * @return \Drupal\Core\Ajax\AjaxResponse
264
   *   Response.
265
   */
266
  public function widgetAjaxCallback(array &$form, FormStateInterface $form_state) {
267
    // If we've got any validation error, print out the form again.
268
    if ($form_state->hasAnyErrors()) {
269
      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...
270
    }
271
272
    $commands = $this->getAjaxCommands($form_state);
273
    $response = new AjaxResponse();
274
    foreach ($commands as $command) {
275
      $response->addCommand($command);
276
    }
277
278
    return $response;
279
  }
280
281
  /**
282
   * Helper function to return commands to return in AjaxResponse.
283
   *
284
   * @return array
285
   *   An array of ajax commands.
286
   */
287
  public function getAjaxCommands(FormStateInterface $form_state) {
288
    $entities = array_map(function(EntityInterface $item) {return [$item->id(), $item->uuid(), $item->getEntityTypeId()];}, $form_state->get(['entity_browser', 'selected_entities']));
289
290
    $commands = array();
291
    $commands[] = new SelectEntitiesCommand($this->uuid, $entities);
292
293
    return $commands;
294
  }
295
296
  /**
297
   * KernelEvents::RESPONSE listener.
298
   *
299
   * Intercepts default response and injects
300
   * response that will trigger JS to propagate selected entities upstream.
301
   *
302
   * @param FilterResponseEvent $event
303
   *   Response event.
304
   */
305 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...
306
    $render = [
307
      'labels' => [
308
        '#markup' => 'Labels: ' . implode(', ', array_map(function (EntityInterface $item) {return $item->label();}, $this->entities)),
309
        '#attached' => [
310
          'library' => ['entity_browser/modal_selection'],
311
          'drupalSettings' => [
312
            'entity_browser' => [
313
              'modal' => [
314
                'entities' => array_map(function (EntityInterface $item) {return [$item->id(), $item->uuid(), $item->getEntityTypeId()];}, $this->entities),
315
                'uuid' => $this->request->query->get('uuid'),
316
              ],
317
            ],
318
          ],
319
        ],
320
      ],
321
    ];
322
323
    $event->setResponse(new Response(\Drupal::service('bare_html_page_renderer')->renderBarePage($render, 'Entity browser', 'entity_browser_propagation')));
324
  }
325
326
  /**
327
   * {@inheritdoc}
328
   */
329
  public function path() {
330
    return '/entity-browser/modal/' . $this->configuration['entity_browser_id'];
331
  }
332
333
  /**
334
   * {@inheritdoc}
335
   */
336
  public function getUuid() {
337
    if (empty($this->uuid)) {
338
      $this->uuid = $this->uuidGenerator->generate();
339
    }
340
    return $this->uuid;
341
  }
342
343
  public function __sleep() {
344
    return ['configuration'];
345
  }
346
347
  /**
348
   * {@inheritdoc}
349
   */
350
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
351
    $configuration = $this->getConfiguration();
352
    $form['width'] = [
353
      '#type' => 'number',
354
      '#title' => $this->t('Width of the modal'),
355
      '#min' => 1,
356
      '#default_value' => $configuration['width'],
357
    ];
358
359
    $form['height'] = [
360
      '#type' => 'number',
361
      '#title' => $this->t('Height of the modal'),
362
      '#min' => 1,
363
      '#default_value' => $configuration['height'],
364
    ];
365
366
    $form['link_text'] = [
367
      '#type' => 'textfield',
368
      '#title' => $this->t('Link text'),
369
      '#default_value' => $configuration['link_text'],
370
    ];
371
372
    return $form;
373
  }
374
375
}
376