Total Complexity | 140 |
Total Lines | 1083 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like RoutingService 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.
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 RoutingService, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
41 | class RoutingService implements LoggerAwareInterface |
||
42 | { |
||
43 | use LoggerAwareTrait; |
||
44 | |||
45 | /** |
||
46 | * Default plugin namespace |
||
47 | */ |
||
48 | const PLUGIN_NAMESPACE = 'tx_solr'; |
||
49 | |||
50 | /** |
||
51 | * Settings from routing configuration |
||
52 | * |
||
53 | * @var array |
||
54 | */ |
||
55 | protected $settings = []; |
||
56 | |||
57 | /** |
||
58 | * List of filter that are placed as path arguments |
||
59 | * |
||
60 | * @var array |
||
61 | */ |
||
62 | protected $pathArguments = []; |
||
63 | |||
64 | /** |
||
65 | * Plugin/extension namespace |
||
66 | * |
||
67 | * @var string |
||
68 | */ |
||
69 | protected $pluginNamespace = 'tx_solr'; |
||
70 | |||
71 | /** |
||
72 | * List of TYPO3 core parameters, that we should ignore |
||
73 | * |
||
74 | * @see \TYPO3\CMS\Frontend\Page\CacheHashCalculator::isCoreParameter |
||
75 | * @var string[] |
||
76 | */ |
||
77 | protected $coreParameters = ['no_cache', 'cHash', 'id', 'MP', 'type']; |
||
78 | |||
79 | /** |
||
80 | * @var UrlFacetService |
||
81 | */ |
||
82 | protected $urlFacetPathService; |
||
83 | |||
84 | /** |
||
85 | * @var UrlFacetService |
||
86 | */ |
||
87 | protected $urlFacetQueryService; |
||
88 | |||
89 | /** |
||
90 | * RoutingService constructor. |
||
91 | * |
||
92 | * @param array $settings |
||
93 | * @param string $pluginNamespace |
||
94 | */ |
||
95 | public function __construct(array $settings = [], string $pluginNamespace = self::PLUGIN_NAMESPACE) |
||
96 | { |
||
97 | $this->settings = $settings; |
||
98 | $this->pluginNamespace = $pluginNamespace; |
||
99 | if (empty($this->pluginNamespace)) { |
||
100 | $this->pluginNamespace = self::PLUGIN_NAMESPACE; |
||
101 | } |
||
102 | $this->initUrlFacetService(); |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * Creates a clone of the current service and replace the settings inside |
||
107 | * |
||
108 | * @param array $settings |
||
109 | * @return RoutingService |
||
110 | */ |
||
111 | public function withSettings(array $settings): RoutingService |
||
112 | { |
||
113 | $service = clone $this; |
||
114 | $service->settings = $settings; |
||
115 | $service->initUrlFacetService(); |
||
116 | return $service; |
||
117 | } |
||
118 | |||
119 | /** |
||
120 | * Creates a clone of the current service and replace the settings inside |
||
121 | * |
||
122 | * @param array $pathArguments |
||
123 | * @return RoutingService |
||
124 | */ |
||
125 | public function withPathArguments(array $pathArguments): RoutingService |
||
126 | { |
||
127 | $service = clone $this; |
||
128 | $service->pathArguments = $pathArguments; |
||
129 | $service->initUrlFacetService(); |
||
130 | return $service; |
||
131 | } |
||
132 | |||
133 | /** |
||
134 | * Load configuration from routing configuration |
||
135 | * |
||
136 | * @param array $routingConfiguration |
||
137 | * @return $this |
||
138 | */ |
||
139 | public function fromRoutingConfiguration(array $routingConfiguration): RoutingService |
||
140 | { |
||
141 | if (empty($routingConfiguration) || |
||
142 | empty($routingConfiguration['type']) || |
||
143 | !$this->isRouteEnhancerForSolr((string)$routingConfiguration['type'])) { |
||
144 | return $this; |
||
145 | } |
||
146 | |||
147 | if (isset($routingConfiguration['solr'])) { |
||
148 | $this->settings = $routingConfiguration['solr']; |
||
149 | $this->initUrlFacetService(); |
||
150 | } |
||
151 | |||
152 | if (isset($routingConfiguration['_arguments'])) { |
||
153 | $this->pathArguments = $routingConfiguration['_arguments']; |
||
154 | } |
||
155 | |||
156 | return $this; |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * Reset the routing service |
||
161 | * |
||
162 | * @return $this |
||
163 | */ |
||
164 | public function reset(): RoutingService |
||
165 | { |
||
166 | $this->settings = []; |
||
167 | $this->pathArguments = []; |
||
168 | $this->pluginNamespace = self::PLUGIN_NAMESPACE; |
||
169 | return $this; |
||
170 | } |
||
171 | |||
172 | /** |
||
173 | * Initialize url facet services for different types |
||
174 | * |
||
175 | * @return $this |
||
176 | */ |
||
177 | protected function initUrlFacetService(): RoutingService |
||
178 | { |
||
179 | $this->urlFacetPathService = new UrlFacetService('path', $this->settings); |
||
180 | $this->urlFacetQueryService = new UrlFacetService('query', $this->settings); |
||
181 | |||
182 | return $this; |
||
183 | } |
||
184 | |||
185 | /** |
||
186 | * @return UrlFacetService |
||
187 | */ |
||
188 | public function getUrlFacetPathService(): UrlFacetService |
||
189 | { |
||
190 | return $this->urlFacetPathService; |
||
191 | } |
||
192 | |||
193 | /** |
||
194 | * @return UrlFacetService |
||
195 | */ |
||
196 | public function getUrlFacetQueryService(): UrlFacetService |
||
197 | { |
||
198 | return $this->urlFacetQueryService; |
||
199 | } |
||
200 | |||
201 | /** |
||
202 | * Test if the given parameter is a Core parameter |
||
203 | * |
||
204 | * @see \TYPO3\CMS\Frontend\Page\CacheHashCalculator::isCoreParameter |
||
205 | * @param string $parameterName |
||
206 | * @return bool |
||
207 | */ |
||
208 | public function isCoreParameter(string $parameterName): bool |
||
209 | { |
||
210 | return in_array($parameterName, $this->coreParameters); |
||
211 | } |
||
212 | |||
213 | /** |
||
214 | * This returns the plugin namespace |
||
215 | * @see https://docs.typo3.org/p/apache-solr-for-typo3/solr/master/en-us/Configuration/Reference/TxSolrView.html#pluginnamespace |
||
216 | * |
||
217 | * @return string |
||
218 | */ |
||
219 | public function getPluginNamespace(): string |
||
220 | { |
||
221 | return $this->pluginNamespace; |
||
222 | } |
||
223 | |||
224 | /** |
||
225 | * Determine if an enhancer is in use for Solr |
||
226 | * |
||
227 | * @param string $enhancerName |
||
228 | * @return bool |
||
229 | */ |
||
230 | public function isRouteEnhancerForSolr(string $enhancerName): bool |
||
231 | { |
||
232 | if (empty($enhancerName)) { |
||
233 | return false; |
||
234 | } |
||
235 | |||
236 | if (!isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers'][$enhancerName])) { |
||
237 | return false; |
||
238 | } |
||
239 | $className = $GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers'][$enhancerName]; |
||
240 | |||
241 | if (!class_exists($className)) { |
||
242 | return false; |
||
243 | } |
||
244 | |||
245 | $interfaces = class_implements($className); |
||
246 | |||
247 | return in_array(SolrRouteEnhancerInterface::class, $interfaces); |
||
248 | } |
||
249 | |||
250 | /** |
||
251 | * Masks Solr filter inside of the query parameters |
||
252 | * |
||
253 | * @param string $uriPath |
||
254 | * @return string |
||
255 | */ |
||
256 | public function finalizePathQuery(string $uriPath): string |
||
257 | { |
||
258 | $pathSegments = explode('/', $uriPath); |
||
259 | $query = array_pop($pathSegments); |
||
260 | $queryValues = explode($this->urlFacetPathService->getMultiValueSeparator(), $query); |
||
261 | $queryValues = array_map([$this->urlFacetPathService, 'decodeSingleValue'], $queryValues); |
||
262 | /* |
||
263 | * In some constellations the path query contains the facet type in front. |
||
264 | * This leads to the result, that the query values could contain the same facet value multiple times. |
||
265 | * |
||
266 | * In order to avoid this behaviour, the query values need to be checked and clean up. |
||
267 | * 1. Remove possible prefix information |
||
268 | * 2. Apply character replacements |
||
269 | * 3. Filter duplicate values |
||
270 | */ |
||
271 | for ($i = 0; $i < count($queryValues); $i++) { |
||
|
|||
272 | $queryValues[$i] = urldecode($queryValues[$i]); |
||
273 | if ($this->containsFacetAndValueSeparator((string)$queryValues[$i])) { |
||
274 | [$facetName, $facetValue] = explode( |
||
275 | $this->detectFacetAndValueSeparator((string)$queryValues[$i]), |
||
276 | (string)$queryValues[$i], |
||
277 | 2 |
||
278 | ); |
||
279 | |||
280 | if ($this->isPathArgument((string)$facetName)) { |
||
281 | $queryValues[$i] = $facetValue; |
||
282 | } |
||
283 | |||
284 | } |
||
285 | $queryValues[$i] = $this->urlFacetPathService->applyCharacterMap($queryValues[$i]); |
||
286 | } |
||
287 | |||
288 | $queryValues = array_unique($queryValues); |
||
289 | $queryValues = array_map([$this->urlFacetPathService, 'encodeSingleValue'], $queryValues); |
||
290 | sort($queryValues); |
||
291 | $pathSegments[] = implode( |
||
292 | $this->urlFacetPathService->getMultiValueSeparator(), |
||
293 | $queryValues |
||
294 | ); |
||
295 | return implode('/', $pathSegments); |
||
296 | } |
||
297 | |||
298 | /** |
||
299 | * This method checks if the query parameter should be masked. |
||
300 | * |
||
301 | * @return bool |
||
302 | */ |
||
303 | public function shouldMaskQueryParameter(): bool |
||
304 | { |
||
305 | if (!isset($this->settings['query']['mask']) || |
||
306 | !(bool)$this->settings['query']['mask']) { |
||
307 | return false; |
||
308 | } |
||
309 | |||
310 | $targetFields = $this->getQueryParameterMap(); |
||
311 | |||
312 | return !empty($targetFields); |
||
313 | } |
||
314 | |||
315 | /** |
||
316 | * Masks Solr filter inside of the query parameters |
||
317 | * |
||
318 | * @param array $queryParams |
||
319 | * @return array |
||
320 | */ |
||
321 | public function maskQueryParameters(array $queryParams): array |
||
322 | { |
||
323 | if (!$this->shouldMaskQueryParameter()) { |
||
324 | return $queryParams; |
||
325 | } |
||
326 | |||
327 | if (!isset($queryParams[$this->getPluginNamespace()])) { |
||
328 | $this->logger |
||
329 | ->error('Mask error: Query parameters has no entry for namespace ' . $this->getPluginNamespace()); |
||
330 | return $queryParams; |
||
331 | } |
||
332 | |||
333 | if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) { |
||
334 | $this->logger |
||
335 | ->info('Mask info: Query parameters has no filter in namespace ' . $this->getPluginNamespace()); |
||
336 | return $queryParams; |
||
337 | } |
||
338 | $queryParameterMap = $this->getQueryParameterMap(); |
||
339 | $newQueryParams = $queryParams; |
||
340 | |||
341 | $newFilterArray = []; |
||
342 | foreach ($newQueryParams[$this->getPluginNamespace()]['filter'] as $queryParamName => $queryParamValue) { |
||
343 | $defaultSeparator = $this->detectFacetAndValueSeparator((string)$queryParamValue); |
||
344 | [$facetName, $facetValue] = explode($defaultSeparator, $queryParamValue, 2); |
||
345 | $keep = false; |
||
346 | if (isset($queryParameterMap[$facetName]) && |
||
347 | isset($newQueryParams[$queryParameterMap[$facetName]])) { |
||
348 | $this->logger->error( |
||
349 | 'Mask error: Facet "' . $facetName . '" as "' . $queryParameterMap[$facetName] . |
||
350 | '" already in query!' |
||
351 | ); |
||
352 | $keep = true; |
||
353 | } |
||
354 | if (!isset($queryParameterMap[$facetName]) || $keep) { |
||
355 | $newFilterArray[] = $queryParamValue; |
||
356 | continue; |
||
357 | } |
||
358 | |||
359 | $newQueryParams[$queryParameterMap[$facetName]] = $facetValue; |
||
360 | } |
||
361 | |||
362 | $newQueryParams[$this->getPluginNamespace()]['filter'] = $newFilterArray; |
||
363 | |||
364 | return $this->cleanUpQueryParameters($newQueryParams); |
||
365 | } |
||
366 | |||
367 | /** |
||
368 | * Unmask incoming parameters if needed |
||
369 | * |
||
370 | * @param array $queryParams |
||
371 | * @return array |
||
372 | */ |
||
373 | public function unmaskQueryParameters(array $queryParams): array |
||
374 | { |
||
375 | if (!$this->shouldMaskQueryParameter()) { |
||
376 | return $queryParams; |
||
377 | } |
||
378 | |||
379 | /* |
||
380 | * The array $queryParameterMap contains the mapping of |
||
381 | * facet name to new url name. In order to unmask we need to switch key and values. |
||
382 | */ |
||
383 | $queryParameterMap = $this->getQueryParameterMap(); |
||
384 | $queryParameterMapSwitched = []; |
||
385 | foreach ($queryParameterMap as $value => $key) { |
||
386 | $queryParameterMapSwitched[$key] = $value; |
||
387 | } |
||
388 | |||
389 | $newQueryParams = []; |
||
390 | foreach ($queryParams as $queryParamName => $queryParamValue) { |
||
391 | // A merge is needed! |
||
392 | if (!isset($queryParameterMapSwitched[$queryParamName])) { |
||
393 | if (isset($newQueryParams[$queryParamName])) { |
||
394 | $newQueryParams[$queryParamName] = array_merge_recursive( |
||
395 | $newQueryParams[$queryParamName], |
||
396 | $queryParamValue |
||
397 | ); |
||
398 | } else { |
||
399 | $newQueryParams[$queryParamName] = $queryParamValue; |
||
400 | } |
||
401 | continue; |
||
402 | } |
||
403 | if (!isset($newQueryParams[$this->getPluginNamespace()])) { |
||
404 | $newQueryParams[$this->getPluginNamespace()] = []; |
||
405 | } |
||
406 | if (!isset($newQueryParams[$this->getPluginNamespace()]['filter'])) { |
||
407 | $newQueryParams[$this->getPluginNamespace()]['filter'] = []; |
||
408 | } |
||
409 | |||
410 | $newQueryParams[$this->getPluginNamespace()]['filter'][] = |
||
411 | $queryParameterMapSwitched[$queryParamName] . ':' . $queryParamValue; |
||
412 | } |
||
413 | |||
414 | return $this->cleanUpQueryParameters($newQueryParams); |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * This method check if the query parameters should be touched or not. |
||
419 | * |
||
420 | * There are following requirements: |
||
421 | * - Masking is activated and the mal is valid or |
||
422 | * - Concat is activated |
||
423 | * |
||
424 | * @return bool |
||
425 | */ |
||
426 | public function shouldConcatQueryParameters(): bool |
||
427 | { |
||
428 | /* |
||
429 | * The concat will activate automatically if parameters should be masked. |
||
430 | * This solution is less complex since not every mapping parameter needs to be tested |
||
431 | */ |
||
432 | if ($this->shouldMaskQueryParameter()) { |
||
433 | return true; |
||
434 | } |
||
435 | |||
436 | return isset($this->settings['query']['concat']) && (bool)$this->settings['query']['concat']; |
||
437 | } |
||
438 | |||
439 | /** |
||
440 | * Returns the query parameter map |
||
441 | * |
||
442 | * Note TYPO3 core query arguments removed from the configured map! |
||
443 | * |
||
444 | * @return array |
||
445 | */ |
||
446 | public function getQueryParameterMap(): array |
||
447 | { |
||
448 | if (!isset($this->settings['query']['map']) || |
||
449 | !is_array($this->settings['query']['map']) || |
||
450 | empty($this->settings['query']['map'])) { |
||
451 | return []; |
||
452 | } |
||
453 | // TODO: Test if there is more than one value! |
||
454 | $self = $this; |
||
455 | return array_filter( |
||
456 | $this->settings['query']['map'], |
||
457 | function($value) use ($self) { |
||
458 | return !$self->isCoreParameter($value); |
||
459 | } |
||
460 | ); |
||
461 | } |
||
462 | |||
463 | /** |
||
464 | * Group all filter values together and concat e |
||
465 | * Note: this will just handle filter values |
||
466 | * |
||
467 | * IN: |
||
468 | * tx_solr => [ |
||
469 | * filter => [ |
||
470 | * color:red |
||
471 | * product:candy |
||
472 | * color:blue |
||
473 | * taste:sour |
||
474 | * ] |
||
475 | * ] |
||
476 | * |
||
477 | * OUT: |
||
478 | * tx_solr => [ |
||
479 | * filter => [ |
||
480 | * color:blue,red |
||
481 | * product:candy |
||
482 | * taste:sour |
||
483 | * ] |
||
484 | * ] |
||
485 | * @param array $queryParams |
||
486 | * @return array |
||
487 | */ |
||
488 | public function concatQueryParameter(array $queryParams = []): array |
||
489 | { |
||
490 | if (!$this->shouldConcatQueryParameters()) { |
||
491 | return $queryParams; |
||
492 | } |
||
493 | |||
494 | if (!isset($queryParams[$this->getPluginNamespace()])) { |
||
495 | $this->logger |
||
496 | ->error('Mask error: Query parameters has no entry for namespace ' . $this->getPluginNamespace()); |
||
497 | return $queryParams; |
||
498 | } |
||
499 | |||
500 | if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) { |
||
501 | $this->logger |
||
502 | ->info('Mask info: Query parameters has no filter in namespace ' . $this->getPluginNamespace()); |
||
503 | return $queryParams; |
||
504 | } |
||
505 | |||
506 | $queryParams[$this->getPluginNamespace()]['filter'] = |
||
507 | $this->concatFilterValues($queryParams[$this->getPluginNamespace()]['filter']); |
||
508 | |||
509 | return $this->cleanUpQueryParameters($queryParams); |
||
510 | } |
||
511 | |||
512 | /** |
||
513 | * This method expect a filter array that should be concat instead of the whole query |
||
514 | * |
||
515 | * @param array $filterArray |
||
516 | * @return array |
||
517 | */ |
||
518 | public function concatFilterValues(array $filterArray): array |
||
519 | { |
||
520 | if (empty($filterArray) || !$this->shouldConcatQueryParameters()) { |
||
521 | return $filterArray; |
||
522 | } |
||
523 | |||
524 | $queryParameterMap = $this->getQueryParameterMap(); |
||
525 | $newFilterArray = []; |
||
526 | $defaultSeparator = $this->detectFacetAndValueSeparator((string)$filterArray[0]); |
||
527 | // Collect parameter names and rename parameter if required |
||
528 | foreach ($filterArray as $set) { |
||
529 | $separator = $this->detectFacetAndValueSeparator((string)$set); |
||
530 | [$facetName, $facetValue] = explode($separator, $set, 2); |
||
531 | if (isset($queryParameterMap[$facetName])) { |
||
532 | $facetName = $queryParameterMap[$facetName]; |
||
533 | } |
||
534 | if (!isset($newFilterArray[$facetName])) { |
||
535 | $newFilterArray[$facetName] = [$facetValue]; |
||
536 | } else { |
||
537 | $newFilterArray[$facetName][] = $facetValue; |
||
538 | } |
||
539 | } |
||
540 | |||
541 | foreach ($newFilterArray as $facetName => $facetValues) { |
||
542 | $newFilterArray[$facetName] = $facetName . $defaultSeparator . $this->queryParameterFacetsToString($facetValues); |
||
543 | } |
||
544 | |||
545 | return array_values($newFilterArray); |
||
546 | } |
||
547 | |||
548 | /** |
||
549 | * Inflate given query parameters if configured |
||
550 | * Note: this will just combine filter values |
||
551 | * |
||
552 | * IN: |
||
553 | * tx_solr => [ |
||
554 | * filter => [ |
||
555 | * color:blue,red |
||
556 | * product:candy |
||
557 | * taste:sour |
||
558 | * ] |
||
559 | * ] |
||
560 | * |
||
561 | * OUT: |
||
562 | * tx_solr => [ |
||
563 | * filter => [ |
||
564 | * color:red |
||
565 | * product:candy |
||
566 | * color:blue |
||
567 | * taste:sour |
||
568 | * ] |
||
569 | * ] |
||
570 | * |
||
571 | * @param array $queryParams |
||
572 | * @return array |
||
573 | */ |
||
574 | public function inflateQueryParameter(array $queryParams = []): array |
||
575 | { |
||
576 | if (!$this->shouldConcatQueryParameters()) { |
||
577 | return $queryParams; |
||
578 | } |
||
579 | |||
580 | if (!isset($queryParams[$this->getPluginNamespace()])) { |
||
581 | $queryParams[$this->getPluginNamespace()] = []; |
||
582 | } |
||
583 | |||
584 | if (!isset($queryParams[$this->getPluginNamespace()]['filter'])) { |
||
585 | $queryParams[$this->getPluginNamespace()]['filter'] = []; |
||
586 | } |
||
587 | |||
588 | $newQueryParams = []; |
||
589 | foreach ($queryParams[$this->getPluginNamespace()]['filter'] as $set) { |
||
590 | $separator = $this->detectFacetAndValueSeparator((string)$set); |
||
591 | [$facetName, $facetValuesString] = explode($separator, $set, 2); |
||
592 | $facetValues = [$facetValuesString]; |
||
593 | $facetValues = explode($this->urlFacetQueryService->getMultiValueSeparator(), $facetValuesString); |
||
594 | |||
595 | /** |
||
596 | * A facet value could contain the multi value separator. This value is masked in order to |
||
597 | * avoid problems during separation of the values (line above). |
||
598 | * |
||
599 | * After splitting the values, the character inside the value need to be restored |
||
600 | * |
||
601 | * @see RoutingService::queryParameterFacetsToString |
||
602 | */ |
||
603 | $facetValues = array_map([$this->urlFacetQueryService, 'decodeSingleValue'], $facetValues); |
||
604 | |||
605 | foreach ($facetValues as $facetValue) { |
||
606 | $newQueryParams[] = $facetName . $separator . $facetValue; |
||
607 | } |
||
608 | } |
||
609 | $queryParams[$this->getPluginNamespace()]['filter'] = array_values($newQueryParams); |
||
610 | |||
611 | return $this->cleanUpQueryParameters($queryParams); |
||
612 | } |
||
613 | |||
614 | /** |
||
615 | * Cleanup the query parameters, to avoid empty solr arguments |
||
616 | * |
||
617 | * @param array $queryParams |
||
618 | * @return array |
||
619 | */ |
||
620 | public function cleanUpQueryParameters(array $queryParams): array |
||
621 | { |
||
622 | if (empty($queryParams[$this->getPluginNamespace()]['filter'])) { |
||
623 | unset($queryParams[$this->getPluginNamespace()]['filter']); |
||
624 | } |
||
625 | |||
626 | if (empty($queryParams[$this->getPluginNamespace()])) { |
||
627 | unset($queryParams[$this->getPluginNamespace()]); |
||
628 | } |
||
629 | return $queryParams; |
||
630 | } |
||
631 | |||
632 | /** |
||
633 | * Builds a string out of multiple facet values |
||
634 | * |
||
635 | * A facet value could contain the multi value separator. This value have to masked in order to |
||
636 | * avoid problems during separation of the values later. |
||
637 | * |
||
638 | * This mask have to apply before contact the values |
||
639 | * |
||
640 | * @param array $facets |
||
641 | * @return string |
||
642 | */ |
||
643 | public function queryParameterFacetsToString(array $facets): string |
||
644 | { |
||
645 | $facets = array_map([$this->urlFacetQueryService, 'encodeSingleValue'], $facets); |
||
646 | sort($facets); |
||
647 | return implode($this->urlFacetQueryService->getMultiValueSeparator(), $facets); |
||
648 | } |
||
649 | |||
650 | /** |
||
651 | * Returns the string which separates the facet from the value |
||
652 | * |
||
653 | * @param string $facetWithValue |
||
654 | * @return string |
||
655 | */ |
||
656 | public function detectFacetAndValueSeparator(string $facetWithValue): string |
||
657 | { |
||
658 | $separator = ':'; |
||
659 | if (mb_strpos($facetWithValue, '%3A') !== false) { |
||
660 | $separator = '%3A'; |
||
661 | } |
||
662 | |||
663 | return $separator; |
||
664 | } |
||
665 | |||
666 | /** |
||
667 | * Check if given facet value combination contains a separator |
||
668 | * |
||
669 | * @param string $facetWithValue |
||
670 | * @return bool |
||
671 | */ |
||
672 | public function containsFacetAndValueSeparator(string $facetWithValue): bool |
||
673 | { |
||
674 | if (mb_strpos($facetWithValue, ':') === false && mb_strpos($facetWithValue, '%3A') === false) { |
||
675 | return false; |
||
676 | } |
||
677 | |||
678 | return true; |
||
679 | } |
||
680 | |||
681 | /** |
||
682 | * Cleanup facet values (strip type if needed) |
||
683 | * |
||
684 | * @param array $facetValues |
||
685 | * @return array |
||
686 | */ |
||
687 | public function cleanupFacetValues(array $facetValues): array |
||
688 | { |
||
689 | for ($i = 0; $i < count($facetValues); $i++) { |
||
690 | if (!$this->containsFacetAndValueSeparator((string)$facetValues[$i])) { |
||
691 | continue; |
||
692 | } |
||
693 | |||
694 | $separator = $this->detectFacetAndValueSeparator((string)$facetValues[$i]); |
||
695 | [$type, $value] = explode($separator, $facetValues[$i]); |
||
696 | |||
697 | if ($this->isMappingArgument($type) || $this->isPathArgument($type)) { |
||
698 | $facetValues[$i] = $value; |
||
699 | } |
||
700 | } |
||
701 | return $facetValues; |
||
702 | } |
||
703 | |||
704 | /** |
||
705 | * Builds a string out of multiple facet values |
||
706 | * |
||
707 | * @param array $facets |
||
708 | * @return string |
||
709 | */ |
||
710 | public function pathFacetsToString(array $facets): string |
||
711 | { |
||
712 | $facets = $this->cleanupFacetValues($facets); |
||
713 | sort($facets); |
||
714 | $facets = array_map([$this->urlFacetPathService, 'applyCharacterMap'], $facets); |
||
715 | $facets = array_map([$this->urlFacetPathService, 'encodeSingleValue'], $facets); |
||
716 | return implode($this->urlFacetPathService->getMultiValueSeparator(), $facets); |
||
717 | } |
||
718 | |||
719 | /** |
||
720 | * Builds a string out of multiple facet values |
||
721 | * |
||
722 | * @param array $facets |
||
723 | * @return string |
||
724 | */ |
||
725 | public function facetsToString(array $facets): string |
||
726 | { |
||
727 | $facets = $this->cleanupFacetValues($facets); |
||
728 | sort($facets); |
||
729 | return implode($this->getDefaultMultiValueSeparator(), $facets); |
||
730 | } |
||
731 | |||
732 | /** |
||
733 | * Builds a string out of multiple facet values |
||
734 | * |
||
735 | * This method is used in two different situation |
||
736 | * 1. Middleware: Here the values should not be decoded |
||
737 | * 2. Within the event listener CachedPathVariableModifier |
||
738 | * |
||
739 | * @param string $facets |
||
740 | * @param bool $decode |
||
741 | * @return array |
||
742 | */ |
||
743 | public function pathFacetStringToArray(string $facets, bool $decode = true): array |
||
744 | { |
||
745 | $facetString = $this->urlFacetPathService->applyCharacterMap($facets); |
||
746 | $facets = explode($this->urlFacetPathService->getMultiValueSeparator(), $facetString); |
||
747 | if (!$decode) { |
||
748 | return $facets; |
||
749 | } |
||
750 | return array_map([$this->urlFacetPathService, 'decodeSingleValue'], $facets); |
||
751 | } |
||
752 | |||
753 | /** |
||
754 | * Returns the multi value separator |
||
755 | * @return string |
||
756 | */ |
||
757 | public function getDefaultMultiValueSeparator(): string |
||
758 | { |
||
759 | return $this->settings['multiValueSeparator'] ?? ','; |
||
760 | } |
||
761 | |||
762 | /** |
||
763 | * Find a enhancer configuration by a given page id |
||
764 | * |
||
765 | * @param int $pageUid |
||
766 | * @return array |
||
767 | */ |
||
768 | public function fetchEnhancerByPageUid(int $pageUid): array |
||
769 | { |
||
770 | $site = $this->findSiteByUid($pageUid); |
||
771 | if ($site instanceof NullSite) { |
||
772 | return []; |
||
773 | } |
||
774 | |||
775 | return $this->fetchEnhancerInSiteConfigurationByPageUid( |
||
776 | $site, |
||
777 | $pageUid |
||
778 | ); |
||
779 | } |
||
780 | |||
781 | /** |
||
782 | * Returns the route enhancer configuration by given site and page uid |
||
783 | * |
||
784 | * @param Site $site |
||
785 | * @param int $pageUid |
||
786 | * @return array |
||
787 | */ |
||
788 | public function fetchEnhancerInSiteConfigurationByPageUid(Site $site, int $pageUid): array |
||
789 | { |
||
790 | $configuration = $site->getConfiguration(); |
||
791 | if (empty($configuration['routeEnhancers']) || !is_array($configuration['routeEnhancers'])) { |
||
792 | return []; |
||
793 | } |
||
794 | $result = []; |
||
795 | foreach ($configuration['routeEnhancers'] as $routing => $settings) { |
||
796 | // Not the page we are looking for |
||
797 | if (isset($settings['limitToPages']) && |
||
798 | is_array($settings['limitToPages']) && |
||
799 | !in_array($pageUid, $settings['limitToPages'])) { |
||
800 | continue; |
||
801 | } |
||
802 | |||
803 | if (empty($settings) || !isset($settings['type']) || |
||
804 | !$this->isRouteEnhancerForSolr((string)$settings['type']) |
||
805 | ) { |
||
806 | continue; |
||
807 | } |
||
808 | $result[] = $settings; |
||
809 | } |
||
810 | |||
811 | return $result; |
||
812 | } |
||
813 | |||
814 | /** |
||
815 | * Add heading slash to given slug |
||
816 | * |
||
817 | * @param string $slug |
||
818 | * @return string |
||
819 | */ |
||
820 | public function cleanupHeadingSlash(string $slug): string |
||
821 | { |
||
822 | if (mb_substr($slug, 0, 1) !== '/') { |
||
823 | return '/' . $slug; |
||
824 | } else if (mb_substr($slug, 0, 2) === '//') { |
||
825 | return mb_substr($slug, 1, mb_strlen($slug) - 1); |
||
826 | } |
||
827 | |||
828 | return $slug; |
||
829 | } |
||
830 | |||
831 | /** |
||
832 | * Add heading slash to given slug |
||
833 | * |
||
834 | * @param string $slug |
||
835 | * @return string |
||
836 | */ |
||
837 | public function addHeadingSlash(string $slug): string |
||
838 | { |
||
839 | if (mb_substr($slug, 0, 1) === '/') { |
||
840 | return $slug; |
||
841 | } |
||
842 | |||
843 | return '/' . $slug; |
||
844 | } |
||
845 | |||
846 | /** |
||
847 | * Remove heading slash from given slug |
||
848 | * |
||
849 | * @param string $slug |
||
850 | * @return string |
||
851 | */ |
||
852 | public function removeHeadingSlash(string $slug): string |
||
853 | { |
||
854 | if (mb_substr($slug, 0, 1) !== '/') { |
||
855 | return $slug; |
||
856 | } |
||
857 | |||
858 | return mb_substr($slug, 1, mb_strlen($slug) - 1); |
||
859 | } |
||
860 | |||
861 | /** |
||
862 | * Retrieve the site by given UID |
||
863 | * |
||
864 | * @param int $pageUid |
||
865 | * @return SiteInterface |
||
866 | */ |
||
867 | public function findSiteByUid(int $pageUid): SiteInterface |
||
875 | } |
||
876 | } |
||
877 | |||
878 | /** |
||
879 | * @param Site $site |
||
880 | * @return PageSlugCandidateProvider |
||
881 | */ |
||
882 | public function getSlugCandidateProvider(Site $site): PageSlugCandidateProvider |
||
883 | { |
||
884 | $context = GeneralUtility::makeInstance(Context::class); |
||
885 | return GeneralUtility::makeInstance( |
||
886 | PageSlugCandidateProvider::class, |
||
887 | $context, |
||
888 | $site, |
||
889 | null |
||
890 | ); |
||
891 | } |
||
892 | |||
893 | /** |
||
894 | * Convert the base string into a URI object |
||
895 | * |
||
896 | * @param string $base |
||
897 | * @return UriInterface|null |
||
898 | */ |
||
899 | public function convertStringIntoUri(string $base): ?UriInterface |
||
900 | { |
||
901 | try { |
||
902 | /* @var Uri $uri */ |
||
903 | $uri = GeneralUtility::makeInstance( |
||
904 | Uri::class, |
||
905 | $base |
||
906 | ); |
||
907 | |||
908 | return $uri; |
||
909 | } catch (\InvalidArgumentException $argumentException) { |
||
910 | return null; |
||
911 | } |
||
912 | } |
||
913 | |||
914 | /** |
||
915 | * In order to search for a path, a possible language prefix need to remove |
||
916 | * |
||
917 | * @param SiteLanguage $language |
||
918 | * @param string $path |
||
919 | * @return string |
||
920 | */ |
||
921 | public function stripLanguagePrefixFromPath(SiteLanguage $language, string $path): string |
||
922 | { |
||
923 | if ($language->getBase()->getPath() === '/') { |
||
924 | return $path; |
||
925 | } |
||
926 | |||
927 | $pathLength = mb_strlen($language->getBase()->getPath()); |
||
928 | |||
929 | $path = mb_substr($path, $pathLength, mb_strlen($path) - $pathLength); |
||
930 | if (mb_substr($path, 0, 1) !== '/') { |
||
931 | $path = '/' . $path; |
||
932 | } |
||
933 | |||
934 | return $path; |
||
935 | } |
||
936 | |||
937 | /** |
||
938 | * Enrich the current query Params with data from path information |
||
939 | * |
||
940 | * @param ServerRequestInterface $request |
||
941 | * @param array $arguments |
||
942 | * @param array $parameters |
||
943 | * @return ServerRequestInterface |
||
944 | */ |
||
945 | public function addPathArgumentsToQuery( |
||
971 | } |
||
972 | |||
973 | /** |
||
974 | * Check if given argument is a mapping argument |
||
975 | * |
||
976 | * @param string $facetName |
||
977 | * @return bool |
||
978 | */ |
||
979 | public function isMappingArgument(string $facetName): bool |
||
987 | } |
||
988 | |||
989 | /** |
||
990 | * Check if given facet type is an path argument |
||
991 | * |
||
992 | * @param string $facetName |
||
993 | * @return bool |
||
994 | */ |
||
995 | public function isPathArgument(string $facetName): bool |
||
996 | { |
||
997 | return isset($this->pathArguments[$facetName]); |
||
998 | } |
||
999 | |||
1000 | /** |
||
1001 | * @param string $variable |
||
1002 | * @return string |
||
1003 | */ |
||
1004 | public function reviewVariable(string $variable): string |
||
1005 | { |
||
1006 | if (!$this->containsFacetAndValueSeparator((string)$variable)) { |
||
1007 | return $variable; |
||
1008 | } |
||
1009 | |||
1010 | $separator = $this->detectFacetAndValueSeparator((string)$variable); |
||
1011 | [$type, $value] = explode($separator, $variable, 2); |
||
1012 | |||
1013 | return $this->isMappingArgument($type) ? $value : $variable; |
||
1014 | } |
||
1015 | |||
1016 | /** |
||
1017 | * Remove type prefix from filter |
||
1018 | * |
||
1019 | * @param array $variables |
||
1020 | * @return array |
||
1021 | */ |
||
1022 | public function reviseFilterVariables(array $variables): array |
||
1023 | { |
||
1024 | $newVariables = []; |
||
1025 | foreach ($variables as $key => $value) { |
||
1026 | $matches = []; |
||
1027 | if (!preg_match('/###' . $this->getPluginNamespace() . ':filter:\d+:(.+?)###/', $key, $matches)) { |
||
1028 | $newVariables[$key] = $value; |
||
1029 | continue; |
||
1030 | } |
||
1031 | if (!$this->isMappingArgument($matches[1]) && !$this->isPathArgument($matches[1])) { |
||
1032 | $newVariables[$key] = $value; |
||
1033 | continue; |
||
1034 | } |
||
1035 | $separator = $this->detectFacetAndValueSeparator((string)$value); |
||
1036 | $parts = explode($separator, $value); |
||
1037 | |||
1038 | do { |
||
1039 | if ($parts[0] === $matches[1]) { |
||
1040 | array_shift($parts); |
||
1041 | } |
||
1042 | } while ($parts[0] === $matches[1]); |
||
1043 | |||
1044 | $newVariables[$key] = implode($separator, $parts); |
||
1045 | } |
||
1046 | |||
1047 | return $newVariables; |
||
1048 | } |
||
1049 | |||
1050 | /** |
||
1051 | * Converts path segment information into query parameters |
||
1052 | * |
||
1053 | * Example: |
||
1054 | * /products/household |
||
1055 | * |
||
1056 | * tx_solr: |
||
1057 | * filter: |
||
1058 | * - type:household |
||
1059 | * |
||
1060 | * @param array $queryParams |
||
1061 | * @param string $fieldName |
||
1062 | * @param array $parameters |
||
1063 | * @param array $pathElements |
||
1064 | * @return array |
||
1065 | */ |
||
1066 | protected function processUriPathArgument( |
||
1067 | array $queryParams, |
||
1068 | string $fieldName, |
||
1069 | array $parameters, |
||
1070 | array $pathElements |
||
1071 | ): array { |
||
1072 | $queryKey = array_shift($pathElements); |
||
1073 | $queryKey = (string)$queryKey; |
||
1074 | |||
1075 | $tmpQueryKey = $queryKey; |
||
1076 | if (strpos($queryKey, '-') !== false) { |
||
1077 | [$tmpQueryKey, $filterName] = explode('-', $tmpQueryKey, 2); |
||
1078 | } |
||
1079 | if (!isset($queryParams[$tmpQueryKey]) || $queryParams[$tmpQueryKey] === null) { |
||
1080 | $queryParams[$tmpQueryKey] = []; |
||
1081 | } |
||
1082 | |||
1083 | if (strpos($queryKey, '-') !== false) { |
||
1084 | [$queryKey, $filterName] = explode('-', $queryKey, 2); |
||
1085 | // explode multiple values |
||
1086 | $values = $this->pathFacetStringToArray($parameters[$fieldName], false); |
||
1087 | sort($values); |
||
1088 | |||
1089 | // @TODO: Support URL data bag |
||
1090 | foreach ($values as $value) { |
||
1091 | $value = $this->urlFacetPathService->applyCharacterMap($value); |
||
1092 | $queryParams[$queryKey][] = $filterName . ':' . $value; |
||
1093 | } |
||
1094 | } else { |
||
1095 | $queryParams[$queryKey] = $this->processUriPathArgument( |
||
1096 | $queryParams[$queryKey], |
||
1097 | $fieldName, |
||
1098 | $parameters, |
||
1099 | $pathElements |
||
1100 | ); |
||
1101 | } |
||
1102 | |||
1103 | return $queryParams; |
||
1104 | } |
||
1105 | |||
1106 | /** |
||
1107 | * Return site matcher |
||
1108 | * |
||
1109 | * @return SiteMatcher |
||
1110 | */ |
||
1111 | public function getSiteMatcher(): SiteMatcher |
||
1112 | { |
||
1113 | return GeneralUtility::makeInstance(SiteMatcher::class, $this->getSiteFinder()); |
||
1114 | } |
||
1115 | |||
1116 | /** |
||
1117 | * Returns the site finder |
||
1118 | * |
||
1119 | * @return SiteFinder|null |
||
1120 | */ |
||
1121 | protected function getSiteFinder(): ?SiteFinder |
||
1124 | } |
||
1125 | } |
||
1126 |
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: