Failed Conditions
Pull Request — task/3376-TYPO3_12_compatibili... (#3569)
by
unknown
46:02 queued 01:00
created

SearchUriBuilder::injectUriBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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\Configuration\TypoScriptConfiguration;
27
use ApacheSolrForTypo3\Solr\System\Url\UrlHelper;
28
use ApacheSolrForTypo3\Solr\Utility\ParameterSortingUtility;
29
use Psr\EventDispatcher\EventDispatcherInterface;
30
use TYPO3\CMS\Core\Http\Uri;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
0 ignored issues
show
Bug introduced by
The type TYPO3\CMS\Core\Utility\GeneralUtility was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
32
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
33
34
/**
35
 * SearchUriBuilder
36
 *
37
 * Responsibility:
38
 *
39
 * The SearchUriBuilder is responsible to build uris, that are used in the
40
 * searchContext. It can use the previous request with its persistent
41
 * arguments to build the url for a search sub request.
42
 *
43
 * @author Frans Saris <[email protected]>
44
 * @author Timo Hund <[email protected]>
45
 */
46
class SearchUriBuilder
47
{
48
    protected ?UriBuilder $uriBuilder = null;
49
50
    protected static array $preCompiledLinks = [];
51
52
    protected static int $hitCount = 0;
53
54
    protected static int $missCount = 0;
55
56
    protected static array $additionalArgumentsCache = [];
57
58
    protected EventDispatcherInterface $eventDispatcher;
59
60
    protected ?RoutingService $routingService = null;
61
62
    public function injectUriBuilder(UriBuilder $uriBuilder): void
63
    {
64
        $this->uriBuilder = $uriBuilder;
65
    }
66
67
    public function injectRoutingService(RoutingService $routingService): void
68
    {
69
        $this->routingService = $routingService;
70
    }
71
72
    public function injectEventDispatcher(EventDispatcherInterface $eventDispatcher): void
73
    {
74
        $this->eventDispatcher = $eventDispatcher;
75
    }
76
77
    /**
78
     * @param mixed $facetValue
79
     */
80
    public function getAddFacetValueUri(SearchRequest $previousSearchRequest, string $facetName, $facetValue): string
81
    {
82
        $persistentAndFacetArguments = $previousSearchRequest
83
            ->getCopyForSubRequest()->removeAllGroupItemPages()->addFacetValue($facetName, $facetValue)
84
            ->getAsArray();
85
86
        $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
87
88
        $arguments = $persistentAndFacetArguments + $additionalArguments;
89
90
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
91
92
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
93
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
94
    }
95
96
    /**
97
     * Removes all other facet values for this name and only set's the passed value for the facet.
98
     */
99
    public function getSetFacetValueUri(SearchRequest $previousSearchRequest, $facetName, $facetValue): string
100
    {
101
        $previousSearchRequest = $previousSearchRequest
102
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacetValuesByName($facetName);
103
104
        return $this->getAddFacetValueUri($previousSearchRequest, $facetName, $facetValue);
105
    }
106
107
    public function getRemoveFacetValueUri(SearchRequest $previousSearchRequest, $facetName, $facetValue): string
108
    {
109
        $persistentAndFacetArguments = $previousSearchRequest
110
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeFacetValue($facetName, $facetValue)
111
            ->getAsArray();
112
113
        $additionalArguments = [];
114
        if (
115
            ($typoScriptConfiguration = $previousSearchRequest->getContextTypoScriptConfiguration())
116
            && $typoScriptConfiguration instanceof TypoScriptConfiguration
117
            && $typoScriptConfiguration->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()
118
        ) {
119
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
120
        }
121
        $arguments = $persistentAndFacetArguments + $additionalArguments;
122
123
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
124
125
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
126
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
127
    }
128
129
    public function getRemoveFacetUri(SearchRequest $previousSearchRequest, $facetName): string
130
    {
131
        $persistentAndFacetArguments = $previousSearchRequest
132
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacetValuesByName($facetName)
133
            ->getAsArray();
134
135
        $additionalArguments = [];
136
        if (
137
            ($typoScriptConfiguration = $previousSearchRequest->getContextTypoScriptConfiguration())
138
            && $typoScriptConfiguration instanceof TypoScriptConfiguration
139
            && $typoScriptConfiguration->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()
140
        ) {
141
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
142
        }
143
144
        $arguments = $persistentAndFacetArguments + $additionalArguments;
145
146
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
147
148
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
149
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
150
    }
151
152
    public function getRemoveAllFacetsUri(SearchRequest $previousSearchRequest): string
153
    {
154
        $persistentAndFacetArguments = $previousSearchRequest
155
            ->getCopyForSubRequest()->removeAllGroupItemPages()->removeAllFacets()
156
            ->getAsArray();
157
158
        $additionalArguments = [];
159
        if (
160
            ($typoScriptConfiguration = $previousSearchRequest->getContextTypoScriptConfiguration())
161
            && $typoScriptConfiguration instanceof TypoScriptConfiguration
162
            && $typoScriptConfiguration->getSearchFacetingFacetLinkUrlParametersUseForFacetResetLinkUrl()
163
        ) {
164
            $additionalArguments = $this->getAdditionalArgumentsFromRequestConfiguration($previousSearchRequest);
165
        }
166
167
        $arguments = $persistentAndFacetArguments + $additionalArguments;
168
169
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
170
171
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
172
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
173
    }
174
175
    public function getResultPageUri(SearchRequest $previousSearchRequest, $page): string
176
    {
177
        $persistentAndFacetArguments = $previousSearchRequest
178
            ->getCopyForSubRequest()->setPage($page)
179
            ->getAsArray();
180
181
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
182
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
183
    }
184
185
    public function getResultGroupItemPageUri(SearchRequest $previousSearchRequest, GroupItem $groupItem, int $page): string
186
    {
187
        $persistentAndFacetArguments = $previousSearchRequest
188
            ->getCopyForSubRequest()->setGroupItemPage($groupItem->getGroup()->getGroupName(), $groupItem->getGroupValue(), $page)
189
            ->getAsArray();
190
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
191
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
192
    }
193
194
    public function getNewSearchUri(SearchRequest $previousSearchRequest, $queryString): string
195
    {
196
        /** @var $request SearchRequest */
197
        $contextConfiguration = $previousSearchRequest->getContextTypoScriptConfiguration();
198
        $contextSystemLanguage = $previousSearchRequest->getContextSystemLanguageUid();
199
        $contextPageUid = $previousSearchRequest->getContextPageUid();
200
201
        $request = GeneralUtility::makeInstance(
202
            SearchRequest::class,
203
            [],
204
            /** @scrutinizer ignore-type */
205
            $contextPageUid,
206
            /** @scrutinizer ignore-type */
207
            $contextSystemLanguage,
208
            /** @scrutinizer ignore-type */
209
            $contextConfiguration
210
        );
211
        $arguments = $request->setRawQueryString($queryString)->getAsArray();
212
213
        $this->sortFilterParametersIfNecessary($previousSearchRequest, $arguments);
214
215
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
216
        return $this->buildLinkWithInMemoryCache($pageUid, $arguments);
217
    }
218
219
    public function getSetSortingUri(SearchRequest $previousSearchRequest, $sortingName, $sortingDirection): string
220
    {
221
        $persistentAndFacetArguments = $previousSearchRequest
222
            ->getCopyForSubRequest()->setSorting($sortingName, $sortingDirection)
223
            ->getAsArray();
224
225
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
226
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
227
    }
228
229
    public function getRemoveSortingUri(SearchRequest $previousSearchRequest): string
230
    {
231
        $persistentAndFacetArguments = $previousSearchRequest
232
            ->getCopyForSubRequest()->removeSorting()
233
            ->getAsArray();
234
235
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
236
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
237
    }
238
239
    public function getCurrentSearchUri(SearchRequest $previousSearchRequest): string
240
    {
241
        $persistentAndFacetArguments = $previousSearchRequest
242
            ->getCopyForSubRequest()
243
            ->getAsArray();
244
245
        $pageUid = $this->getTargetPageUidFromRequestConfiguration($previousSearchRequest);
246
        return $this->buildLinkWithInMemoryCache($pageUid, $persistentAndFacetArguments);
247
    }
248
249
    protected function getAdditionalArgumentsFromRequestConfiguration(SearchRequest $request): array
250
    {
251
        if ($request->getContextTypoScriptConfiguration() === null) {
252
            return [];
253
        }
254
255
        $reQuestId = $request->getId();
256
        if (isset(self::$additionalArgumentsCache[$reQuestId])) {
257
            return self::$additionalArgumentsCache[$reQuestId];
258
        }
259
260
        self::$additionalArgumentsCache[$reQuestId] = $request->getContextTypoScriptConfiguration()
261
            ->getSearchFacetingFacetLinkUrlParametersAsArray();
262
263
        return self::$additionalArgumentsCache[$reQuestId];
264
    }
265
266
    protected function getTargetPageUidFromRequestConfiguration(SearchRequest $request): ?int
267
    {
268
        return $request->getContextTypoScriptConfiguration()?->getSearchTargetPage();
269
    }
270
271
    /**
272
     * Build the link with an i memory cache that reduces the amount of required typolink calls.
273
     */
274
    protected function buildLinkWithInMemoryCache(?int $pageUid, array $arguments): string
275
    {
276
        $values = [];
277
        $structure = $arguments;
278
        $this->getSubstitution($structure, $values);
279
        $hash = md5($pageUid . json_encode($structure));
280
        if (isset(self::$preCompiledLinks[$hash])) {
281
            self::$hitCount++;
282
            $uriCacheTemplate = self::$preCompiledLinks[$hash];
283
        } else {
284
            self::$missCount++;
285
            $this->uriBuilder->reset()->setTargetPageUid($pageUid);
0 ignored issues
show
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

285
            $this->uriBuilder->/** @scrutinizer ignore-call */ 
286
                               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...
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

285
            $this->uriBuilder->reset()->setTargetPageUid(/** @scrutinizer ignore-type */ $pageUid);
Loading history...
286
            $uriCacheTemplate = $this->uriBuilder->setArguments($structure)->build();
287
288
            /* @var UrlHelper $urlHelper */
289
            $urlHelper = GeneralUtility::makeInstance(UrlHelper::class, $uriCacheTemplate);
290
            self::$preCompiledLinks[$hash] = (string)$urlHelper;
291
        }
292
293
        $keys = array_map(static function ($value) {
294
            return urlencode((string)$value);
295
        }, array_keys($values));
296
        $values = array_map(static function ($value) {
297
            return urlencode((string)$value);
298
        }, $values);
299
300
        $routingConfigurations = $this->routingService
301
            ->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

301
            ->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

301
        /** @scrutinizer ignore-call */ 
302
        $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...
302
        $enhancedRouting = count($routingConfigurations) > 0;
303
        $this->routingService->reset();
304
        if ($enhancedRouting && is_array($routingConfigurations[0] ?? null)) {
305
            $this->routingService->fromRoutingConfiguration($routingConfigurations[0]);
306
        }
307
308
        /* @var Uri $uri */
309
        $uri = GeneralUtility::makeInstance(
310
            Uri::class,
311
            $uriCacheTemplate
312
        );
313
314
        $urlEvent = new BeforeReplaceVariableInCachedUrlEvent($uri, $enhancedRouting);
315
        /* @var BeforeReplaceVariableInCachedUrlEvent $urlEvent */
316
        $urlEvent = $this->eventDispatcher->dispatch($urlEvent);
317
        $uriCacheTemplate = (string)$urlEvent->getUri();
318
319
        $variableEvent = new BeforeProcessCachedVariablesEvent(
320
            $uri,
321
            $routingConfigurations,
322
            $keys,
323
            $values
324
        );
325
        $this->eventDispatcher->dispatch($variableEvent);
326
327
        $values = $variableEvent->getVariableValues();
328
        // Take care that everything is urlencoded!
329
        $keys = array_map(static function ($value) {
330
            if (!str_contains($value, '###')) {
331
                return $value;
332
            }
333
            return urlencode($value);
334
        }, array_keys($values));
335
336
        $uri = str_replace($keys, $values, $uriCacheTemplate);
337
        $uri = GeneralUtility::makeInstance(
338
            Uri::class,
339
            $uri
340
        );
341
        $uriEvent = new PostProcessUriEvent($uri, $routingConfigurations);
342
        $this->eventDispatcher->dispatch($uriEvent);
343
        $uri = $uriEvent->getUri();
344
        return (string)$uri;
345
    }
346
347
    /**
348
     * Flushes the internal in memory cache.
349
     */
350
    public function flushInMemoryCache(): void
351
    {
352
        self::$preCompiledLinks = [];
353
    }
354
355
    /**
356
     * This method is used to build two arrays from a nested array. The first one represents the structure.
357
     * In this structure the values are replaced with the pass to the value. At the same time the values get collected
358
     * in the $values array, with the path as key. This can be used to build a comparable hash from the arguments
359
     * in order to reduce the amount of typolink calls
360
     *
361
     * Example input
362
     *
363
     * $data = [
364
     *  'foo' => [
365
     *      'bar' => 111
366
     *   ]
367
     * ]
368
     *
369
     * will return:
370
     *
371
     * $structure = [
372
     *  'foo' => [
373
     *      'bar' => '###foo:bar###'
374
     *   ]
375
     * ]
376
     *
377
     * $values = [
378
     *  '###foo:bar###' => 111
379
     * ]
380
     */
381
    protected function getSubstitution(array &$structure, array &$values, array $branch = []): void
382
    {
383
        /*
384
         * Adds information about the filter facet to the placeholder.
385
         *
386
         * This feature allows the handle even placeholder in RouteEnhancer
387
         */
388
        $filter = false;
389
        if (count($branch) > 0 && $branch[count($branch) - 1] === 'filter') {
390
            $filter = true;
391
        }
392
        foreach ($structure as $key => &$value) {
393
            $branch[] = $key;
394
            if (is_array($value)) {
395
                $this->getSubstitution($value, $values, $branch);
396
            } else {
397
                // @todo: Refactor to multi-dimensional array.
398
                // https://solr-ddev-site.ddev.site/content-examples/form-elements/search?tx_solr[filter][type:tx_news_domain_model_news]=1&tx_solr[q]=*
399
                // https://solr-ddev-site.ddev.site/content-examples/form-elements/search?tx_solr[filter][0]=type:pages&tx_solr[q]=*
400
                if ($filter && $value !== 1) {
401
                    [$facetType] = explode(':', $value);
402
                    $branch[] = $facetType;
403
                }
404
                $path = '###' . implode(':', $branch) . '###';
405
                $values[$path] = $value;
406
                $structure[$key] = $path;
407
                if ($filter) {
408
                    array_pop($branch);
409
                }
410
            }
411
            array_pop($branch);
412
        }
413
    }
414
415
    /**
416
     * Sorts filter arguments if enabled.
417
     */
418
    protected function sortFilterParametersIfNecessary(SearchRequest $searchRequest, array &$arguments): void
419
    {
420
        if (!$searchRequest->isActiveFacetsSorted()) {
421
            return;
422
        }
423
424
        if (
425
            ($typoScriptConfiguration = $searchRequest->getContextTypoScriptConfiguration())
426
            && $typoScriptConfiguration instanceof TypoScriptConfiguration
427
        ) {
428
            $pluginNameSpace = $typoScriptConfiguration->getSearchPluginNamespace();
429
            if (!empty($arguments[$pluginNameSpace]['filter']) && is_array($arguments[$pluginNameSpace]['filter'])) {
430
                $arguments[$pluginNameSpace]['filter'] = ParameterSortingUtility::sortByType(
431
                    $arguments[$pluginNameSpace]['filter'],
432
                    $searchRequest->getActiveFacetsUrlParameterStyle()
433
                );
434
            }
435
        }
436
    }
437
}
438