DefaultFacetManager.php 15.3 KB
Newer Older
1
2
<?php

3
namespace Drupal\facets\FacetManager;
4

borisson_'s avatar
borisson_ committed
5
use Drupal\Core\Entity\EntityTypeManagerInterface;
borisson_'s avatar
borisson_ committed
6
use Drupal\Core\StringTranslation\StringTranslationTrait;
7
8
9
10
use Drupal\facets\Exception\InvalidProcessorException;
use Drupal\facets\FacetInterface;
use Drupal\facets\FacetSource\FacetSourcePluginManager;
use Drupal\facets\Processor\BuildProcessorInterface;
11
use Drupal\facets\Processor\PostQueryProcessorInterface;
12
13
14
15
16
use Drupal\facets\Processor\PreQueryProcessorInterface;
use Drupal\facets\Processor\ProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginManager;
use Drupal\facets\QueryType\QueryTypePluginManager;
use Drupal\facets\Widget\WidgetPluginManager;
17
18

/**
borisson_'s avatar
borisson_ committed
19
 * The facet manager.
20
 *
21
22
23
 * The manager is responsible for interactions with the Search backend, such as
 * altering the query, it is also responsible for executing and building the
 * facet. It is also responsible for running the processors.
24
 */
25
class DefaultFacetManager {
26

borisson_'s avatar
borisson_ committed
27
28
  use StringTranslationTrait;

29
  /**
30
   * The query type plugin manager.
borisson_'s avatar
borisson_ committed
31
32
33
   *
   * @var \Drupal\facets\QueryType\QueryTypePluginManager
   *   The query type plugin manager.
34
   */
borisson_'s avatar
borisson_ committed
35
  protected $queryTypePluginManager;
36

Jur de Vries's avatar
Jur de Vries committed
37
  /**
38
   * The facet source plugin manager.
Jur de Vries's avatar
Jur de Vries committed
39
   *
borisson_'s avatar
borisson_ committed
40
   * @var \Drupal\facets\FacetSource\FacetSourcePluginManager
Jur de Vries's avatar
Jur de Vries committed
41
   */
borisson_'s avatar
borisson_ committed
42
  protected $facetSourcePluginManager;
Jur de Vries's avatar
Jur de Vries committed
43

44
  /**
45
   * The processor plugin manager.
46
   *
47
   * @var \Drupal\facets\Processor\ProcessorPluginManager
48
   */
borisson_'s avatar
borisson_ committed
49
  protected $processorPluginManager;
50

51
52
53
54
55
56
57
  /**
   * The widget plugin manager.
   *
   * @var \Drupal\facets\Widget\WidgetPluginManager
   */
  protected $widgetPluginManager;

58
  /**
59
   * An array of facets that are being rendered.
60
   *
61
   * @var \Drupal\facets\FacetInterface[]
62
   *
63
64
   * @see \Drupal\facets\FacetInterface
   * @see \Drupal\facets\Entity\Facet
65
   */
borisson_'s avatar
borisson_ committed
66
  protected $facets = [];
67

68
69
  /**
   * An array of all entity ids in the active resultset which are a child.
70
71
   *
   * @var string[]
72
73
74
   */
  protected $childIds = [];

75
  /**
76
   * An array flagging which facet source' facets have been processed.
77
   *
78
79
   * This variable acts as a semaphore that ensures facet data is processed
   * only once.
80
   *
81
   * @var bool[]
82
   *
83
   * @see \Drupal\facets\FacetsFacetManager::processFacets()
84
   */
85
  protected $processedFacetSources = [];
86
87
88
89
90
91
92
93
94
95
96
97
98

  /**
   * Stores the search path associated with this searcher.
   *
   * @var string
   */
  protected $searchPath;

  /**
   * Stores settings with defaults.
   *
   * @var array
   *
99
   * @see \Drupal\facets\FacetsFacetManager::getFacetSettings()
100
   */
borisson_'s avatar
borisson_ committed
101
  protected $settings = [];
102
103

