Passed
Push — upcoming-feature/Introduce_Rou... ( e466c2...d478f4 )
by Rafael
67:01 queued 31:07
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
namespace ApacheSolrForTypo3\Solr\Domain\Search;
4
5
/***************************************************************
6
 *  Copyright notice
7
 *
8
 *  (c) 2015-2016 Timo Schmidt <[email protected]>
9
 *  All rights reserved
10
 *
11
 *  This script is part of the TYPO3 project. The TYPO3 project is
12
 *  free software; you can redistribute it and/or modify
13
 *  it under the terms of the GNU General Public License as published by
14
 *  the Free Software Foundation; either version 3 of the License, or
15
 *  (at your option) any later version.
16
 *
17
 *  The GNU General Public License can be found at
18
 *  http://www.gnu.org/copyleft/gpl.html.
19
 *
20
 *  This script is distributed in the hope that it will be useful,
21
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23
 *  GNU General Public License for more details.
24
 *
25
 *  This copyright notice MUST APPEAR in all copies of the script!
26
 ***************************************************************/
27
28
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
29
use ApacheSolrForTypo3\Solr\System\Util\ArrayAccessor;
30
use TYPO3\CMS\Core\Utility\ArrayUtility;
31
32
/**
33
 * The searchRequest is used to act as an api to the arguments that have been passed
34
 * with GET and POST.
35
 *
36
 * @author Timo Schmidt <[email protected]>
37
 */
