Passed
Push — release-11.1.x ( 058dc3...7cde52 )
by Rafael
21:11
created

SearchUriBuilder::getNewSearchUri()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 20
ccs 0
cts 16
cp 0
rs 9.8666
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
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 10
    public function injectUriBuilder(UriBuilder $uriBuilder)
84
    {
85 10
        $this->uriBuilder = $uriBuilder;
86 10
    }
87
88
    /**
89
     * @param RoutingService $routingService
90
     */
91 10
    public function injectRoutingService(RoutingService $routingService)
92
    {
93 10
        $this->routingService = $routingService;
94 10
    }
95
96
    /**
97
     * @param EventDispatcherInterface $eventDispatcher
98
     */
99 10
    public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher)
100
    {
101 10
        $this->eventDispatcher = $eventDispatcher;
102 10
    }
103
104
    /**
105
     * @param SearchRequest $previousSearchRequest
106
     * @param $facetName
107
     * @param $facetValue
108
     * @return string
109
     */
110 4
    public function getAddFacetValueUri(SearchRequest $previousSearchRequest, $facetName, $facetValue): string
111
    {
112
        $persistentAndFacetArguments = $previousSearchRequest
113 4
            ->getCopyForSubRequest()->removeAllGroupItemPages()->addFacetValue($facetName, $facetValue)
114 4
            ->getAsArray();
115
116 4
        $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
117 4
        $additionalArguments = is_array($additionalArguments) ? $additionalArguments : [];
0 ignored issues
show
introduced by
The condition is_array($additionalArguments) is always true.
Loading history...
118
119 4
        $arguments = $persistentAndFacetArguments + $additionalArguments;
120
121 4
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments['tx_solr']['filter']);
122
123 4
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
124 4
        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
    public function getSetFacetValueUri(SearchRequest $previousSearchRequest, $facetName, $facetValue): string
136
    {
137
        $previousSearchRequest = $previousSearchRequest
138
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacetValuesByName($facetName);
139
140
        return $this->getAddFacetValueUri($previousSearchRequest, $facetName, $facetValue);
141
    }
142
143
    /**
144
     * @param SearchRequest $previousSearchRequest
145
     * @param $facetName
146
     * @param $facetValue
147
     * @return string
148
     */
149 1
    public function getRemoveFacetValueUri(SearchRequest $previousSearchRequest, $facetName, $facetValue): string
150
    {
151
        $persistentAndFacetArguments = $previousSearchRequest
152 1
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeFacetValue($facetName, $facetValue)
153 1
            ->getAsArray();
154
155 1
        $additionalArguments = [];
156 1
        if ($previousSearchRequest->getContextTypoScriptConfiguration()->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()) {
157
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
158
        }
159 1
        $arguments = $persistentAndFacetArguments + $additionalArguments;
160
161 1
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments['tx_solr']['filter']);
162
163 1
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
164 1
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
165
    }
166
167
    /**
168
     * @param SearchRequest $previousSearchRequest
169
     * @param $facetName
170
     * @return string
171
     */
172 1
    public function getRemoveFacetUri(SearchRequest $previousSearchRequest, $facetName): string
173
    {
174
        $persistentAndFacetArguments = $previousSearchRequest
175 1
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacetValuesByName($facetName)
176 1
            ->getAsArray();
177
178 1
        $additionalArguments = [];
179 1
        if ($previousSearchRequest->getContextTypoScriptConfiguration()->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()) {
180
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
181
        }
182
183 1
        $arguments = $persistentAndFacetArguments + $additionalArguments;
184
185 1
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments['tx_solr']['filter']);
186
187 1
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
188 1
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
189
    }
190
191
    /**
192
     * @param SearchRequest $previousSearchRequest
193
     * @return string
194
     */
195
    public function getRemoveAllFacetsUri(SearchRequest $previousSearchRequest): string
196
    {
197
        $persistentAndFacetArguments = $previousSearchRequest
198
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacets()
199
            ->getAsArray();
200
201
        $additionalArguments = [];
202
        if ($previousSearchRequest->getContextTypoScriptConfiguration()->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()) {
203
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
204
        }
205
206
        $arguments = $persistentAndFacetArguments + $additionalArguments;
207
208
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments['tx_solr']['filter']);
209
210
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
211
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
212
    }
213
214
    /**
215
     * @param SearchRequest $previousSearchRequest
216
     * @param $page
217
     * @return string
218
     */
