Passed
Push — release-11.5.x ( 385fe8...cd49eb )
by Rafael
53:22 queued 14:05
created

SearchUriBuilder::getRemoveSortingUri()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
ccs 0
cts 6
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
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
namespace ApacheSolrForTypo3\Solr\Domain\Search\Uri;
19
20
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Grouping\GroupItem;
21
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
22
use ApacheSolrForTypo3\Solr\Event\Routing\BeforeProcessCachedVariablesEvent;
23
use ApacheSolrForTypo3\Solr\Event\Routing\BeforeReplaceVariableInCachedUrlEvent;
24
use ApacheSolrForTypo3\Solr\Event\Routing\PostProcessUriEvent;
25
use ApacheSolrForTypo3\Solr\Routing\RoutingService;
26
use ApacheSolrForTypo3\Solr\System\Url\UrlHelper;
27
use ApacheSolrForTypo3\Solr\Utility\ParameterSortingUtility;
28
use Psr\EventDispatcher\EventDispatcherInterface;
29
use TYPO3\CMS\Core\Http\Uri;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
32
33
/**
34
 * SearchUriBuilder
35
 *
36
 * Responsibility:
37
 *
38
 * The SearchUriBuilder is responsible to build uris, that are used in the
39
 * searchContext. It can use the previous request with its persistent
40
 * arguments to build the url for a search sub request.
41
 *
42
 * @author Frans Saris <[email protected]>
43
 * @author Timo Hund <[email protected]>
44
 */
45
class SearchUriBuilder
46
{
47
    /**
48
     * @var UriBuilder|null
49
     */
50
    protected ?UriBuilder $uriBuilder = null;
51
52
    /**
53
     * @var array
54
     */
55
    protected static array $preCompiledLinks = [];
56
57
    /**
58
     * @var int
59
     */
60
    protected static int $hitCount = 0;
61
62
    /**
63
     * @var int
64
     */
65
    protected static int $missCount = 0;
66
67
    /**
68
     * @var array
69
     */
70
    protected static array $additionalArgumentsCache = [];
71
72
    /**
73
     * @var EventDispatcherInterface
74
     */
75
    protected EventDispatcherInterface $eventDispatcher;
76
77
    /**
78
     * @var ?RoutingService
79
     */
80
    protected ?RoutingService $routingService = null;
81
82
    /**
83
     * @param UriBuilder $uriBuilder
84
     */
85 36
    public function injectUriBuilder(UriBuilder $uriBuilder)
86
    {
87 36
        $this->uriBuilder = $uriBuilder;
88
    }
89
90
    /**
91
     * @param RoutingService $routingService
92
     */
93 36
    public function injectRoutingService(RoutingService $routingService)
94
    {
95 36
        $this->routingService = $routingService;
96
    }
97
98
    /**
99
     * @param EventDispatcherInterface $eventDispatcher
100
     */
101 36
    public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher)
102
    {
103 36
        $this->eventDispatcher = $eventDispatcher;
104
    }
105
106
    /**
107
     * @param SearchRequest $previousSearchRequest
108
     * @param string $facetName
109
     * @param mixed $facetValue
110
     * @return string
111
     */
112 19
    public function getAddFacetValueUri(SearchRequest $previousSearchRequest, string $facetName, $facetValue): string