  /**
104
   * The entity storage for facets.
105
   *
106
   * @var \Drupal\Core\Entity\EntityStorageInterface|object
107
   */
108
  protected $facetStorage;
109

110
111
112
113
114
115
116
  /**
   * Prepared facets.
   *
   * @var bool
   */
  protected $preparedFacets = FALSE;

117
  /**
borisson_'s avatar
borisson_ committed
118
   * Constructs a new instance of the DefaultFacetManager.
119
   *
120
   * @param \Drupal\facets\QueryType\QueryTypePluginManager $query_type_plugin_manager
borisson_'s avatar
borisson_ committed
121
   *   The query type plugin manager.
122
   * @param \Drupal\facets\Widget\WidgetPluginManager $widget_plugin_manager
borisson_'s avatar
borisson_ committed
123
   *   The widget plugin manager.
124
   * @param \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_manager
borisson_'s avatar
borisson_ committed
125
   *   The facet source plugin manager.
126
   * @param \Drupal\facets\Processor\ProcessorPluginManager $processor_plugin_manager
borisson_'s avatar
borisson_ committed
127
   *   The processor plugin manager.
borisson_'s avatar
borisson_ committed
128
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
borisson_'s avatar
borisson_ committed
129
   *   The entity type plugin manager.
130
   */
borisson_'s avatar
borisson_ committed
131
  public function __construct(QueryTypePluginManager $query_type_plugin_manager, WidgetPluginManager $widget_plugin_manager, FacetSourcePluginManager $facet_source_manager, ProcessorPluginManager $processor_plugin_manager, EntityTypeManagerInterface $entity_type_manager) {
borisson_'s avatar
borisson_ committed
132
    $this->queryTypePluginManager = $query_type_plugin_manager;
133
    $this->widgetPluginManager = $widget_plugin_manager;
borisson_'s avatar
borisson_ committed
134
135
    $this->facetSourcePluginManager = $facet_source_manager;
    $this->processorPluginManager = $processor_plugin_manager;
136
    $this->facetStorage = $entity_type_manager->getStorage('facets_facet');
137
138
139
140
141
142
  }

  /**
   * Allows the backend to add facet queries to its native query object.
   *
   * This method is called by the implementing module to initialize the facet
143
   * display process.
144
145
146
   *
   * @param mixed $query
   *   The backend's native query object.
147
148
   * @param string $facetsource_id
   *   The facet source ID to process.
149
   */
150
  public function alterQuery(&$query, $facetsource_id) {
151
    /** @var \Drupal\facets\FacetInterface[] $facets */
152
153
154
    foreach ($this->getFacetsByFacetSourceId($facetsource_id) as $facet) {
      /** @var \Drupal\facets\QueryType\QueryTypeInterface $query_type_plugin */
      $query_type_plugin = $this->queryTypePluginManager->createInstance($facet->getQueryType(), ['query' => $query, 'facet' => $facet]);
155
      $query_type_plugin->execute();
156
157
158
159
    }
  }

  /**
160
   * Returns enabled facets for the searcher associated with this FacetManager.
161
   *
162
   * @return \Drupal\facets\FacetInterface[]
163
164
165
   *   An array of enabled facets.
   */
  public function getEnabledFacets() {
166
    return $this->facetStorage->loadMultiple();
167
168
169
  }

  /**
170
   * Returns currently rendered facets filtered by facetsource ID.
171
   *
172
173
174
175
176
   * @param string $facetsource_id
   *   The facetsource ID to filter by.
   *
   * @return \Drupal\facets\FacetInterface[]
   *   An array of enabled facets.
177
   */
178
  public function getFacetsByFacetSourceId($facetsource_id) {
179
180
    // Immediately initialize the facets.
    $this->initFacets();
181
182
183
184
185
186
187
    $facets = [];
    foreach ($this->facets as $facet) {
      if ($facet->getFacetSourceId() == $facetsource_id) {
        $facets[] = $facet;
      }
    }
    return $facets;
188
189
190
191
192
  }

  /**
   * Initializes facet builds, sets the breadcrumb trail.
   *
193
194
   * Facets are built via FacetsFacetProcessor objects. Facets only need to be
   * processed, or built, once The FacetsFacetManager::processed semaphore is
195
196
   * set when this method is called ensuring that facets are built only once
   * regardless of how many times this method is called.
197
   *
198
   * @param string|null $facetsource_id
199
   *   The facetsource if of the currently processed facet.
200
   */
201
202
203
204
205
206
207
208
  public function processFacets($facetsource_id = NULL) {
    if (empty($facetsource_id)) {
      foreach ($this->facets as $facet) {
        $current_facetsource_id = $facet->getFacetSourceId();
        $this->processFacets($current_facetsource_id);
      }
    }
    elseif (empty($this->processedFacetSources[$facetsource_id])) {
209
      // First add the results to the facets.
210
      $this->updateResults($facetsource_id);
Jur de Vries's avatar
Jur de Vries committed
211

212
      $this->processedFacetSources[$facetsource_id] = TRUE;
213
    }
214
215
216
217
218
219

    foreach ($this->facets as $facet) {
      foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_POST_QUERY) as $processor) {
        /** @var \Drupal\facets\processor\PostQueryProcessorInterface $post_query_processor */
        $post_query_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]);
        if (!$post_query_processor instanceof PostQueryProcessorInterface) {
220
          throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a post_query definition but doesn't implement the required PostQueryProcessor interface");
221
222
223
224
225
        }
        $post_query_processor->postQuery($facet);
      }
    }