219 2
    public function getResultPageUri(SearchRequest $previousSearchRequest, $page): string
220
    {
221
        $persistentAndFacetArguments = $previousSearchRequest
222 2
            ->getCopyForSubRequest()->setPage($page)
223 2
            ->getAsArray();
224
225 2
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
226 2
        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 1
    public function getResultGroupItemPageUri(SearchRequest $previousSearchRequest, GroupItem $groupItem, int $page): string
236
    {
237
        $persistentAndFacetArguments = $previousSearchRequest
238 1
            ->getCopyForSubRequest()->setGroupItemPage($groupItem->getGroup()->getGroupName(), $groupItem->getGroupValue(), $page)
239 1
            ->getAsArray();
240 1
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
241 1
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
242
    }
243
    /**
244
     * @param SearchRequest $previousSearchRequest
245
     * @param $queryString
246
     * @return string
247
     */
248
    public function getNewSearchUri(SearchRequest $previousSearchRequest, $queryString): string
249
    {
250
        /** @var $request SearchRequest */
251
        $contextConfiguration = $previousSearchRequest->getContextTypoScriptConfiguration();
252
        $contextSystemLanguage = $previousSearchRequest->getContextSystemLanguageUid();
253
        $contextPageUid = $previousSearchRequest->getContextPageUid();
254
255
        $request = GeneralUtility::makeInstance(
256
            SearchRequest::class,
257
            [],
258
            /** @scrutinizer ignore-type */ $contextPageUid,
259
            /** @scrutinizer ignore-type */ $contextSystemLanguage,
260
            /** @scrutinizer ignore-type */ $contextConfiguration
261
        );
262
        $arguments = $request->setRawQueryString($queryString)->getAsArray();
263
264
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments['tx_solr']['filter']);
265
266
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
267
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
268
    }
269
270
    /**
271
     * @param SearchRequest $previousSearchRequest
272
     * @param $sortingName
273
     * @param $sortingDirection
274
     * @return string
275
     */
276 1
    public function getSetSortingUri(SearchRequest $previousSearchRequest, $sortingName, $sortingDirection): string
277
    {
278
        $persistentAndFacetArguments = $previousSearchRequest
279 1
            ->getCopyForSubRequest()->setSorting($sortingName, $sortingDirection)
280 1
            ->getAsArray();
281
282 1
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
283 1
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
284
    }
285
286
    /**
287
     * @param SearchRequest $previousSearchRequest
288
     * @return string
289
     */
290
    public function getRemoveSortingUri(SearchRequest $previousSearchRequest): string
291
    {
292
        $persistentAndFacetArguments = $previousSearchRequest
293
            ->getCopyForSubRequest()->removeSorting()
294
            ->getAsArray();
295
296
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
297
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
298
    }
299
300
    /**
301
     * @param SearchRequest $previousSearchRequest
302
     * @return string
303
     */
304
    public function getCurrentSearchUri(SearchRequest $previousSearchRequest): string
305
    {
306
        $persistentAndFacetArguments = $previousSearchRequest
307
            ->getCopyForSubRequest()
308
            ->getAsArray();
309
310
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
311
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
312
    }
313
314
    /**
315
     * @param SearchRequest $request
316
     * @return array
317
     */
318 4
    protected function getAdditionalArgumentsFromRequestConfiguration(SearchRequest $request): array
319
    {
320 4
        if ($request->getContextTypoScriptConfiguration() == null) {
321
            return [];
322
        }
323
324 4
        $reQuestId = $request->getId();
325 4
        if (isset(self::$additionalArgumentsCache[$reQuestId])) {
326 1
            return self::$additionalArgumentsCache[$reQuestId];
327
        }
328
329 4
        self::$additionalArgumentsCache[$reQuestId] = $request->getContextTypoScriptConfiguration()
330 4
            ->getSearchFacetingFacetLinkUrlParametersAsArray();
331
332 4
        return self::$additionalArgumentsCache[$reQuestId];
333
    }
334
335
    /**
336
     * @param SearchRequest $request
337
     * @return int|null
338
     */
339 10
    protected function getTargetPageUidFromRequestConfiguration(SearchRequest $request): ?int