38
class SearchRequest
39
{
40
    /**
41
     * @var string
42
     */
43
    protected $id;
44
45
    /**
46
     * Default namespace overwritten with the configured plugin namespace.
47
     *
48
     * @var string
49
     */
50
    protected $argumentNameSpace = 'tx_solr';
51
52
    /**
53
     * Arguments that should be kept for sub requests.
54
     *
55
     * Default values, overwritten in the constructor with the namespaced arguments
56
     *
57
     * @var array
58
     */
59
    protected $persistentArgumentsPaths = ['tx_solr:q', 'tx_solr:filter', 'tx_solr:sort'];
60
61
    /**
62
     * @var bool
63
     */
64
    protected $stateChanged = false;
65
66
    /**
67
     * @var ArrayAccessor
68
     */
69
    protected $argumentsAccessor;
70
71
    /**
72
     * The sys_language_uid that was used in the context where the request was build.
73
     * This could be different from the "L" parameter and and not relevant for urls,
74
     * because typolink itself will handle it.
75
     *
76
     * @var int
77
     */
78
    protected $contextSystemLanguageUid;
79
80
    /**
81
     * The page_uid that was used in the context where the request was build.
82
     *
83
     * The pageUid is not relevant for the typolink additionalArguments and therefore
84
     * a separate property.
85
     *
86
     * @var int
87
     */
88
    protected $contextPageUid;
89
90
    /**
91
     * @var TypoScriptConfiguration
92
     */
93
    protected $contextTypoScriptConfiguration;
94
95
    /**
96
     * @var array
97
     */
98
    protected $persistedArguments = [];
99
100
    /**
101
     * @param array $argumentsArray
102
     * @param int $pageUid
103
     * @param int $sysLanguageUid
104
     * @param TypoScriptConfiguration $typoScriptConfiguration
105
     */
106 70
    public function __construct(array $argumentsArray = [], $pageUid = 0, $sysLanguageUid = 0, TypoScriptConfiguration $typoScriptConfiguration = null)
107
    {
108 70
        $this->stateChanged = true;
109 70
        $this->persistedArguments = $argumentsArray;
110 70
        $this->contextPageUid = $pageUid;
111 70
        $this->contextSystemLanguageUid = $sysLanguageUid;
112 70
        $this->contextTypoScriptConfiguration = $typoScriptConfiguration;
113 70
        $this->id = spl_object_hash($this);
114
115
        // overwrite the plugin namespace and the persistentArgumentsPaths
116 70
        if (!is_null($typoScriptConfiguration)) {
117 18
            $this->argumentNameSpace = $typoScriptConfiguration->getSearchPluginNamespace();
118
        }
119
120 70
        $this->persistentArgumentsPaths = [$this->argumentNameSpace . ':q', $this->argumentNameSpace . ':filter', $this->argumentNameSpace . ':sort', $this->argumentNameSpace . ':groupPage'];
121
122 70
        if (!is_null($typoScriptConfiguration)) {
123 18
            $additionalPersistentArgumentsNames = $typoScriptConfiguration->getSearchAdditionalPersistentArgumentNames();
124 18
            foreach ($additionalPersistentArgumentsNames ?? [] as $additionalPersistentArgumentsName) {
125
                $this->persistentArgumentsPaths[] = $this->argumentNameSpace . ':' . $additionalPersistentArgumentsName;
126
            }
127 18
            $this->persistentArgumentsPaths = array_unique($this->persistentArgumentsPaths);
128
        }
129
130 70
        $this->reset();
131 70
    }
132
133
    /**
134
     * @return string
135
     */
136 5
    public function getId()
137
    {
138 5
        return $this->id;
139
    }
140
141
    /**
142
     * Can be used do merge arguments into the request arguments
143
     *
144
     * @param array $argumentsToMerge
145
     * @return SearchRequest
146
     */
147 1
    public function mergeArguments(array $argumentsToMerge)
148
    {
149 1
        ArrayUtility::mergeRecursiveWithOverrule(
150 1
            $this->persistedArguments,
151 1
            $argumentsToMerge
152
        );
153
154 1
        $this->reset();
155
156 1
        return $this;
157
    }
158
159
    /**
160
     * Helper method to prefix an accessor with the arguments namespace.
161
     *
162
     * @param string $path
163
     * @return string
164
     */
165 49
    protected function prefixWithNamespace($path)
166
    {
167 49
        return $this->argumentNameSpace . ':' . $path;
168
    }
169
170
    /**
171
     * @return array
172
     */
173 1
    public function getActiveFacetNames()
174
    {
175 1
        $activeFacets = $this->getActiveFacets();
176 1
        $facetNames = [];
177
178
        array_map(function($activeFacet) use (&$facetNames) {
179 1
            $facetNames[] = substr($activeFacet, 0, strpos($activeFacet, ':'));
180 1
        }, $activeFacets);
181
182 1
        return $facetNames;
183
    }
184
185
    /**
186
     * Returns all facet values for a certain facetName
187
     * @param string $facetName
188
     * @return array
189
     */
190 12
    public function getActiveFacetValuesByName($facetName)
191
    {
192 12
        $values = [];
193 12
        $activeFacets = $this->getActiveFacets();
194
195
        array_map(function($activeFacet) use (&$values, $facetName) {
196 8
            $parts = explode(':', $activeFacet, 2);
197 8
            if ($parts[0] === $facetName) {
198 8
                $values[] = $parts[1];
199
            }
200 12
        }, $activeFacets);
201
202 12
        return $values;
203
    }
204
205
    /**
206
     * @return array
207
     */
208 17
    public function getActiveFacets()
209
    {
210 17
        $path = $this->prefixWithNamespace('filter');
211 17
        $pathValue = $this->argumentsAccessor->get($path, []);
212
213 17
        return is_array($pathValue) ? $pathValue : [];
214
    }
215
216
    /**
217
     * @return int
218
     */
219 2
    public function getActiveFacetCount()
220
    {
221 2
        return count($this->getActiveFacets());
222
    }
223
224
    /**
225
     * @param $activeFacets
226
     *
227
     * @return SearchRequest
228
     */
229 14
    protected function setActiveFacets($activeFacets = [])
230
    {
231 14
        $path = $this->prefixWithNamespace('filter');
232 14
        $this->argumentsAccessor->set($path, $activeFacets);
233
234 14
        return $this;
235
    }
236
237
    /**
238
     * Adds a facet value to the request.
239
     *
240
     * @param string $facetName
241
     * @param mixed $facetValue
242
     *
243
     * @return SearchRequest
244
     */
245 10
    public function addFacetValue($facetName, $facetValue)
246
    {
247 10
        if ($this->getHasFacetValue($facetName, $facetValue)) {
248
            return $this;
249
        }
250
251 10
        $facetValues = $this->getActiveFacets();
252 10
        $facetValues[] = $facetName . ':' . $facetValue;
253 10
        $this->setActiveFacets($facetValues);
254
255 10
        $this->stateChanged = true;
256 10
        return $this;
257
    }
258
259
    /**
260
     * Removes a facet value from the request.
261
     *
262
     * @param string $facetName
263
     * @param mixed $facetValue
264
     *
265
     * @return SearchRequest
266
     */
267 2
    public function removeFacetValue($facetName, $facetValue)
268
    {
269 2
        if (!$this->getHasFacetValue($facetName, $facetValue)) {
270
            return $this;
271
        }
272 2
        $facetValues = $this->getActiveFacets();
273 2
        $facetValueToLookFor = $facetName . ':' . $facetValue;
274
275 2
        foreach ($facetValues as $index => $facetValue) {
0 ignored issues
show
introduced by
$facetValue is overwriting one of the parameters of this function.
Loading history...
276 2
            if ($facetValue === $facetValueToLookFor) {
277 2
                unset($facetValues[$index]);
278 2
                break;
279
            }
280
        }
281
282 2
        $this->setActiveFacets($facetValues);
283 2
        $this->stateChanged = true;
284 2
        return $this;
285
    }
286
287
    /**
288
     * Removes all facet values from the request by a certain facet name
289
     *
290
     * @param string $facetName
291
     *
292
     * @return SearchRequest
293
     */
294 2
    public function removeAllFacetValuesByName($facetName)
295
    {
296 2
        $facetValues = $this->getActiveFacets();
297
        $facetValues = array_filter($facetValues, function($facetValue) use ($facetName) {
298 2
            $parts = explode(':', $facetValue, 2);
299 2
            return $parts[0] !== $facetName;
300 2
        });
301
302 2
        $this->setActiveFacets($facetValues);
303 2
        $this->stateChanged = true;
304 2
        return $this;
305
    }
306
307
    /**
308
     * Removes all active facets from the request.
309
     *
310
     * @return SearchRequest
311
     */
312 2
    public function removeAllFacets()
313
    {
314 2
        $path = $this->prefixWithNamespace('filter');
315 2
        $this->argumentsAccessor->reset($path);
316 2
        $this->stateChanged = true;
317 2
        return $this;
318
    }
319
320
    /**
321
     * @param string $facetName
322
     * @param mixed $facetValue
323
     * @return bool
324
     */
325 12
    public function getHasFacetValue($facetName, $facetValue)
326
    {
327 12
        $facetNameAndValueToCheck = $facetName . ':' . $facetValue;
328 12
        return in_array($facetNameAndValueToCheck, $this->getActiveFacets());
329
    }
330
331
    /**
332
     * @return bool
333
     */
334 14
    public function getHasSorting()
335
    {
336 14
        $path = $this->prefixWithNamespace('sort');
337 14
        return $this->argumentsAccessor->has($path);
338
    }
339
340
    /**
341
     * Returns the sorting string in the url e.g. title asc.
342
     *
343
     * @return string
344
     */
345 13
    public function getSorting()
346
    {
347 13
        $path = $this->prefixWithNamespace('sort');
348 13
        return $this->argumentsAccessor->get($path, '');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->argumentsAccessor->get($path, '') also could return the type array which is incompatible with the documented return type string.
Loading history...
349
    }
350
351
    /**
352
     * Helper function to get the sorting configuration name or direction.
353
     *
354
     * @param int $index
355
     * @return string
356
     */
357 13
    protected function getSortingPart($index)
358
    {
359 13
        $sorting = $this->getSorting();
360 13
        if ($sorting === '') {
361 11
            return null;
362
        }
363
364 2
        $parts = explode(' ', $sorting);
365 2
        return isset($parts[$index]) ? $parts[$index] : null;
366
    }
367
368
    /**
369
     * Returns the sorting configuration name that is currently used.
370
     *
371
     * @return string
372
     */
373 13
    public function getSortingName()
374
    {
375 13
        return $this->getSortingPart(0);
376
    }
377
378
    /**
379
     * Returns the sorting direction that is currently used.
380
     *
381
     * @return string
382
     */
383 12
    public function getSortingDirection()
384
    {
385 12
        return mb_strtolower($this->getSortingPart(1));
386
    }
387
388
    /**
389
     * @return SearchRequest
390
     */
391 1
    public function removeSorting()
392
    {
393 1
        $path = $this->prefixWithNamespace('sort');
394 1
        $this->argumentsAccessor->reset($path);
395 1
        $this->stateChanged = true;
396 1
        return $this;
397
    }
398
399
    /**
400
     * @param string $sortingName
401
     * @param string $direction (asc or desc)
402
     *
403
     * @return SearchRequest
404
     */
405 2
    public function setSorting($sortingName, $direction = 'asc')
406
    {
407 2
        $value = $sortingName . ' ' . $direction;
408 2
        $path = $this->prefixWithNamespace('sort');
409 2
        $this->argumentsAccessor->set($path, $value);
410 2
        $this->stateChanged = true;
411 2
        return $this;
412
    }
413
414
    /**
415
     * Method to set the paginated page of the search
416
     *
417
     * @param int $page
418
     * @return SearchRequest
419
     */
420 4
    public function setPage($page)
421
    {
422 4
        $this->stateChanged = true;
423 4
        $path = $this->prefixWithNamespace('page');
424 4
        $this->argumentsAccessor->set($path, $page);
425
        // use initial url by switching back to page 0
426 4
        if ($page === 0) {
427 3
            $this->argumentsAccessor->reset($path);
428
        }
429 4
        return $this;
430
    }
431
432
    /**
433
     * Returns the passed page.
434
     *
435
     * @return int|null
436
     */
437 16
    public function getPage()
438
    {
439 16
        $path = $this->prefixWithNamespace('page');
440 16
        return $this->argumentsAccessor->get($path);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->argumentsAccessor->get($path) also could return the type array which is incompatible with the documented return type integer|null.
Loading history...
441
    }
442
443
    /**
444
     * Can be used to reset all groupPages.
445
     *
446
     * @return SearchRequest
447
     */
448 7
    public function removeAllGroupItemPages(): SearchRequest
449
    {
450 7
        $path = $this->prefixWithNamespace('groupPage');
451 7
        $this->argumentsAccessor->reset($path);
452
453 7
        return $this;
454
    }
455
456
    /**
457
     * Can be used to paginate within a groupItem.
458
     *
459
     * @param string $groupName e.g. type
460
     * @param string $groupItemValue e.g. pages
461
     * @param int $page
462
     * @return SearchRequest
463
     */
464 4
    public function setGroupItemPage(string $groupName, string $groupItemValue, int $page): SearchRequest
465
    {
466 4
        $this->stateChanged = true;
467 4
        $escapedValue = $this->getEscapedGroupItemValue($groupItemValue);
468 4
        $path = $this->prefixWithNamespace('groupPage:' . $groupName . ':' . $escapedValue);
469 4
        $this->argumentsAccessor->set($path, $page);
470 4
        return $this;
471
    }
472
473
    /**
474
     * Retrieves the current page for this group item.
475
     *
476
     * @param string $groupName
477
     * @param string $groupItemValue
478
     * @return int
479
     */
480 3
    public function getGroupItemPage(string $groupName, string $groupItemValue): int
481
    {
482 3
        $escapedValue = $this->getEscapedGroupItemValue($groupItemValue);
483 3
        $path = $this->prefixWithNamespace('groupPage:' . $groupName . ':' . $escapedValue);
484 3
        return max(1, (int)$this->argumentsAccessor->get($path));
485
    }
486
487
    /**
488
     * Removes all non alphanumeric values from the groupItem value to have a valid array key.
489
     *
490
     * @param string $groupItemValue
491
     * @return string
492
     */
493 5
    protected function getEscapedGroupItemValue(string $groupItemValue)
494
    {
495 5
        return preg_replace("/[^A-Za-z0-9]/", '', $groupItemValue);
496
    }
497
498
    /**
499
     * Retrieves the highest page of the groups.
500
     *
501
     * @return int
502
     */
503 1
    public function getHighestGroupPage()
504
    {
505 1
        $max = 1;
506 1
        $path = $this->prefixWithNamespace('groupPage');
507 1
        $groupPages = $this->argumentsAccessor->get($path, []);
508 1
        foreach ($groupPages as $groups) {
509
            if (!is_array($groups)) continue;
510
            foreach ($groups as $groupItemPage) {
511
                if ($groupItemPage > $max) {
512
                    $max = $groupItemPage;
513
                }
514
            }
515
        }
516
517 1
        return $max;
518
    }
519
520
    /**
521
     * Method to overwrite the query string.
522
     *
523
     * @param string $rawQueryString
524
     * @return SearchRequest
525
     */
526 12
    public function setRawQueryString($rawQueryString)
527
    {
528 12
        $this->stateChanged = true;
529 12
        $path = $this->prefixWithNamespace('q');
530 12
        $this->argumentsAccessor->set($path, $rawQueryString);
531 12
        return $this;
532
    }
533
534
    /**
535
     * Returns the passed rawQueryString.
536
     *
537
     * @return string|null
538
     */
539 12
    public function getRawUserQuery()
540
    {
541 12
        $path = $this->prefixWithNamespace('q');
542 12
        $query = $this->argumentsAccessor->get($path, null);
543 12
        return is_null($query) ? $query : (string)$query;
544
    }
545
546
    /**
547
     * Method to check if the query string is an empty string
548
     * (also empty string or whitespaces only are handled as empty).
549
     *
550
     * When no query string is set (null) the method returns false.
551
     * @return bool
552
     */
553 12
    public function getRawUserQueryIsEmptyString()
554
    {
555 12
        $path = $this->prefixWithNamespace('q');
556 12
        $query = $this->argumentsAccessor->get($path, null);
557
558 12
        if ($query === null) {
559
            return false;
560
        }
561
562 12
        if (trim($query) === '') {
0 ignored issues
show
Bug introduced by
$query of type array is incompatible with the type string expected by parameter $string of trim(). ( Ignorable by Annotation )

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

562
        if (trim(/** @scrutinizer ignore-type */ $query) === '') {
Loading history...
563 1
            return true;
564
        }
565
566 11
        return false;
567
    }
568
569
    /**
570
     * This method returns true when no querystring is present at all.
571
     * Which means no search by the user was triggered
572
     *
573
     * @return bool
574
     */
575 13
    public function getRawUserQueryIsNull()
576
    {
577 13
        $path = $this->prefixWithNamespace('q');
578 13
        $query = $this->argumentsAccessor->get($path, null);
579 13
        return $query === null;
580
    }
581
582
    /**
583
     * Sets the results per page that are used during search.
584
     *
585
     * @param int $resultsPerPage
586
     * @return SearchRequest
587
     */
588 14
    public function setResultsPerPage($resultsPerPage)
589
    {
590 14
        $path = $this->prefixWithNamespace('resultsPerPage');
591 14
        $this->argumentsAccessor->set($path, $resultsPerPage);
592 14
        $this->stateChanged = true;
593
594 14
        return $this;
595
    }
596
597
    /**
598
     * @return bool
599
     */
600 1
    public function getStateChanged()
601
    {
602 1
        return $this->stateChanged;
603
    }
604
605
    /**
606
     * Returns the passed resultsPerPage value
607
     * @return int|null
608
     */
609 15
    public function getResultsPerPage()
610
    {
611 15
        $path = $this->prefixWithNamespace('resultsPerPage');
612 15
        return $this->argumentsAccessor->get($path);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->argumentsAccessor->get($path) also could return the type array which is incompatible with the documented return type integer|null.
Loading history...
613
    }
614
615
    /**
616
     * Allows to set additional filters that are used on time and not transported during the request.
617
     *
618
     * @param array $additionalFilters
619
     * @return SearchRequest
620
     */
621
    public function setAdditionalFilters($additionalFilters)
622
    {
623
        $path = $this->prefixWithNamespace('additionalFilters');
624
        $this->argumentsAccessor->set($path, $additionalFilters);
625
        $this->stateChanged = true;
626
627
        return $this;
628
    }
629
630
    /**
631
     * Retrieves the addtional filters that have been set
632
     *
633
     * @return array
634
     */
635 11
    public function getAdditionalFilters()
636
    {
637 11
        $path = $this->prefixWithNamespace('additionalFilters');
638 11
        return $this->argumentsAccessor->get($path, []);
639
    }
640
641
    /**
642
     * @return int
643
     */
644 1
    public function getContextSystemLanguageUid()
645
    {
646 1
        return $this->contextSystemLanguageUid;
647
    }
648
649
    /**
650
     * @return int
651
     */
652 1
    public function getContextPageUid()
653
    {
654 1
        return $this->contextPageUid;
655
    }
656
657
    /**
658
     * Get contextTypoScriptConfiguration
659
     *
660
     * @return TypoScriptConfiguration
661
     */
662 22
    public function getContextTypoScriptConfiguration()
663
    {
664 22
        return $this->contextTypoScriptConfiguration;
665
    }
666
667
    /**
668
     * Assigns the last known persistedArguments and restores their state.
669
     *
670
     * @return SearchRequest
671
     */
672 68
    public function reset()
673
    {
674 68
        $this->argumentsAccessor = new ArrayAccessor($this->persistedArguments);
675 68
        $this->stateChanged = false;
676 68
        return $this;
677
    }
678
679
    /**
680
     * This can be used to start a new sub request, e.g. for a faceted search.
681
     *
682
     * @param bool $onlyPersistentArguments
683
     * @return SearchRequest
684
     */
685 12
    public function getCopyForSubRequest($onlyPersistentArguments = true)
686
    {
687 12
        if (!$onlyPersistentArguments) {
688
            // create a new request with all data
689
            $argumentsArray = $this->argumentsAccessor->getData();
690
            return new SearchRequest(
691
                $argumentsArray,
692
                $this->contextPageUid,
693
                $this->contextSystemLanguageUid,
694
                $this->contextTypoScriptConfiguration
695
            );
696
        }
697
698 12
        $arguments = new ArrayAccessor();
699 12
        foreach ($this->persistentArgumentsPaths as $persistentArgumentPath) {
700 12
            if ($this->argumentsAccessor->has($persistentArgumentPath)) {
701 7
                $arguments->set($persistentArgumentPath, $this->argumentsAccessor->get($persistentArgumentPath));
702
            }
703
        }
704
705 12
        return new SearchRequest(
706 12
            $arguments->getData(),
707 12
            $this->contextPageUid,
708 12
            $this->contextSystemLanguageUid,
709 12
            $this->contextTypoScriptConfiguration
710
        );
711
    }
712
713
    /**
714
     * @return string
715
     */
716
    public function getArgumentNameSpace()
717
    {
718
        return $this->argumentNameSpace;
719
    }
720
721
    /**
722
     * @return array
723
     */
724 19
    public function getAsArray()
725
    {
726 19
        return $this->argumentsAccessor->getData();
727
    }
728
729
    /**
730
     * Returns only the arguments as array.
731
     *
732
     * @return array
733
     */
734
    public function getArguments() {
735
        return $this->argumentsAccessor->get($this->argumentNameSpace, []);
736
    }
737
}
738