Failed Conditions
Pull Request — main (#3628)
by
unknown
41:14
created

SearchRequest::mergeArguments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 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;
19
20
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\UrlFacetContainer;
21
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
22
use ApacheSolrForTypo3\Solr\System\Util\ArrayAccessor;
23
use TYPO3\CMS\Core\Utility\ArrayUtility;
0 ignored issues
show
Bug introduced by
The type TYPO3\CMS\Core\Utility\ArrayUtility 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...
24
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...
25
26
/**
27
 * The searchRequest is used to act as an api to the arguments that have been passed
28
 * with GET and POST.
29
 *
30
 * @author Timo Schmidt <[email protected]>
31
 */
32
class SearchRequest
33
{
34
    public const DEFAULT_PLUGIN_NAMESPACE = 'tx_solr';
35
36
    protected string $id;
37
38
    /**
39
     * Default namespace overwritten with the configured plugin namespace.
40
     */
41
    protected string $argumentNameSpace = self::DEFAULT_PLUGIN_NAMESPACE;
42
43
    /**
44
     * Arguments that should be kept for sub requests.
45
     * Default values, overwritten in the constructor with the namespaced arguments
46
     */
47
    protected array $persistentArgumentsPaths = [
48
        'tx_solr:q',
49
        'tx_solr:filter',
50
        'tx_solr:sort',
51
    ];
52
53
    protected bool $stateChanged = false;
54
55
    protected ?ArrayAccessor $argumentsAccessor = null;
56
57
    /**
58
     * The sys_language_uid that was used in the context where the request was build.
59
     * This could be different from the "L" parameter and not relevant for urls,
60
     * because typolink itself will handle it.
61
     */
62
    protected int $contextSystemLanguageUid = 0;
63
64
    /**
65
     * The page_uid that was used in the context where the request was build.
66
     *
67
     * The pageUid is not relevant for the typolink additionalArguments and therefore
68
     * a separate property.
69
     */
70
    protected int $contextPageUid;
71
72
    protected ?TypoScriptConfiguration $contextTypoScriptConfiguration;
73
74
    /**
75
     * Container for all active facets inside the URL(TYPO3/FE)
76
     */
77
    protected ?UrlFacetContainer $activeFacetContainer;
78
79
    protected array $persistedArguments = [];
80 104
81
    public function __construct(
82
        array $argumentsArray = [],
83
        int $pageUid = 0,
84
        int $sysLanguageUid = 0,
85
        TypoScriptConfiguration $typoScriptConfiguration = null,
86 104
    ) {
87 104
        $this->stateChanged = true;
88 104
        $this->persistedArguments = $argumentsArray;
89 104
        $this->contextPageUid = $pageUid;
90 104
        $this->contextSystemLanguageUid = $sysLanguageUid;
91 104
        $this->contextTypoScriptConfiguration = $typoScriptConfiguration;
92
        $this->id = spl_object_hash($this);
93
94 104
        // overwrite the plugin namespace and the persistentArgumentsPaths
95 54
        if (!is_null($typoScriptConfiguration)) {
96
            $this->argumentNameSpace = $typoScriptConfiguration->getSearchPluginNamespace() ?? self::DEFAULT_PLUGIN_NAMESPACE;
97
        }
98 104
99
        $this->persistentArgumentsPaths = [$this->argumentNameSpace . ':q', $this->argumentNameSpace . ':filter', $this->argumentNameSpace . ':sort', $this->argumentNameSpace . ':groupPage'];
100 104
101 54
        if (!is_null($typoScriptConfiguration)) {
102 54
            $additionalPersistentArgumentsNames = $typoScriptConfiguration->getSearchAdditionalPersistentArgumentNames();
103
            foreach ($additionalPersistentArgumentsNames as $additionalPersistentArgumentsName) {
104
                $this->persistentArgumentsPaths[] = $this->argumentNameSpace . ':' . $additionalPersistentArgumentsName;
105 54
            }
106
            $this->persistentArgumentsPaths = array_unique($this->persistentArgumentsPaths);
107
        }
108 104
109
        $this->reset();
110
    }
111 21
112
    public function getId(): string
113 21
    {
114
        return $this->id;
115
    }
116
117
    /**
118
     * Can be used do merge arguments into the request arguments
119 1
     */
120
    public function mergeArguments(array $argumentsToMerge): SearchRequest
121 1
    {
122 1
        ArrayUtility::mergeRecursiveWithOverrule(
123 1
            $this->persistedArguments,
124 1
            $argumentsToMerge
125
        );
126 1
127
        $this->reset();
128 1
129
        return $this;
130
    }
131
132
    /**
133
     * Helper method to prefix an accessor with the argument's namespace.
134 78
     */
135
    protected function prefixWithNamespace(string $path): string
136 78
    {
137
        return $this->argumentNameSpace . ':' . $path;
138
    }
139
140
    /**
141
     * Returns active facet names
142 2
     */
143
    public function getActiveFacetNames(): array
144 2
    {
145
        return $this->activeFacetContainer->getActiveFacetNames();
0 ignored issues
show
Bug introduced by
The method getActiveFacetNames() 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

145
        return $this->activeFacetContainer->/** @scrutinizer ignore-call */ getActiveFacetNames();

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...
146
    }
147
148
    /**
149
     * Returns all facet values for a certain facetName
150 18
     */
151
    public function getActiveFacetValuesByName(string $facetName): array
152 18
    {
153
        return $this->activeFacetContainer->getActiveFacetValuesByName($facetName);
154
    }
155
156
    /**
157
     * Returns active facets
158
     */
159
    public function getActiveFacets(): array
160
    {
161
        return $this->activeFacetContainer->getActiveFacets();
162
    }
163
164
    /**
165
     * Enable sorting of URL parameters
166
     *
167
     * @noinspection PhpUnused
168
     */
169
    public function sortActiveFacets(): void
170
    {
171
        $this->activeFacetContainer->enableSort();
172
    }
173 23
174
    public function isActiveFacetsSorted(): bool
175 23
    {
176
        return $this->activeFacetContainer->isSorted();
177
    }
178
179
    public function getActiveFacetsUrlParameterStyle(): string
180
    {
181
        return $this->activeFacetContainer->getParameterStyle();
182
    }
183
184
    /**
185
     * Returns the active count of facets
186 2
     */
187
    public function getActiveFacetCount(): int
188 2
    {
189
        return $this->activeFacetContainer->count();
190
    }
191
192
    /**
193
     * Sets active facets for current result set
194
     *
195
     * @noinspection PhpUnused
196
     */
197
    protected function setActiveFacets(array $activeFacets = []): SearchRequest
198
    {
199
        $this->activeFacetContainer->setActiveFacets($activeFacets);
200
201
        return $this;
202
    }
203
204
    /**
205
     * Adds a facet value to the request.
206 25
     */
207
    public function addFacetValue(string $facetName, $facetValue): SearchRequest
208 25
    {
209
        $this->activeFacetContainer->addFacetValue($facetName, $facetValue);
210 25
211 24
        if ($this->activeFacetContainer->hasChanged()) {
212 24
            $this->stateChanged = true;
213
            $this->activeFacetContainer->acknowledgeChange();
214
        }
215 25
216
        return $this;
217
    }
218
219
    /**
220
     * Removes a facet value from the request.
221 6
     */
222
    public function removeFacetValue(string $facetName, mixed $facetValue): SearchRequest
223 6
    {
224 6
        $this->activeFacetContainer->removeFacetValue($facetName, $facetValue);
225 6
        if ($this->activeFacetContainer->hasChanged()) {
226 6
            $this->stateChanged = true;
227
            $this->activeFacetContainer->acknowledgeChange();
228
        }
229 6
230
        return $this;
231
    }
232
233
    /**
234
     * Removes all facet values from the request by a certain facet name
235 3
     */
236
    public function removeAllFacetValuesByName(string $facetName): SearchRequest
237 3
    {
238 3
        $this->activeFacetContainer->removeAllFacetValuesByName($facetName);
239 3
        if ($this->activeFacetContainer->hasChanged()) {
240 3
            $this->stateChanged = true;
241
            $this->activeFacetContainer->acknowledgeChange();
242 3
        }
243
        return $this;
244
    }
245
246
    /**
247
     * Removes all active facets from the request.
248 6
     */
249
    public function removeAllFacets(): SearchRequest
250 6
    {
251 6
        $this->activeFacetContainer->removeAllFacets();
252 6
        if ($this->activeFacetContainer->hasChanged()) {
253 6
            $this->stateChanged = true;
254
            $this->activeFacetContainer->acknowledgeChange();
255 6
        }
256
        return $this;
257
    }
258
259
    /**
260
     * Check if an active facet has a given value
261 2
     */
262
    public function getHasFacetValue(string $facetName, mixed $facetValue): bool
263 2
    {
264
        return $this->activeFacetContainer->hasFacetValue($facetName, $facetValue);
265
    }
266 44
267
    /**
268 44
     * Returns all sortings in the sorting string e.g. ['title' => 'asc', 'relevance' => 'desc']
269 44
     */
270
    public function getSeperatedSortings(): array
271
    {
272
        $parsedSortings = [];
273
        $explodedSortings = GeneralUtility::trimExplode(',', $this->getSorting(), true);
274
275 43
        foreach ($explodedSortings as $sorting) {
276
            $sortingSeperated = explode(' ', $sorting);
277 43
            $parsedSortings[$sortingSeperated[0]] = $sortingSeperated[1];
278 43
        }
279
280
        return $parsedSortings;
281
    }
282
283
    public function getHasSorting(): bool
284 43
    {
285
        $path = $this->prefixWithNamespace('sort');
286 43
        return $this->argumentsAccessor->has($path);
0 ignored issues
show
Bug introduced by
The method has() 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

286
        return $this->argumentsAccessor->/** @scrutinizer ignore-call */ has($path);

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...
287 43
    }
288 41
289
    /**
290
     * Returns the sorting string in the url e.g. title asc.
291 2
     */
292 2
    public function getSorting(): string
293
    {
294
        $path = $this->prefixWithNamespace('sort');
295
        return $this->argumentsAccessor->get($path, '');
296
    }
297
298 43
    /**
299
     * Helper function to get the sorting configuration name or direction.
300 43
     */
301
    protected function getSortingPart(int $index): ?string
302
    {
303
        $sorting = $this->getSorting();
304
        if ($sorting === '') {
305
            return null;
306 42
        }
307
308 42
        $parts = explode(' ', $sorting);
309
        return $parts[$index] ?? null;
310
    }
311 1
312
    /**
313 1
     * Returns the sorting configuration name that is currently used.
314 1
     */
315 1
    public function getSortingName(): ?string
316 1
    {
317
        return $this->getSortingPart(0);
318
    }
319 2
320
    /**
321 2
     * Returns the sorting direction that is currently used.
322 2
     */
323 2
    public function getSortingDirection(): string
324 2
    {
325 2
        return mb_strtolower($this->getSortingPart(1) ?? '');
326
    }
327
328
    public function removeSorting(): SearchRequest
329
    {
330
        $path = $this->prefixWithNamespace('sort');
331 5
        $this->argumentsAccessor->reset($path);
332
        $this->stateChanged = true;
333 5
        return $this;
334 5
    }
335 5
336
    public function setSorting(string $sortingName, string $direction = 'asc'): SearchRequest
337 5
    {
338 4
        $value = $sortingName . ' ' . $direction;
339
        $path = $this->prefixWithNamespace('sort');
340 5
        $this->argumentsAccessor->set($path, $value);
341
        $this->stateChanged = true;
342
        return $this;
343
    }
344
345
    /**
346 52
     * Method to set the paginated page of the search
347
     */
348 52
    public function setPage(int $page): SearchRequest
349 52
    {
350
        $this->stateChanged = true;
351
        $path = $this->prefixWithNamespace('page');
352
        $this->argumentsAccessor->set($path, $page);
353
        // use initial url by switching back to page 0
354
        if ($page === 0) {
355 23
            $this->argumentsAccessor->reset($path);
356
        }
357 23
        return $this;
358 23
    }
359
360 23
    /**
361
     * Returns the passed page.
362
     */
363
    public function getPage(): ?int
364
    {
365
        $path = $this->prefixWithNamespace('page');
366 4
        return $this->argumentsAccessor->get($path);
367
    }
368
369
    /**
370
     * Can be used to reset all groupPages.
371 4
     */
372 4
    public function removeAllGroupItemPages(): SearchRequest
373 4
    {
374 4
        $path = $this->prefixWithNamespace('groupPage');
375 4
        $this->argumentsAccessor->reset($path);
376
377
        return $this;
378
    }
379
380
    /**
381 3
     * Can be used to paginate within a groupItem.
382
     */
383 3
    public function setGroupItemPage(
384 3
        string $groupName,
385 3
        string $groupItemValue,
386
        int $page,
387
    ): SearchRequest {
388
        $this->stateChanged = true;
389
        $escapedValue = $this->getEscapedGroupItemValue($groupItemValue);
390
        $path = $this->prefixWithNamespace('groupPage:' . $groupName . ':' . $escapedValue);
391 5
        $this->argumentsAccessor->set($path, $page);
392
        return $this;
393 5
    }
394
395
    /**
396
     * Retrieves the current page for this group item.
397
     */
398
    public function getGroupItemPage(string $groupName, string $groupItemValue): int
399 1
    {
400
        $escapedValue = $this->getEscapedGroupItemValue($groupItemValue);
401 1
        $path = $this->prefixWithNamespace('groupPage:' . $groupName . ':' . $escapedValue);
402 1
        return max(1, (int)$this->argumentsAccessor->get($path));
403 1
    }
404 1
405
    /**
406
     * Removes all non-alphanumeric values from the groupItem value to have a valid array key.
407
     */
408
    protected function getEscapedGroupItemValue(string $groupItemValue): string
409
    {
410
        return preg_replace('/[^A-Za-z0-9]/', '', $groupItemValue);
411
    }
412
413
    /**
414
     * Retrieves the highest page of the groups.
415 1
     */
416
    public function getHighestGroupPage(): int
417
    {
418
        $max = 1;
419
        $path = $this->prefixWithNamespace('groupPage');
420
        $groupPages = $this->argumentsAccessor->get($path, []);
421 17
        foreach ($groupPages as $groups) {
422
            if (!is_array($groups)) {
423 17
                continue;
424 17
            }
425 17
            foreach ($groups as $groupItemPage) {
426 17
                if ((int)$groupItemPage > $max) {
427
                    $max = (int)$groupItemPage;
428
                }
429
            }
430
        }
431
432 44
        return $max;
433
    }
434 44
435 44
    /**
436 44
     * Method to overwrite the query string.
437
     */
438
    public function setRawQueryString(string $rawQueryString = ''): SearchRequest
439
    {
440
        $this->stateChanged = true;
441
        $path = $this->prefixWithNamespace('q');
442
        $this->argumentsAccessor->set($path, $rawQueryString);
443
        return $this;
444
    }
445 43
446
    /**
447 43
     * Returns the passed rawQueryString.
448 43
     */
449
    public function getRawUserQuery(): string
450 43
    {
451 3
        $path = $this->prefixWithNamespace('q');
452
        $query = $this->argumentsAccessor->get($path);
453
        return (string)($query ?? '');
454 40
    }
455 2
456
    /**
457
     * Method to check if the query string is an empty string
458 38
     * (also empty string or whitespaces only are handled as empty).
459
     *
460
     * When no query string is set (null) the method returns false.
461
     */
462
    public function getRawUserQueryIsEmptyString(): bool
463
    {
464
        $path = $this->prefixWithNamespace('q');
465 49
        $query = $this->argumentsAccessor->get($path);
466
467 49
        if ($query === null) {
468 49
            return false;
469 49
        }
470
471
        if (trim($query) === '') {
472
            return true;
473
        }
474
475 50
        return false;
476
    }
477 50
478 50
    /**
479 50
     * This method returns true when no querystring is present at all.
480
     * Which means no search by the user was triggered
481 50
     */
482
    public function getRawUserQueryIsNull(): bool
483
    {
484 1
        $path = $this->prefixWithNamespace('q');
485
        $query = $this->argumentsAccessor->get($path);
486 1
        return $query === null;
487
    }
488
489
    /**
490
     * Sets the results per page that are used during search.
491
     */
492 51
    public function setResultsPerPage(int $resultsPerPage): SearchRequest
493
    {
494 51
        $path = $this->prefixWithNamespace('resultsPerPage');
495 51
        $this->argumentsAccessor->set($path, $resultsPerPage);
496
        $this->stateChanged = true;
497
498
        return $this;
499
    }
500
501 3
    public function getStateChanged(): bool
502
    {
503 3
        return $this->stateChanged;
504 3
    }
505 3
506
    /**
507 3
     * Returns the passed resultsPerPage value
508
     */
509
    public function getResultsPerPage(): int
510
    {
511
        $path = $this->prefixWithNamespace('resultsPerPage');
512
        return (int)$this->argumentsAccessor->get($path);
513 41
    }
514
515 41
    /**
516 41
     * Allows setting additional filters that are used on time and not transported during the request.
517
     */
518
    public function setAdditionalFilters(array $additionalFilters): SearchRequest
519 2
    {
520
        $path = $this->prefixWithNamespace('additionalFilters');
521 2
        $this->argumentsAccessor->set($path, $additionalFilters);
522
        $this->stateChanged = true;
523
524 2
        return $this;
525
    }
526 2
527
    /**
528
     * Retrieves the additional filters that have been set
529
     */
530
    public function getAdditionalFilters(): array
531
    {
532 54
        $path = $this->prefixWithNamespace('additionalFilters');
533
        return $this->argumentsAccessor->get($path, []);
534 54
    }
535
536
    public function getContextSystemLanguageUid(): int
537
    {
538
        return $this->contextSystemLanguageUid;
539
    }
540 104
541
    public function getContextPageUid(): int
542 104
    {
543 104
        return $this->contextPageUid;
544 104
    }
545 104
546 104
    /**
547 104
     * Get contextTypoScriptConfiguration
548 51
     */
549 104
    public function getContextTypoScriptConfiguration(): ?TypoScriptConfiguration
550 104
    {
551
        return $this->contextTypoScriptConfiguration;
552
    }
553
554 104
    /**
555 104
     * Assigns the last known persistedArguments and restores their state.
556
     */
557
    public function reset(): SearchRequest
558
    {
559
        $this->argumentsAccessor = new ArrayAccessor($this->persistedArguments);
560 104
        $this->stateChanged = false;
561
        $this->activeFacetContainer = new UrlFacetContainer(
562
            $this->argumentsAccessor,
563
            $this->argumentNameSpace ?? self::DEFAULT_PLUGIN_NAMESPACE,
564
            $this->contextTypoScriptConfiguration === null ?
565
                UrlFacetContainer::PARAMETER_STYLE_INDEX :
566 39
                $this->contextTypoScriptConfiguration->getSearchFacetingUrlParameterStyle()
567
        );
568 39
569
        // If the default of sorting parameter should be true, a modification of this condition is needed.
570
        // If instance of contextTypoScriptConfiguration is not TypoScriptConfiguration the sort should be enabled too
571
        if ($this->contextTypoScriptConfiguration instanceof TypoScriptConfiguration
572
            && $this->contextTypoScriptConfiguration->getSearchFacetingUrlParameterSort()
573
        ) {
574
            $this->activeFacetContainer->enableSort();
575
        }
576
577
        return $this;
578
    }
579 39
580 39
    /**
581 39
     * This can be used to start a new sub request, e.g. for a faceted search.
582 31
     */
583
    public function getCopyForSubRequest(bool $onlyPersistentArguments = true): SearchRequest
584
    {
585
        if (!$onlyPersistentArguments) {
586 39
            // create a new request with all data
587 39
            $argumentsArray = $this->argumentsAccessor->getData();
588 39
            return new SearchRequest(
589 39
                $argumentsArray,
590 39
                $this->contextPageUid,
591 39
                $this->contextSystemLanguageUid,
592
                $this->contextTypoScriptConfiguration
593
            );
594
        }
595
596
        $arguments = new ArrayAccessor();
597
        foreach ($this->persistentArgumentsPaths as $persistentArgumentPath) {
598
            if ($this->argumentsAccessor->has($persistentArgumentPath)) {
599 22
                $arguments->set($persistentArgumentPath, $this->argumentsAccessor->get($persistentArgumentPath));
600
            }
601 22
        }
602
603
        return new SearchRequest(
604 43
            $arguments->getData(),
605
            $this->contextPageUid,
606 43
            $this->contextSystemLanguageUid,
607
            $this->contextTypoScriptConfiguration
608
        );
609
    }
610
611
    /**
612 17
     * Returns argument's namespace
613
     *
614 17
     * @noinspection PhpUnused
615
     */
616
    public function getArgumentNamespace(): string
617
    {
618
        return $this->argumentNameSpace;
619
    }
620
621
    public function getAsArray(): array
622
    {
623
        return $this->argumentsAccessor->getData();
624
    }
625
626
    /**
627
     * Returns only the arguments as array.
628
     */
629
    public function getArguments(): array
630
    {
631
        return $this->argumentsAccessor->get($this->argumentNameSpace) ?? [];
632
    }
633
}
634