340
    {
341 10
        if ($request->getContextTypoScriptConfiguration() == null) {
342
            return null;
343
        }
344
345 10
        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 10
    protected function buildLinkWithInMemoryCache(?int $pageUid, array $arguments): string
356
    {
357 10
        $values = [];
358 10
        $structure = $arguments;
359 10
        $this->getSubstitution($structure, $values);
360 10
        $hash = md5($pageUid . json_encode($structure));
361 10
        if (isset(self::$preCompiledLinks[$hash])) {
362 1
            self::$hitCount++;
363 1
            $uriCacheTemplate = self::$preCompiledLinks[$hash];
364
        } else {
365 10
            self::$missCount++;
366 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

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

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 10
            $urlHelper = GeneralUtility::makeInstance(UrlHelper::class, $uriCacheTemplate);
371 10
            self::$preCompiledLinks[$hash] = (string)$urlHelper;
372
        }
373
374
        $keys = array_map(function($value) {
375 8
            return urlencode($value);
376 10
        }, array_keys($values));
377
        $values = array_map(function($value) {
378 8
            return urlencode($value);
379 10
        }, $values);
380
381 10
        $routingConfigurations = $this->routingService
382 10
            ->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 10
        $enhancedRouting = count($routingConfigurations) > 0;
384 10
        $this->routingService->reset();
385 10
        if ($enhancedRouting && is_array($routingConfigurations[0])) {
386
            $this->routingService->fromRoutingConfiguration($routingConfigurations[0]);
387
        }
388
389
        /* @var Uri $uri */
390 10
        $uri = GeneralUtility::makeInstance(
391 10
            Uri::class,
392 10
            $uriCacheTemplate
393
        );
394
395 10
        $urlEvent = new BeforeReplaceVariableInCachedUrlEvent($uri, $enhancedRouting);
396
        /* @var BeforeReplaceVariableInCachedUrlEvent $urlEvent */
397 10
        $urlEvent = $this->eventDispatcher->dispatch($urlEvent);
398 10
        $uriCacheTemplate = (string)$urlEvent->getUri();
399
400 10
        $variableEvent = new BeforeProcessCachedVariablesEvent(
401 10
            $uri,
402 10
            $routingConfigurations,
403 10
            $keys,
404 10
            $values
405
        );
406 10
        $this->eventDispatcher->dispatch($variableEvent);
407
408 10
        $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 8
            if (strpos($value, '###') === false) {
413
                return $value;
414
            }
415 8
            return urlencode($value);
416 10
        }, array_keys($values));
417
418 10
        $uri = str_replace($keys, $values, $uriCacheTemplate);
419 10
        $uri = GeneralUtility::makeInstance(
420 10
            Uri::class,
421 10
            $uri
422
        );
423 10
        $uriEvent = new PostProcessUriEvent($uri, $routingConfigurations);
424 10
        $this->eventDispatcher->dispatch($uriEvent);
425 10
        $uri = $uriEvent->getUri();
426 10
        return (string)$uri;
427
    }
428
429
    /**
430
     * Flushes the internal in memory cache.
431
     *
432
     * @return void
433
     */
434 10
    public function flushInMemoryCache()
435
    {
436 10
        self::$preCompiledLinks = [];
437 10
    }
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 10
    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 10
        $filter = false;
478 10
        if (count($branch) > 0 && $branch[count($branch) - 1] === 'filter') {
479 8
            $filter = true;
480
        }
481 10
        foreach ($structure as $key => &$value) {
482 10
            $branch[] = $key;
483 10
            if (is_array($value)) {
484 10
                $this->getSubstitution($value, $values, $branch);
485
            } else {
486 8
                if ($filter) {
487 6
                    [$facetType, $facetValue] = explode(':', $value);
488 6
                    $branch[] = $facetType;
489
                }
490 8
                $path = '###' . implode(':', $branch) . '###';
491 8
                $values[$path] = $value;
492 8
                $structure[$key] = $path;
493 8
                if ($filter) {
494 6
                    array_pop($branch);
495
                }
496
            }
497 10
            array_pop($branch);
498
        }
499 10
    }
500
501
    /**
502
     * Sorts filter arguments if enabled.
503
     *
504
     *
505
     * @param SearchRequest $searchRequest
506
     * @param array|null $filterArguments
507
     */
508 6
    protected function sortFilterParametersIfNecessary(SearchRequest $searchRequest, ?array &$filterArguments)
509
    {
510 6
        if (is_array($filterArguments) && !empty($filterArguments) && $searchRequest->isActiveFacetsSorted()) {
511
            ParameterSortingUtility::sortByType(
512
                $filterArguments,
513
                $searchRequest->getActiveFacetsUrlParameterStyle()
514
            );
515
        }
516 6
    }
517
}
518