226
227
  }

228
  /**
229
   * Initializes enabled facets.
230
   *
231
232
   * In this method all pre-query processors get called and their contents are
   * executed.
233
234
   */
  protected function initFacets() {
235
    if (!$this->preparedFacets && empty($this->facets)) {
236
237
      $this->facets = $this->getEnabledFacets();
      foreach ($this->facets as $facet) {
238
239
240
241
        foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_PRE_QUERY) as $processor) {
          /** @var PreQueryProcessorInterface $pre_query_processor */
          $pre_query_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]);
          if (!$pre_query_processor instanceof PreQueryProcessorInterface) {
242
            throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a pre_query definition but doesn't implement the required PreQueryProcessorInterface interface");
243
          }
244
          $pre_query_processor->preQuery($facet);
245
        }
246
      }
247
      $this->preparedFacets = TRUE;
248
249
250
    }
  }

251
  /**
252
   * Builds a facet and returns it as a renderable array.
borisson_'s avatar
borisson_ committed
253
254
255
256
257
258
259
260
261
   *
   * This method delegates to the relevant plugins to render a facet, it calls
   * out to a widget plugin to do the actual rendering when results are found.
   * When no results are found it calls out to the correct empty result plugin
   * to build a render array.
   *
   * Before doing any rendering, the processors that implement the
   * BuildProcessorInterface enabled on this facet will run.
   *
262
   * @param \Drupal\facets\FacetInterface $facet
borisson_'s avatar
borisson_ committed
263
   *   The facet we should build.
borisson_'s avatar
borisson_ committed
264
   *
borisson_'s avatar
borisson_ committed
265
266
   * @return array
   *   Facet render arrays.
borisson_'s avatar
borisson_ committed
267
   *
268
   * @throws \Drupal\facets\Exception\InvalidProcessorException
borisson_'s avatar
borisson_ committed
269
   *   Throws an exception when an invalid processor is linked to the facet.
270
271
   */
  public function build(FacetInterface $facet) {
272
273
    // Immediately initialize the facets.
    $this->initFacets();
274
275
276
277
    // It might be that the facet received here, is not the same as the already
    // loaded facets in the FacetManager.
    // For that reason, get the facet from the already loaded facets in the
    // FacetManager.
278
    $facet = $this->facets[$facet->id()];
279
    $facet_source_id = $facet->getFacetSourceId();
280
281
282

    if ($facet->getOnlyVisibleWhenFacetSourceIsVisible()) {
      // Block rendering and processing should be stopped when the facet source
283
284
      // is not available on the page. Returning an empty array here is enough
      // to halt all further processing.
285
      $facet_source = $facet->getFacetSource();
286
      if (is_null($facet_source) || !$facet_source->isRenderedInCurrentRequest()) {
287
288
289
        return [];
      }
    }
Jur de Vries's avatar
Jur de Vries committed
290

291
    // For clarity, process facets is called each build.
292
293
294
    // The first facet therefor will trigger the processing. Note that
    // processing is done only once, so repeatedly calling this method will not
    // trigger the processing more than once.
295
    $this->processFacets($facet_source_id);
296

297
298
    // Get the current results from the facets and let all processors that
    // trigger on the build step do their build processing.
299
    // @see \Drupal\facets\Processor\BuildProcessorInterface.
300
    // @see \Drupal\facets\Processor\SortProcessorInterface.
301
302
    $results = $facet->getResults();

303
    foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_BUILD) as $processor) {
304
305
      if (!$processor instanceof BuildProcessorInterface) {
        throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a build definition but doesn't implement the required BuildProcessorInterface interface");
306
      }
307
308
309
      $results = $processor->build($facet, $results);
    }

310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
    // Handle hierarchy.
    if ($results && $facet->getUseHierarchy()) {
      $keyed_results = [];
      foreach ($results as $result) {
        $keyed_results[$result->getRawValue()] = $result;
      }

      $parent_groups = $facet->getHierarchyInstance()->getChildIds(array_keys($keyed_results));
      $keyed_results = $this->buildHierarchicalTree($keyed_results, $parent_groups);

      // Remove children from primary level.
      foreach (array_unique($this->childIds) as $child_id) {
        unset($keyed_results[$child_id]);
      }

      $results = array_values($keyed_results);
    }

