Passed
Pull Request — master (#2755)
by Rafael
03:55
created

SearchUriBuilder::getCurrentSearchUri()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1.0233

Importance

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

366
            $this->uriBuilder->reset()->setTargetPageUid(/** @scrutinizer ignore-type */ $pageUid);
Loading history...
367 35
            $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

367
            $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...
368
369
            /* @var UrlHelper $urlHelper */
370 35
            $urlHelper = GeneralUtility::makeInstance(UrlHelper::class, $uriCacheTemplate);
371 35
            self::$preCompiledLinks[$hash] = (string)$urlHelper;
372
        }
373
374
        $keys = array_map(function($value) {
375 35
            return urlencode($value);
376 35
        }, array_keys($values));
377
        $values = array_map(function($value) {
378 35
            return urlencode($value);
379 35
        }, $values);
380
381 35
        $routingConfigurations = $this->routingService
382 35
            ->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

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