DefaultFacetManager.php 12.2 KB
Newer Older
1
2
3
4
<?php

/**
 * @file
5
 * Contains Drupal\facets\FacetManager\DefaultFacetManager.
6
7
 */

8
namespace Drupal\facets\FacetManager;
9

10
use Drupal\Component\Render\FormattableMarkup;
Joris Vercammen's avatar
Joris Vercammen committed
11
use Drupal\Core\Entity\EntityTypeManagerInterface;
12
use Drupal\Core\StringTranslation\StringTranslationTrait;
13
14
15
16
17
18
19
20
21
use Drupal\facets\Exception\InvalidProcessorException;
use Drupal\facets\FacetInterface;
use Drupal\facets\FacetSource\FacetSourcePluginManager;
use Drupal\facets\Processor\BuildProcessorInterface;
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;
22
23

/**
Joris Vercammen's avatar
Joris Vercammen committed
24
 * The facet manager.
25
 *
26
27
28
 * 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.
29
 */
30
class DefaultFacetManager {
31

32
33
  use StringTranslationTrait;

34
  /**
35
   * The query type plugin manager.
Joris Vercammen's avatar
Joris Vercammen committed
36
37
38
   *
   * @var \Drupal\facets\QueryType\QueryTypePluginManager
   *   The query type plugin manager.
39
   */
Joris Vercammen's avatar
Joris Vercammen committed
40
  protected $queryTypePluginManager;
41

Jur de Vries's avatar
Jur de Vries committed
42
  /**
43
   * The facet source plugin manager.
Jur de Vries's avatar
Jur de Vries committed
44
   *
Joris Vercammen's avatar
Joris Vercammen committed
45
   * @var \Drupal\facets\FacetSource\FacetSourcePluginManager
Jur de Vries's avatar
Jur de Vries committed
46
   */
Joris Vercammen's avatar
Joris Vercammen committed
47
  protected $facetSourcePluginManager;
Jur de Vries's avatar
Jur de Vries committed
48

49
  /**
50
   * The processor plugin manager.
51
   *
52
   * @var \Drupal\facets\Processor\ProcessorPluginManager
53
   */
Joris Vercammen's avatar
Joris Vercammen committed
54
  protected $processorPluginManager;
55

56
57
58
59
60
61
62
  /**
   * The widget plugin manager.
   *
   * @var \Drupal\facets\Widget\WidgetPluginManager
   */
  protected $widgetPluginManager;

63
  /**
64
   * An array of facets that are being rendered.
65
   *
66
   * @var \Drupal\facets\FacetInterface[]
67
   *
68
69
   * @see \Drupal\facets\FacetInterface
   * @see \Drupal\facets\Entity\Facet
70
   */
71
  protected $facets = [];
72
73

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

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

  /**
   * Stores settings with defaults.
   *
   * @var array
   *
97
   * @see \Drupal\facets\FacetsFacetManager::getFacetSettings()
98
   */
99
  protected $settings = [];
100
101

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

108
  /**
109
   * Constructs a new instance of the DefaultFacetManager.
110
   *
111
   * @param \Drupal\facets\QueryType\QueryTypePluginManager $query_type_plugin_manager
Joris Vercammen's avatar
Joris Vercammen committed
112
   *   The query type plugin manager.
113
   * @param \Drupal\facets\Widget\WidgetPluginManager $widget_plugin_manager
Joris Vercammen's avatar
Joris Vercammen committed
114
   *   The widget plugin manager.
115
   * @param \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_manager
Joris Vercammen's avatar
Joris Vercammen committed
116
   *   The facet source plugin manager.
117
   * @param \Drupal\facets\Processor\ProcessorPluginManager $processor_plugin_manager
Joris Vercammen's avatar
Joris Vercammen committed
118
   *   The processor plugin manager.
Joris Vercammen's avatar
Joris Vercammen committed
119
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
Joris Vercammen's avatar
Joris Vercammen committed
120
   *   The entity type plugin manager.
121
   */
Joris Vercammen's avatar
Joris Vercammen committed
122
  public function __construct(QueryTypePluginManager $query_type_plugin_manager, WidgetPluginManager $widget_plugin_manager, FacetSourcePluginManager $facet_source_manager, ProcessorPluginManager $processor_plugin_manager, EntityTypeManagerInterface $entity_type_manager) {
Joris Vercammen's avatar
Joris Vercammen committed
123
    $this->queryTypePluginManager = $query_type_plugin_manager;
124
    $this->widgetPluginManager = $widget_plugin_manager;
Joris Vercammen's avatar
Joris Vercammen committed
125
126
    $this->facetSourcePluginManager = $facet_source_manager;
    $this->processorPluginManager = $processor_plugin_manager;
127
    $this->facetStorage = $entity_type_manager->getStorage('facets_facet');
128

129
130
    // Immediately initialize the facets. This can be done directly because the
    // only thing needed is the url.
131
    $this->initFacets();
132
133
134
135
136
137
  }