328
329
330
331
    // Trigger sort stage.
    $active_sort_processors = [];
    foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_SORT) as $processor) {
      $active_sort_processors[] = $processor;
332
    }
333
    uasort($results, function ($a, $b) use ($active_sort_processors) {
334
      $return = 0;
335
336
337
      foreach ($active_sort_processors as $sort_processor) {
        if ($return = $sort_processor->sortResults($a, $b)) {
          if ($sort_processor->getConfiguration()['sort'] == 'DESC') {
338
339
340
341
342
343
344
345
            $return *= -1;
          }
          break;
        }
      }
      return $return;
    });

346
347
    $facet->setResults($results);

348
349
    // No results behavior handling. Return a custom text or false depending on
    // settings.
350
    if (empty($facet->getResults())) {
351
      $empty_behavior = $facet->getEmptyBehavior();
borisson_'s avatar
borisson_ committed
352
      if ($empty_behavior['behavior'] == 'text') {
353
354
355
356
357
358
359
360
361
362
363
364
        return [
          [
            '#type' => 'container',
            '#attributes' => [
              'data-drupal-facet-id' => $facet->id(),
              'class' => 'facet-empty',
            ],
            'empty_text' => [
              '#markup' => t($empty_behavior['text']),
            ],
          ],
        ];
borisson_'s avatar
borisson_ committed
365
366
      }
      else {
367
        return [];
368
      }
369
370
    }

371
    // Let the widget plugin render the facet.
372
373
    /** @var \Drupal\facets\Widget\WidgetPluginInterface $widget */
    $widget = $facet->getWidgetInstance();
374

375
    return [$widget->build($facet)];
Jur de Vries's avatar
Jur de Vries committed
376
377
  }

378
  /**
379
380
381
382
   * Updates all facets of a given facet source with the results.
   *
   * @param string $facetsource_id
   *   The facet source ID of the currently processed facet.
383
   */
384
385
386
387
388
  public function updateResults($facetsource_id) {
    $facets = $this->getFacetsByFacetSourceId($facetsource_id);
    if ($facets) {
      /** @var \drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source_plugin */
      $facet_source_plugin = $this->facetSourcePluginManager->createInstance($facetsource_id);
389

390
391
      $facet_source_plugin->fillFacetsWithResults($facets);
    }
392
  }
393

394
395
396
397
398
399
400
401
  /**
   * Returns one of the processed facets.
   *
   * Returns one of the processed facets, this is a facet with filled results.
   * Keep in mind that if you want to have the facet's build processor executed,
   * there needs to be an extra call to the FacetManager::build with the facet
   * returned here as argument.
   *
402
403
   * @param \Drupal\facets\FacetInterface $facet
   *   The facet to process.
404
   *
405
   * @return \Drupal\facets\FacetInterface|null
406
407
   *   The updated facet if it exists, NULL otherwise.
   */
borisson_'s avatar
borisson_ committed
408
  public function returnProcessedFacet(FacetInterface $facet) {
409
410
    $this->processFacets($facet->getFacetSourceId());
    return !empty($this->facets[$facet->id()]) ? $this->facets[$facet->id()] : NULL;
411
412
  }

413
414
415
416
417
418
419
420
421
422
423
424
425
426
  /**
   * Builds an hierarchical structure for results.
   *
   * When given an array of results and an array which defines the hierarchical
   * structure, this will build the results structure and set all childs.
   *
   * @param \Drupal\facets\Result\ResultInterface[] $keyed_results
   *   An array of results keyed by id.
   * @param array $parent_groups
   *   An array of 'child id arrays' keyed by their parent id.
   *
   * @return \Drupal\facets\Result\ResultInterface[]
   *   An array of results structured hierarchicaly.
   */
borisson_'s avatar
borisson_ committed
427
  protected function buildHierarchicalTree(array $keyed_results, array $parent_groups) {
428
429
430
431
432
    foreach ($keyed_results as &$result) {
      $current_id = $result->getRawValue();
      if (isset($parent_groups[$current_id]) && $parent_groups[$current_id]) {
        $child_ids = $parent_groups[$current_id];
        $child_keyed_results = [];
borisson_'s avatar
borisson_ committed
433
434
        foreach ($child_ids as $child_id) {
          if (isset($keyed_results[$child_id])) {
435
436
437
            $child_keyed_results[$child_id] = $keyed_results[$child_id];
          }
        }
438
        $result->setChildren($child_keyed_results);
439
440
441
442
443
444
445
        $this->childIds = array_merge($this->childIds, $child_ids);
      }
    }

    return $keyed_results;
  }

446
}