Passed
Pull Request — release-11.2.x (#3594)
by Markus
14:43 queued 10:47
created

SearchUriBuilder   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 478
Duplicated Lines 0 %

Test Coverage

Coverage 73.41%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 40
eloc 177
dl 0
loc 478
ccs 127
cts 173
cp 0.7341
rs 9.2
c 1
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getTargetPageUidFromRequestConfiguration() 0 7 2
A getAdditionalArgumentsFromRequestConfiguration() 0 15 3
A getSetSortingUri() 0 8 1
A flushInMemoryCache() 0 3 1
A getRemoveSortingUri() 0 8 1
A injectEventDispatcher() 0 3 1
A getResultPageUri() 0 8 1
A getResultGroupItemPageUri() 0 7 1
A injectUriBuilder() 0 3 1
B buildLinkWithInMemoryCache() 0 72 5
A sortFilterParametersIfNecessary() 0 11 4
A getCurrentSearchUri() 0 8 1
A injectRoutingService() 0 3 1
A getNewSearchUri() 0 23 1
A getSetFacetValueUri() 0 6 1
A getRemoveAllFacetsUri() 0 17 2
A getRemoveFacetValueUri() 0 16 2
A getAddFacetValueUri() 0 15 2
B getSubstitution() 0 28 7
A getRemoveFacetUri() 0 17 2

How to fix   Complexity   

Complex Class

Complex classes like SearchUriBuilder 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 SearchUriBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace ApacheSolrForTypo3\Solr\Domain\Search\Uri;
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Grouping\GroupItem;
19
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
20
use ApacheSolrForTypo3\Solr\Event\Routing\BeforeProcessCachedVariablesEvent;
21
use ApacheSolrForTypo3\Solr\Event\Routing\BeforeReplaceVariableInCachedUrlEvent;
22
use ApacheSolrForTypo3\Solr\Event\Routing\PostProcessUriEvent;
23
use ApacheSolrForTypo3\Solr\Routing\RoutingService;
24
use ApacheSolrForTypo3\Solr\System\Url\UrlHelper;
25
use ApacheSolrForTypo3\Solr\Utility\ParameterSortingUtility;
26
use Psr\EventDispatcher\EventDispatcherInterface;
27
use TYPO3\CMS\Core\Http\Uri;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
30
31
/**
32
 * SearchUriBuilder
33
 *
34
 * Responsibility:
35
 *
36
 * The SearchUriBuilder is responsible to build uris, that are used in the
37
 * searchContext. It can use the previous request with it's persistent
38
 * arguments to build the url for a search sub request.
39
 *
40
 * @author Frans Saris <[email protected]>
41
 * @author Timo Hund <[email protected]>
42
 */
43
class SearchUriBuilder
44
{
45
46
    /**
47
     * @var UriBuilder
48
     */
49
    protected $uriBuilder;
50
51
    /**
52
     * @var array
53
     */
54
    protected static $preCompiledLinks = [];
55
56
    /**
57
     * @var int
58
     */
59
    protected static $hitCount;
60
61
    /**
62
     * @var int
63
     */
64
    protected static $missCount;
65
66
    /**
67
     * @var array
68
     */
69
    protected static $additionalArgumentsCache = [];
70
71
    /**
72
     * @var EventDispatcherInterface
73
     */
74
    protected $eventDispatcher;
75
76
    /**
77
     * @var RoutingService
78
     */
79
    protected $routingService;
80
81
    /**
82
     * @param UriBuilder $uriBuilder
83 10
     */
84
    public function injectUriBuilder(UriBuilder $uriBuilder)
85 10
    {
86 10
        $this->uriBuilder = $uriBuilder;
87
    }
88
89
    /**
90
     * @param RoutingService $routingService
91 10
     */
92
    public function injectRoutingService(RoutingService $routingService)
93 10
    {
94 10
        $this->routingService = $routingService;
95
    }
96
97
    /**
98
     * @param EventDispatcherInterface $eventDispatcher
99 10
     */
100
    public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher)
101 10
    {
102 10
        $this->eventDispatcher = $eventDispatcher;
103
    }
104
105
    /**
106
     * @param SearchRequest $previousSearchRequest
107
     * @param $facetName
108
     * @param $facetValue
109
     * @return string
110 4
     */
111
    public function getAddFacetValueUri(SearchRequest $previousSearchRequest, $facetName, $facetValue): string
112
    {
113 4
        $persistentAndFacetArguments = $previousSearchRequest
114 4
            ->getCopyForSubRequest()->removeAllGroupItemPages()->addFacetValue($facetName, $facetValue)
115
            ->getAsArray();
116 4
117 4
        $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
118
        $additionalArguments = is_array($additionalArguments) ? $additionalArguments : [];
0 ignored issues
show
introduced by
The condition is_array($additionalArguments) is always true.
Loading history...
119 4
120
        $arguments = $persistentAndFacetArguments + $additionalArguments;
121 4
122
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
123 4
124 4
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
125
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
126
    }
127
128
    /**
129
     * Removes all other facet values for this name and only set's the passed value for the facet.
130
     *
131
     * @param SearchRequest $previousSearchRequest
132
     * @param $facetName
133
     * @param $facetValue
134
     * @return string
135
     */
136
    public function getSetFacetValueUri(SearchRequest $previousSearchRequest, $facetName, $facetValue): string
137
    {
138
        $previousSearchRequest = $previousSearchRequest
139
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacetValuesByName($facetName);
140
141
        return $this->getAddFacetValueUri($previousSearchRequest, $facetName, $facetValue);
142
    }
143
144
    /**
145
     * @param SearchRequest $previousSearchRequest
146
     * @param $facetName
147
     * @param $facetValue
148
     * @return string
149 1
     */
150
    public function getRemoveFacetValueUri(SearchRequest $previousSearchRequest, $facetName, $facetValue): string
151
    {
152 1
        $persistentAndFacetArguments = $previousSearchRequest
153 1
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeFacetValue($facetName, $facetValue)
154
            ->getAsArray();
155 1
156 1
        $additionalArguments = [];
157
        if ($previousSearchRequest->getContextTypoScriptConfiguration()->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()) {
158
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
159 1
        }
160
        $arguments = $persistentAndFacetArguments + $additionalArguments;
161 1
162
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
163 1
164 1
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
165
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
166
    }
167
168
    /**
169
     * @param SearchRequest $previousSearchRequest
170
     * @param $facetName
171
     * @return string
172 1
     */
173
    public function getRemoveFacetUri(SearchRequest $previousSearchRequest, $facetName): string
174
    {
175 1
        $persistentAndFacetArguments = $previousSearchRequest
176 1
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacetValuesByName($facetName)
177
            ->getAsArray();
178 1
179 1
        $additionalArguments = [];
180
        if ($previousSearchRequest->getContextTypoScriptConfiguration()->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()) {
181
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
182
        }
183 1
184
        $arguments = $persistentAndFacetArguments + $additionalArguments;
185 1
186
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
187 1
188 1
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
189
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
190
    }
191
192
    /**
193
     * @param SearchRequest $previousSearchRequest
194
     * @return string
195
     */
196
    public function getRemoveAllFacetsUri(SearchRequest $previousSearchRequest): string
197
    {
198
        $persistentAndFacetArguments = $previousSearchRequest
199
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacets()
200
            ->getAsArray();
201
202
        $additionalArguments = [];
203
        if ($previousSearchRequest->getContextTypoScriptConfiguration()->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()) {
204
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
205
        }
206
207
        $arguments = $persistentAndFacetArguments + $additionalArguments;
208
209
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
210
211
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
212
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
213
    }
214
215
    /**
216
     * @param SearchRequest $previousSearchRequest
217
     * @param $page
218
     * @return string
219 2
     */
220
    public function getResultPageUri(SearchRequest $previousSearchRequest, $page): string
221
    {
222 2
        $persistentAndFacetArguments = $previousSearchRequest
223 2
            ->getCopyForSubRequest()->setPage($page)
224
            ->getAsArray();
225 2
226 2
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
227
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
228
    }
229
230
    /**
231
     * @param SearchRequest $previousSearchRequest
232
     * @param GroupItem $groupItem
233
     * @param int $page
234
     * @return string
235 1
     */
236
    public function getResultGroupItemPageUri(SearchRequest $previousSearchRequest, GroupItem $groupItem, int $page): string
237
    {
238 1
        $persistentAndFacetArguments = $previousSearchRequest
239 1
            ->getCopyForSubRequest()->setGroupItemPage($groupItem->getGroup()->getGroupName(), $groupItem->getGroupValue(), $page)
240 1
            ->getAsArray();
241 1
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
242
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
243
    }
244
    /**
245
     * @param SearchRequest $previousSearchRequest
246
     * @param $queryString
247
     * @return string
248
     */
249
    public function getNewSearchUri(SearchRequest $previousSearchRequest, $queryString): string
250
    {
251
        /** @var $request SearchRequest */
252
        $contextConfiguration = $previousSearchRequest->getContextTypoScriptConfiguration();
253
        $contextSystemLanguage = $previousSearchRequest->getContextSystemLanguageUid();
254
        $contextPageUid = $previousSearchRequest->getContextPageUid();
255
256
        $request = GeneralUtility::makeInstance(
257
            SearchRequest::class,
258
            [],
259
            /** @scrutinizer ignore-type */
260
            $contextPageUid,
261
            /** @scrutinizer ignore-type */
262
            $contextSystemLanguage,
263
            /** @scrutinizer ignore-type */
264
            $contextConfiguration
265
        );
266
        $arguments = $request->setRawQueryString($queryString)->getAsArray();
267
268
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
269
270
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
271
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
272
    }
273
274
    /**
275
     * @param SearchRequest $previousSearchRequest
276 1
     * @param $sortingName
277
     * @param $sortingDirection
278
     * @return string
279 1
     */
280 1
    public function getSetSortingUri(SearchRequest $previousSearchRequest, $sortingName, $sortingDirection): string
281
    {
282 1
        $persistentAndFacetArguments = $previousSearchRequest
283 1
            ->getCopyForSubRequest()->setSorting($sortingName, $sortingDirection)
284
            ->getAsArray();
285
286
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
287
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
288
    }
289
290
    /**
291
     * @param SearchRequest $previousSearchRequest
292
     * @return string
293
     */
294
    public function getRemoveSortingUri(SearchRequest $previousSearchRequest): string
295
    {
296
        $persistentAndFacetArguments = $previousSearchRequest
297
            ->getCopyForSubRequest()->removeSorting()
298
            ->getAsArray();
299
300
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
301
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
302
    }
303
304
    /**
305
     * @param SearchRequest $previousSearchRequest
306
     * @return string
307
     */
308
    public function getCurrentSearchUri(SearchRequest $previousSearchRequest): string
309
    {
310
        $persistentAndFacetArguments = $previousSearchRequest
311
            ->getCopyForSubRequest()
312
            ->getAsArray();
313
314
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
315
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
316
    }
317
318 4
    /**
319
     * @param SearchRequest $request
320 4
     * @return array
321
     */
322
    protected function getAdditionalArgumentsFromRequestConfiguration(SearchRequest $request): array
323
    {
324 4
        if ($request->getContextTypoScriptConfiguration() == null) {
325 4
            return [];
326 1
        }
327
328
        $reQuestId = $request->getId();
329 4
        if (isset(self::$additionalArgumentsCache[$reQuestId])) {
330 4
            return self::$additionalArgumentsCache[$reQuestId];
331
        }
332 4
333
        self::$additionalArgumentsCache[$reQuestId] = $request->getContextTypoScriptConfiguration()
334
            ->getSearchFacetingFacetLinkUrlParametersAsArray();
335
336
        return self::$additionalArgumentsCache[$reQuestId];
337
    }
338
339 10
    /**
340
     * @param SearchRequest $request
341 10
     * @return int|null
342
     */
343
    protected function getTargetPageUidFromRequestConfiguration(SearchRequest $request): ?int
344
    {
345 10
        if ($request->getContextTypoScriptConfiguration() == null) {
346
            return null;
347
        }
348
349
        return $request->getContextTypoScriptConfiguration()->getSearchTargetPage();
350
    }
351
352
    /**
353
     * Build the link with an i memory cache that reduces the amount of required typolink calls.
354
     *
355 10
     * @param int|null $pageUid
356
     * @param array $arguments
357 10
     * @return string
358 10
     */
359 10
    protected function buildLinkWithInMemoryCache(?int $pageUid, array $arguments): string
360 10
    {
361 10
        $values = [];
362 1
        $structure = $arguments;
363 1
        $this->getSubstitution($structure, $values);
364
        $hash = md5($pageUid . json_encode($structure));
365 10
        if (isset(self::$preCompiledLinks[$hash])) {
366 10
            self::$hitCount++;
367 10
            $uriCacheTemplate = self::$preCompiledLinks[$hash];
368
        } else {
369
            self::$missCount++;
370 10
            $this->uriBuilder->reset()->setTargetPageUid($pageUid);
0 ignored issues
show
Bug introduced by
It seems like $pageUid can also be of type null; however, parameter $targetPageUid of TYPO3\CMS\Extbase\Mvc\We...der::setTargetPageUid() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

370
            $this->uriBuilder->reset()->setTargetPageUid(/** @scrutinizer ignore-type */ $pageUid);
Loading history...
371 10
            $uriCacheTemplate = $this->uriBuilder->setArguments($structure)->setUseCacheHash(false)->build();
0 ignored issues
show
Unused Code introduced by
The call to TYPO3\CMS\Extbase\Mvc\We...lder::setUseCacheHash() has too many arguments starting with false. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

371
            $uriCacheTemplate = $this->uriBuilder->setArguments($structure)->/** @scrutinizer ignore-call */ setUseCacheHash(false)->build();

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
372
373
            /* @var UrlHelper $urlHelper */
374
            $urlHelper = GeneralUtility::makeInstance(UrlHelper::class, $uriCacheTemplate);
375 8
            self::$preCompiledLinks[$hash] = (string)$urlHelper;
376 10
        }
377
378 8
        $keys = array_map(function ($value) {
379 10
            return urlencode($value);
380
        }, array_keys($values));
381 10
        $values = array_map(function ($value) {
382 10
            return urlencode($value);
383 10
        }, $values);
384 10
385 10
        $routingConfigurations = $this->routingService
386
            ->fetchEnhancerByPageUid($pageUid);
0 ignored issues
show
Bug introduced by
It seems like $pageUid can also be of type null; however, parameter $pageUid of ApacheSolrForTypo3\Solr\...etchEnhancerByPageUid() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

386
            ->fetchEnhancerByPageUid(/** @scrutinizer ignore-type */ $pageUid);
Loading history...
387
        $enhancedRouting = count($routingConfigurations) > 0;
388
        $this->routingService->reset();
389
        if ($enhancedRouting && is_array($routingConfigurations[0])) {
390 10
            $this->routingService->fromRoutingConfiguration($routingConfigurations[0]);
391
        }
392 10
393
        /* @var Uri $uri */
394
        $uri = GeneralUtility::makeInstance(
395 10
            Uri::class,
396
            $uriCacheTemplate
397 10
        );
398 10
399
        $urlEvent = new BeforeReplaceVariableInCachedUrlEvent($uri, $enhancedRouting);
400 10
        /* @var BeforeReplaceVariableInCachedUrlEvent $urlEvent */
401
        $urlEvent = $this->eventDispatcher->dispatch($urlEvent);
402
        $uriCacheTemplate = (string)$urlEvent->getUri();
403
404 10
        $variableEvent = new BeforeProcessCachedVariablesEvent(
405
            $uri,
406 10
            $routingConfigurations,
407
            $keys,
408 10
            $values
409
        );
410
        $this->eventDispatcher->dispatch($variableEvent);
411
412 8
        $values = $variableEvent->getVariableValues();
413
        // Take care that everything is urlencoded!
414
        $keys = array_map(function ($value) {
415 8
            // @TODO: With only PHP 8 support, replace this with str_contains()
416 10
            if (strpos($value, '###') === false) {
417
                return $value;
418 10
            }
419 10
            return urlencode($value);
420
        }, array_keys($values));
421 10
422
        $uri = str_replace($keys, $values, $uriCacheTemplate);
423 10
        $uri = GeneralUtility::makeInstance(
424 10
            Uri::class,
425 10
            $uri
426 10
        );
427
        $uriEvent = new PostProcessUriEvent($uri, $routingConfigurations);
428
        $this->eventDispatcher->dispatch($uriEvent);
429
        $uri = $uriEvent->getUri();
430
        return (string)$uri;
431
    }
432
433
    /**
434 10
     * Flushes the internal in memory cache.
435
     */
436 10
    public function flushInMemoryCache()
437 10
    {
438
        self::$preCompiledLinks = [];
439
    }
440
441
    /**
442
     * This method is used to build two arrays from a nested array. The first one represents the structure.
443
     * In this structure the values are replaced with the pass to the value. At the same time the values get collected
444
     * in the $values array, with the path as key. This can be used to build a comparable hash from the arguments
445
     * in order to reduce the amount of typolink calls
446
     *
447
     *
448
     * Example input
449
     *
450
     * $data = [
451
     *  'foo' => [
452
     *      'bar' => 111
453
     *   ]
454
     * ]
455
     *
456
     * will return:
457
     *
458
     * $structure = [
459
     *  'foo' => [
460
     *      'bar' => '###foo:bar###'
461
     *   ]
462
     * ]
463
     *
464
     * $values = [
465
     *  '###foo:bar###' => 111
466
     * ]
467
     *
468
     * @param array $structure
469
     * @param array $values
470 10
     * @param array $branch
471
     */
472
    protected function getSubstitution(array &$structure, array &$values, array $branch = []): void
473
    {
474
        /*
475
         * Adds information about the filter facet to the placeholder.
476
         *
477 10
         * This feature allows to handle even placeholder in RouteEnhancer
478 10
         */
479 8
        $filter = false;
480
        if (count($branch) > 0 && $branch[count($branch) - 1] === 'filter') {
481 10
            $filter = true;
482 10
        }
483 10
        foreach ($structure as $key => &$value) {
484 10
            $branch[] = $key;
485
            if (is_array($value)) {
486 8
                $this->getSubstitution($value, $values, $branch);
487 6
            } else {
488 6
                if ($filter) {
489
                    [$facetType, $facetValue] = explode(':', $value);
490 8
                    $branch[] = $facetType;
491 8
                }
492 8
                $path = '###' . implode(':', $branch) . '###';
493 8
                $values[$path] = $value;
494 6
                $structure[$key] = $path;
495
                if ($filter) {
496
                    array_pop($branch);
497 10
                }
498
            }
499 10
            array_pop($branch);
500
        }
501
    }
502
503
    /**
504
     * Sorts filter arguments if enabled.
505
     *
506
     *
507
     * @param SearchRequest $searchRequest
508 6
     * @param array $arguments
509
     */
510 6
    protected function sortFilterParametersIfNecessary(SearchRequest $searchRequest, array &$arguments)
511 6
    {
512
        if (!$searchRequest->isActiveFacetsSorted()) {
513
            return;
514
        }
515
516
        $pluginNameSpace = $searchRequest->getContextTypoScriptConfiguration()->getSearchPluginNamespace();
517
        if (!empty($arguments[$pluginNameSpace]['filter']) && is_array($arguments[$pluginNameSpace]['filter'])) {
518
            $arguments[$pluginNameSpace]['filter'] = ParameterSortingUtility::sortByType(
519
                $arguments[$pluginNameSpace]['filter'],
520
                $searchRequest->getActiveFacetsUrlParameterStyle()
521
            );
522
        }
523
    }
524
}
525