  /**
   * Allows the backend to add facet queries to its native query object.
   *
   * This method is called by the implementing module to initialize the facet
138
   * display process.
139
140
141
   *
   * @param mixed $query
   *   The backend's native query object.
142
143
   * @param string $facetsource_id
   *   The facet source ID to process.
144
   */
145
  public function alterQuery(&$query, $facetsource_id) {
146
    /** @var \Drupal\facets\FacetInterface[] $facets */
147
148
149
150
151
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]);
      $unfiltered_results = $query_type_plugin->execute();

      // Save unfiltered results in facet.
      if (!is_null($unfiltered_results)) {
        $facet->setUnfilteredResults($unfiltered_results);
155
      }
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
179
180
181
182
183
184
185
  protected function getFacetsByFacetSourceId($facetsource_id) {
    $facets = [];
    foreach ($this->facets as $facet) {
      if ($facet->getFacetSourceId() == $facetsource_id) {
        $facets[] = $facet;
      }
    }
    return $facets;
186
187
188
189
190
  }

  /**
   * Initializes facet builds, sets the breadcrumb trail.
   *
191
192
   * Facets are built via FacetsFacetProcessor objects. Facets only need to be
   * processed, or built, once The FacetsFacetManager::processed semaphore is
193
194
   * set when this method is called ensuring that facets are built only once
   * regardless of how many times this method is called.
195
   *
Joris Vercammen's avatar
Joris Vercammen committed
196
   * @param string|NULL $facetsource_id
197
   *   The facetsource if of the currently processed facet.
198
   */
199
200
201
202
203
204
205
206
  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])) {
207
      // First add the results to the facets.
208
      $this->updateResults($facetsource_id);
Jur de Vries's avatar
Jur de Vries committed
209

210
      $this->processedFacetSources[$facetsource_id] = TRUE;
211
    }
212
213
  }

214
  /**
215
   * Initializes enabled facets.
216
   *
217
218
   * In this method all pre-query processors get called and their contents are
   * executed.
219
220
221
222
223
   */
  protected function initFacets() {
    if (empty($this->facets)) {
      $this->facets = $this->getEnabledFacets();
      foreach ($this->facets as $facet) {
224

225
226
227
228
        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) {
229
            throw new InvalidProcessorException(new FormattableMarkup("The processor @processor has a pre_query definition but doesn't implement the required PreQueryProcessorInterface interface", ['@processor' => $processor->getPluginDefinition()['id']]));
230
          }
231
          $pre_query_processor->preQuery($facet);
232
        }
233
234
235
236
      }
    }
  }

237
  /**
238
   * Builds a facet and returns it as a renderable array.
239
240
241
242
243
244
245
246
247
   *
   * 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.
   *
248
   * @param \Drupal\facets\FacetInterface $facet
Joris Vercammen's avatar
Joris Vercammen committed
249
   *   The facet we should build.
Joris Vercammen's avatar
Joris Vercammen committed
250
   *
251
252
   * @return array
   *   Facet render arrays.
Joris Vercammen's avatar
Joris Vercammen committed
253
   *
254
   * @throws \Drupal\facets\Exception\InvalidProcessorException
Joris Vercammen's avatar
Joris Vercammen committed
255
   *   Throws an exception when an invalid processor is linked to the facet.
256
257
   */
  public function build(FacetInterface $facet) {
258
259
260
261
    // 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.
262
    $facet = $this->facets[$facet->id()];
263
    $facet_source_id = $facet->getFacetSourceId();
264
265
266

    if ($facet->getOnlyVisibleWhenFacetSourceIsVisible()) {
      // Block rendering and processing should be stopped when the facet source
267
268
      // is not available on the page. Returning an empty array here is enough
      // to halt all further processing.
269
      $facet_source = $facet->getFacetSource();
270
      if (!$facet_source->isRenderedInCurrentRequest()) {
271
272
273
        return [];
      }
    }
Jur de Vries's avatar
Jur de Vries committed
274

275
    // For clarity, process facets is called each build.
276
277
278
    // 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.
279
    $this->processFacets($facet_source_id);
280

281
282
    // Get the current results from the facets and let all processors that
    // trigger on the build step do their build processing.
283
284
    // @see \Drupal\facets\Processor\BuildProcessorInterface.
    // @see \Drupal\facets\Processor\WidgetOrderProcessorInterface.
285
286
    $results = $facet->getResults();

287
    foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_BUILD) as $processor) {
288
      /** @var \Drupal\facets\Processor\BuildProcessorInterface $build_processor */
289
290
291
      $build_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]);
      if (!$build_processor instanceof BuildProcessorInterface) {
        throw new InvalidProcessorException(new FormattableMarkup("The processor @processor has a build definition but doesn't implement the required BuildProcessorInterface interface", ['@processor' => $processor['processor_id']]));
292
      }
293
      $results = $build_processor->build($facet, $results);
294
295
296
    }
    $facet->setResults($results);

297
298
    // No results behavior handling. Return a custom text or false depending on
    // settings.
299
    if (empty($facet->getResults())) {
300
      $empty_behavior = $facet->getEmptyBehavior();
Joris Vercammen's avatar
Joris Vercammen committed
301
      if ($empty_behavior['behavior'] == 'text') {
302
        return ['#markup' => $empty_behavior['text']];
Joris Vercammen's avatar
Joris Vercammen committed
303
304
      }
      else {
305
        return [];
306
      }
307
308
    }

309
    // Let the widget plugin render the facet.
310
    /** @var \Drupal\facets\Widget\WidgetInterface $widget */
311
    $widget = $this->widgetPluginManager->createInstance($facet->getWidget());
312
313

    return $widget->build($facet);
Jur de Vries's avatar
Jur de Vries committed
314
315
  }

316
  /**
317
318
319
320
   * Updates all facets of a given facet source with the results.
   *
   * @param string $facetsource_id
   *   The facet source ID of the currently processed facet.
321
   */
322
323
324
325
326
  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);
327

328
329
      $facet_source_plugin->fillFacetsWithResults($facets);
    }
330
  }
331

332
333
334
335
336
337
338
339
  /**
   * 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.
   *
340
341
   * @param \Drupal\facets\FacetInterface $facet
   *   The facet to process.
342
   *
343
344
345
   * @return \Drupal\facets\FacetInterface|NULL
   *   The updated facet if it exists, NULL otherwise.
   */
Joris Vercammen's avatar
Joris Vercammen committed
346
  public function returnProcessedFacet(FacetInterface $facet) {
347
348
    $this->processFacets($facet->getFacetSourceId());
    return !empty($this->facets[$facet->id()]) ? $this->facets[$facet->id()] : NULL;
349
350
  }

351
}