113
    {
114 19
        $persistentAndFacetArguments = $previousSearchRequest
115 19
            ->getCopyForSubRequest()->removeAllGroupItemPages()->addFacetValue($facetName, $facetValue)
116 19
            ->getAsArray();
117
118 19
        $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
119
120 19
        $arguments = $persistentAndFacetArguments + $additionalArguments;
121
122 19
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
123
124 19
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
125 19
        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 1
    public function getSetFacetValueUri(SearchRequest $previousSearchRequest, $facetName, $facetValue): string
137
    {
138 1
        $previousSearchRequest = $previousSearchRequest
139 1
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacetValuesByName($facetName);
140
141 1
        return $this->getAddFacetValueUri($previousSearchRequest, $facetName, $facetValue);
142
    }
143
144
    /**
145
     * @param SearchRequest $previousSearchRequest
146
     * @param $facetName
147
     * @param $facetValue
148
     * @return string
149
     */
150 5
    public function getRemoveFacetValueUri(SearchRequest $previousSearchRequest, $facetName, $facetValue): string
151
    {
152 5
        $persistentAndFacetArguments = $previousSearchRequest
153 5
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeFacetValue($facetName, $facetValue)
154 5
            ->getAsArray();
155
156 5
        $additionalArguments = [];
157 5
        if ($previousSearchRequest->getContextTypoScriptConfiguration()->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()) {
158 4
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
159
        }
160 5
        $arguments = $persistentAndFacetArguments + $additionalArguments;
161
162 5
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
163
164 5
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
165 5
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
166
    }
167
168
    /**
169
     * @param SearchRequest $previousSearchRequest
170
     * @param $facetName
171
     * @return string
172
     */
173 1
    public function getRemoveFacetUri(SearchRequest $previousSearchRequest, $facetName): string
174
    {
175 1
        $persistentAndFacetArguments = $previousSearchRequest
176 1
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacetValuesByName($facetName)
177 1
            ->getAsArray();
178
179 1
        $additionalArguments = [];
180 1
        if ($previousSearchRequest->getContextTypoScriptConfiguration()->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()) {
181
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
182
        }
183
184 1
        $arguments = $persistentAndFacetArguments + $additionalArguments;
185
186 1
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
187
188 1
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
189 1
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
190
    }
191
192
    /**
193
     * @param SearchRequest $previousSearchRequest
194
     * @return string
195
     */
196 4
    public function getRemoveAllFacetsUri(SearchRequest $previousSearchRequest): string
197
    {
198 4
        $persistentAndFacetArguments = $previousSearchRequest
199 4
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacets()
200 4
            ->getAsArray();
201
202 4
        $additionalArguments = [];
203 4
        if ($previousSearchRequest->getContextTypoScriptConfiguration()->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()) {
204 4
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
205
        }
206
207 4
        $arguments = $persistentAndFacetArguments + $additionalArguments;
208
209 4
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
210
211 4
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
212 4
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
213
    }
214
215
    /**
216
     * @param SearchRequest $previousSearchRequest
217
     * @param $page
218
     * @return string
219
     */
220 4
    public function getResultPageUri(SearchRequest $previousSearchRequest, $page): string
221
    {
222 4
        $persistentAndFacetArguments = $previousSearchRequest
223 4
            ->getCopyForSubRequest()->setPage($page)
224 4
            ->getAsArray();
225
226 4
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
227 4
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
228
    }
229
230
    /**
231
     * @param SearchRequest $previousSearchRequest
232
     * @param GroupItem $groupItem
233
     * @param int $page
234
     * @return string
235
     */
236 1
    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 1
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
243
    }
244
    /**
245
     * @param SearchRequest $previousSearchRequest
246
     * @param $queryString
247
     * @return string
248
     */
249 1
    public function getNewSearchUri(SearchRequest $previousSearchRequest, $queryString): string
250
    {
251
        /** @var $request SearchRequest */
252 1
        $contextConfiguration = $previousSearchRequest->getContextTypoScriptConfiguration();
253 1
        $contextSystemLanguage = $previousSearchRequest->getContextSystemLanguageUid();
254 1
        $contextPageUid = $previousSearchRequest->getContextPageUid();
255
256 1
        $request = GeneralUtility::makeInstance(
257 1
            SearchRequest::class,
258 1
            [],
259
            /** @scrutinizer ignore-type */
260 1
            $contextPageUid,
261
            /** @scrutinizer ignore-type */
262 1
            $contextSystemLanguage,
263
            /** @scrutinizer ignore-type */
264 1
            $contextConfiguration
265 1
        );
266 1
        $arguments = $request->setRawQueryString($queryString)->getAsArray();
267
268 1
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
269
270 1
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
271 1
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
272
    }
273
274
    /**
275
     * @param SearchRequest $previousSearchRequest
276
     * @param $sortingName
277
     * @param $sortingDirection
278
     * @return string
279
     */
280 1
    public function getSetSortingUri(SearchRequest $previousSearchRequest, $sortingName, $sortingDirection): string
281
    {
282 1
        $persistentAndFacetArguments = $previousSearchRequest
283 1
            ->getCopyForSubRequest()->setSorting($sortingName, $sortingDirection)
284 1
            ->getAsArray();
285
286 1
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
287 1
        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 22
    public function getCurrentSearchUri(SearchRequest $previousSearchRequest): string
309
    {
310 22
        $persistentAndFacetArguments = $previousSearchRequest
311 22
            ->getCopyForSubRequest()
312 22
            ->getAsArray();
313
314 22
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
315 22
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
316
    }
317
318
    /**
319
     * @param SearchRequest $request
320
     * @return array
321
     */
322 20
    protected function getAdditionalArgumentsFromRequestConfiguration(SearchRequest $request): array
323
    {
324 20
        if ($request->getContextTypoScriptConfiguration() == null) {
325
            return [];
326
        }
327
328 20
        $reQuestId = $request->getId();
329 20
        if (isset(self::$additionalArgumentsCache[$reQuestId])) {
330 13
            return self::$additionalArgumentsCache[$reQuestId];
331
        }
332
333 20
        self::$additionalArgumentsCache[$reQuestId] = $request->getContextTypoScriptConfiguration()
334 20
            ->getSearchFacetingFacetLinkUrlParametersAsArray();
335
336 20
        return self::$additionalArgumentsCache[$reQuestId];
337
    }
338
339
    /**
340
     * @param SearchRequest $request
341
     * @return int|null
342
     */
343 34
    protected function getTargetPageUidFromRequestConfiguration(SearchRequest $request): ?int
344
    {
345 34
        if ($request->getContextTypoScriptConfiguration() == null) {
346
            return null;
347
        }
348
349 34
        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
     * @param int|null $pageUid
356
     * @param array $arguments
357
     * @return string
358
     */
359 34
    protected function buildLinkWithInMemoryCache(?int $pageUid, array $arguments): string
360
    {
361 34
        $values = [];
362 34
        $structure = $arguments;
363 34
        $this->getSubstitution($structure, $values);
364 34
        $hash = md5($pageUid . json_encode($structure));
365 34
        if (isset(self::$preCompiledLinks[$hash])) {
366 13
            self::$hitCount++;
367 13
            $uriCacheTemplate = self::$preCompiledLinks[$hash];
368
        } else {
369 34
            self::$missCount++;
370 34
            $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...
Bug introduced by
The method reset() does not exist on null. ( Ignorable by Annotation )

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

370
            $this->uriBuilder->/** @scrutinizer ignore-call */ 
371
                               reset()->setTargetPageUid($pageUid);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
371 34
            $uriCacheTemplate = $this->uriBuilder->setArguments($structure)->build();
372
373
            /* @var UrlHelper $urlHelper */
374 34
            $urlHelper = GeneralUtility::makeInstance(UrlHelper::class, $uriCacheTemplate);
375 34
            self::$preCompiledLinks[$hash] = (string)$urlHelper;
376
        }
377
378 34
        $keys = array_map(function ($value) {
379 32
            return urlencode((string)$value);
380 34
        }, array_keys($values));
381 34
        $values = array_map(function ($value) {
382 32
            return urlencode((string)$value);
383 34
        }, $values);
384
385 34
        $routingConfigurations = $this->routingService
386 34
            ->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...
Bug introduced by
The method fetchEnhancerByPageUid() does not exist on null. ( Ignorable by Annotation )

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

386
        /** @scrutinizer ignore-call */ 
387
        $routingConfigurations = $this->routingService

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
387 34
        $enhancedRouting = count($routingConfigurations) > 0;
388 34
        $this->routingService->reset();
389 34
        if ($enhancedRouting && is_array($routingConfigurations[0] ?? null)) {
390
            $this->routingService->fromRoutingConfiguration($routingConfigurations[0]);
391
        }
392
393
        /* @var Uri $uri */
394 34
        $uri = GeneralUtility::makeInstance(
395 34
            Uri::class,
396 34
            $uriCacheTemplate
397 34
        );
398
399 34
        $urlEvent = new BeforeReplaceVariableInCachedUrlEvent($uri, $enhancedRouting);
400
        /* @var BeforeReplaceVariableInCachedUrlEvent $urlEvent */
401 34
        $urlEvent = $this->eventDispatcher->dispatch($urlEvent);
402 34
        $uriCacheTemplate = (string)$urlEvent->getUri();
403
404 34
        $variableEvent = new BeforeProcessCachedVariablesEvent(
405 34
            $uri,
406 34
            $routingConfigurations,
407 34
            $keys,
408 34
            $values
409 34
        );
410 34
        $this->eventDispatcher->dispatch($variableEvent);
411
412 34
        $values = $variableEvent->getVariableValues();
413
        // Take care that everything is urlencoded!
414 34
        $keys = array_map(function ($value) {
415
            // @TODO: With only PHP 8 support, replace this with str_contains()
416 32
            if (strpos($value, '###') === false) {
417
                return $value;
418
            }
419 32
            return urlencode($value);
420 34
        }, array_keys($values));
421
422 34
        $uri = str_replace($keys, $values, $uriCacheTemplate);
423 34
        $uri = GeneralUtility::makeInstance(
424 34
            Uri::class,
425 34
            $uri
426 34
        );
427 34
        $uriEvent = new PostProcessUriEvent($uri, $routingConfigurations);
428 34
        $this->eventDispatcher->dispatch($uriEvent);
429 34
        $uri = $uriEvent->getUri();
430 34
        return (string)$uri;
431
    }
432
433
    /**
434
     * Flushes the internal in memory cache.
435
     */
436 10
    public function flushInMemoryCache()
437
    {
438 10
        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
     * @param array $branch
471
     */
472 34
    protected function getSubstitution(array &$structure, array &$values, array $branch = []): void
473
    {
474
        /*
475
         * Adds information about the filter facet to the placeholder.
476
         *
477
         * This feature allows the handle even placeholder in RouteEnhancer
478
         */
479 34
        $filter = false;
480 34
        if (count($branch) > 0 && $branch[count($branch) - 1] === 'filter') {
481 24
            $filter = true;
482
        }
483 34
        foreach ($structure as $key => &$value) {
484 34
            $branch[] = $key;
485 34
            if (is_array($value)) {
486 34
                $this->getSubstitution($value, $values, $branch);
487
            } else {
488
                // @todo: Refactor to multi-dimensional array.
489
                // https://solr-ddev-site.ddev.site/content-examples/form-elements/search?tx_solr[filter][type:tx_news_domain_model_news]=1&tx_solr[q]=*
490
                // https://solr-ddev-site.ddev.site/content-examples/form-elements/search?tx_solr[filter][0]=type:pages&tx_solr[q]=*
491 32
                if ($filter && $value !== 1) {
492 21
                    [$facetType] = explode(':', $value);
493 21
                    $branch[] = $facetType;
494
                }
495 32
                $path = '###' . implode(':', $branch) . '###';
496 32
                $values[$path] = $value;
497 32
                $structure[$key] = $path;
498 32
                if ($filter) {
499 21
                    array_pop($branch);
500
                }
501
            }
502 34
            array_pop($branch);
503
        }
504
    }
505
506
    /**
507
     * Sorts filter arguments if enabled.
508
     *
509
     *
510
     * @param SearchRequest $searchRequest
511
     * @param array $arguments
512
     */
513 23
    protected function sortFilterParametersIfNecessary(SearchRequest $searchRequest, array &$arguments)
514
    {
515 23
        if (!$searchRequest->isActiveFacetsSorted()) {
516 23
            return;
517
        }
518
519
        $pluginNameSpace = $searchRequest->getContextTypoScriptConfiguration()->getSearchPluginNamespace();
520
        if (!empty($arguments[$pluginNameSpace]['filter']) && is_array($arguments[$pluginNameSpace]['filter'])) {
521
            $arguments[$pluginNameSpace]['filter'] = ParameterSortingUtility::sortByType(
522
                $arguments[$pluginNameSpace]['filter'],
523
                $searchRequest->getActiveFacetsUrlParameterStyle()
524
            );
525
        }
526
    }
527
}
528