Passed
Push — master ( eb988a...82337b )
by Timo
21:29
created

SearchRequest::getEscapedGroupItemValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 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 96
    public function __construct(array $argumentsArray = [], $pageUid = 0, $sysLanguageUid = 0, TypoScriptConfiguration $typoScriptConfiguration = null)
107
    {
108 96
        $this->stateChanged = true;
109 96
        $this->persistedArguments = $argumentsArray;
110 96
        $this->contextPageUid = $pageUid;
111 96
        $this->contextSystemLanguageUid = $sysLanguageUid;
112 96
        $this->contextTypoScriptConfiguration = $typoScriptConfiguration;
113 96
        $this->id = spl_object_hash($this);
114
115
        // overwrite the plugin namespace and the persistentArgumentsPaths
116 96
        if (!is_null($typoScriptConfiguration)) {
117 51
            $this->argumentNameSpace = $typoScriptConfiguration->getSearchPluginNamespace();
118
        }
119
120 96
        $this->persistentArgumentsPaths = [$this->argumentNameSpace . ':q', $this->argumentNameSpace . ':filter', $this->argumentNameSpace . ':sort', $this->argumentNameSpace . ':groupPage'];
121 96
        $this->reset();
122 96
    }
123
124
    /**
125
     * @return string
126
     */
127 34
    public function getId()
128
    {
129 34
        return $this->id;
130
    }
131
132
    /**
133
     * Can be used do merge arguments into the request arguments
134
     *
135
     * @param array $argumentsToMerge
136
     * @return SearchRequest
137
     */
138 1
    public function mergeArguments(array $argumentsToMerge)
139
    {
140 1
        ArrayUtility::mergeRecursiveWithOverrule(
141 1
            $this->persistedArguments,
142 1
            $argumentsToMerge
143
        );
144
145 1
        $this->reset();
146
147 1
        return $this;
148
    }
149
150
    /**
151
     * Helper method to prefix an accessor with the arguments namespace.
152
     *
153
     * @param string $path
154
     * @return string
155
     */
156 78
    protected function prefixWithNamespace($path)
157
    {
158 78
        return $this->argumentNameSpace . ':' . $path;
159
    }
160
161
    /**
162
     * @return array
163
     */
164 29
    public function getActiveFacetNames()
165
    {
166 29
        $activeFacets = $this->getActiveFacets();
167 29
        $facetNames = [];
168
169 29
        array_map(function($activeFacet) use (&$facetNames) {
170 3
            $facetNames[] = substr($activeFacet, 0, strpos($activeFacet, ':'));
171 29
        }, $activeFacets);
172
173 29
        return $facetNames;
174
    }
175
176
    /**
177
     * Returns all facet values for a certain facetName
178
     * @param string $facetName
179
     * @return array
180
     */
181 41
    public function getActiveFacetValuesByName($facetName)
182
    {
183 41
        $values = [];
184 41
        $activeFacets = $this->getActiveFacets();
185
186 41
        array_map(function($activeFacet) use (&$values, $facetName) {
187 11
            $parts = explode(':', $activeFacet, 2);
188 11
            if ($parts[0] === $facetName) {
189 11
                $values[] = $parts[1];
190
            }
191 41
        }, $activeFacets);
192
193 41
        return $values;
194
    }
195
196
    /**
197
     * @return array
198
     */
199 49
    public function getActiveFacets()
200
    {
201 49
        $path = $this->prefixWithNamespace('filter');
202 49
        $pathValue = $this->argumentsAccessor->get($path, []);
203
204 49
        return is_array($pathValue) ? $pathValue : [];
0 ignored issues
show
introduced by
The condition is_array($pathValue) can never be false.
Loading history...
205
    }
206
207
    /**
208
     * @return int
209
     */
210 2
    public function getActiveFacetCount()
211
    {
212 2
        return count($this->getActiveFacets());
213
    }
214
215
    /**
216
     * @param $activeFacets
217
     *
218
     * @return SearchRequest
219
     */
220 43
    protected function setActiveFacets($activeFacets = [])
221
    {
222 43
        $path = $this->prefixWithNamespace('filter');
223 43
        $this->argumentsAccessor->set($path, $activeFacets);
224
225 43
        return $this;
226
    }
227
228
    /**
229
     * Adds a facet value to the request.
230
     *
231
     * @param string $facetName
232
     * @param mixed $facetValue
233
     *
234
     * @return SearchRequest
235
     */
236 38
    public function addFacetValue($facetName, $facetValue)
237
    {
238 38
        if ($this->getHasFacetValue($facetName, $facetValue)) {
239 3
            return $this;
240
        }
241
242 38
        $facetValues = $this->getActiveFacets();
243 38
        $facetValues[] = $facetName . ':' . $facetValue;
244 38
        $this->setActiveFacets($facetValues);
245
246 38
        $this->stateChanged = true;
247 38
        return $this;
248
    }
249
250
    /**
251
     * Removes a facet value from the request.
252
     *
253
     * @param string $facetName
254
     * @param mixed $facetValue
255
     *
256
     * @return SearchRequest
257
     */
258 6
    public function removeFacetValue($facetName, $facetValue)
259
    {
260 6
        if (!$this->getHasFacetValue($facetName, $facetValue)) {
261
            return $this;
262
        }
263 6
        $facetValues = $this->getActiveFacets();
264 6
        $facetValueToLookFor = $facetName . ':' . $facetValue;
265
266 6
        foreach ($facetValues as $index => $facetValue) {
0 ignored issues
show
introduced by
$facetValue is overwriting one of the parameters of this function.
Loading history...
267 6
            if ($facetValue === $facetValueToLookFor) {
268 6
                unset($facetValues[$index]);
269 6
                break;
270
            }
271
        }
272
273 6
        $this->setActiveFacets($facetValues);
274 6
        $this->stateChanged = true;
275 6
        return $this;
276
    }
277
278
    /**
279
     * Removes all facet values from the request by a certain facet name
280
     *
281
     * @param string $facetName
282
     *
283
     * @return SearchRequest
284
     */
285 3
    public function removeAllFacetValuesByName($facetName)
286
    {
287 3
        $facetValues = $this->getActiveFacets();
288 3
        $facetValues = array_filter($facetValues, function($facetValue) use ($facetName) {
289 2
            $parts = explode(':', $facetValue, 2);
290 2
            return $parts[0] !== $facetName;
291 3
        });
292
293 3
        $this->setActiveFacets($facetValues);
294 3
        $this->stateChanged = true;
295 3
        return $this;
296
    }
297
298
    /**
299
     * Removes all active facets from the request.
300
     *
301
     * @return SearchRequest
302
     */
303 6
    public function removeAllFacets()
304
    {
305 6
        $path = $this->prefixWithNamespace('filter');
306 6
        $this->argumentsAccessor->reset($path);
307 6
        $this->stateChanged = true;
308 6
        return $this;
309
    }
310
311
    /**
312
     * @param string $facetName
313
     * @param mixed $facetValue
314
     * @return bool
315
     */
316 42
    public function getHasFacetValue($facetName, $facetValue)
317
    {
318 42
        $facetNameAndValueToCheck = $facetName . ':' . $facetValue;
319 42
        return in_array($facetNameAndValueToCheck, $this->getActiveFacets());
320
    }
321
322
    /**
323
     * @return bool
324
     */
325 45
    public function getHasSorting()
326
    {
327 45
        $path = $this->prefixWithNamespace('sort');
328 45
        return $this->argumentsAccessor->has($path);
329
    }
330
331
    /**
332
     * Returns the sorting string in the url e.g. title asc.
333
     *
334
     * @return string
335
     */
336 44
    public function getSorting()
337
    {
338 44
        $path = $this->prefixWithNamespace('sort');
339 44
        return $this->argumentsAccessor->get($path, '');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->argumentsAccessor->get($path, '') returns the type array which is incompatible with the documented return type string.
Loading history...
340
    }
341
342
    /**
343
     * Helper function to get the sorting configuration name or direction.
344
     *
345
     * @param int $index
346
     * @return string
347
     */
348 44
    protected function getSortingPart($index)
349
    {
350 44
        $sorting = $this->getSorting();
351 44
        if ($sorting === '') {
352 42
            return null;
353
        }
354
355 2
        $parts = explode(' ', $sorting);
356 2
        return isset($parts[$index]) ? $parts[$index] : null;
357
    }
358
359
    /**
360
     * Returns the sorting configuration name that is currently used.
361
     *
362
     * @return string
363
     */
364 44
    public function getSortingName()
365
    {
366 44
        return $this->getSortingPart(0);
367
    }
368
369
    /**
370
     * Returns the sorting direction that is currently used.
371
     *
372
     * @return string
373
     */
374 43
    public function getSortingDirection()
375
    {
376 43
        return mb_strtolower($this->getSortingPart(1));
377
    }
378
379
    /**
380
     * @return SearchRequest
381
     */
382 31
    public function removeSorting()
383
    {
384 31
        $path = $this->prefixWithNamespace('sort');
385 31
        $this->argumentsAccessor->reset($path);
386 31
        $this->stateChanged = true;
387 31
        return $this;
388
    }
389
390
    /**
391
     * @param string $sortingName
392
     * @param string $direction (asc or desc)
393
     *
394
     * @return SearchRequest
395
     */
396 32
    public function setSorting($sortingName, $direction = 'asc')
397
    {
398 32
        $value = $sortingName . ' ' . $direction;
399 32
        $path = $this->prefixWithNamespace('sort');
400 32
        $this->argumentsAccessor->set($path, $value);
401 32
        $this->stateChanged = true;
402 32
        return $this;
403
    }
404
405
    /**
406
     * Method to set the paginated page of the search
407
     *
408
     * @param int $page
409
     * @return SearchRequest
410
     */
411 14
    public function setPage($page)
412
    {
413 14
        $this->stateChanged = true;
414 14
        $path = $this->prefixWithNamespace('page');
415 14
        $this->argumentsAccessor->set($path, $page);
416 14
        return $this;
417
    }
418
419
    /**
420
     * Returns the passed page.
421
     *
422
     * @return int|null
423
     */
424 46
    public function getPage()
425
    {
426 46
        $path = $this->prefixWithNamespace('page');
427 46
        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 null|integer.
Loading history...
428
    }
429
430
    /**
431
     * Can be used to reset all groupPages.
432
     *
433
     * @return SearchRequest
434
     */
435 36
    public function removeAllGroupItemPages(): SearchRequest
436
    {
437 36
        $path = $this->prefixWithNamespace('groupPage');
438 36
        $this->argumentsAccessor->reset($path);
439
440 36
        return $this;
441
    }
442
443
    /**
444
     * Can be used to paginate within a groupItem.
445
     *
446
     * @param string $groupName e.g. type
447
     * @param string $groupItemValue e.g. pages
448
     * @param int $page
449
     * @return SearchRequest
450
     */
451 4
    public function setGroupItemPage(string $groupName, string $groupItemValue, int $page): SearchRequest
452
    {
453 4
        $this->stateChanged = true;
454 4
        $escapedValue = $this->getEscapedGroupItemValue($groupItemValue);
455 4
        $path = $this->prefixWithNamespace('groupPage:' . $groupName . ':' . $escapedValue);
456 4
        $this->argumentsAccessor->set($path, $page);
457 4
        return $this;
458
    }
459
460
    /**
461
     * Retrieves the current page for this group item.
462
     *
463
     * @param string $groupName
464
     * @param string $groupItemValue
465
     * @return int
466
     */
467 2
    public function getGroupItemPage(string $groupName, string $groupItemValue): int
468
    {
469 2
        $escapedValue = $this->getEscapedGroupItemValue($groupItemValue);
470 2
        $path = $this->prefixWithNamespace('groupPage:' . $groupName . ':' . $escapedValue);
471 2
        return max(1, (int)$this->argumentsAccessor->get($path));
472
    }
473
474
    /**
475
     * Removes all non alphanumeric values from the groupItem value to have a valid array key.
476
     *
477
     * @param string $groupItemValue
478
     * @return string
479
     */
480 4
    protected function getEscapedGroupItemValue(string $groupItemValue)
481
    {
482 4
        return preg_replace("/[^A-Za-z0-9]/", '', $groupItemValue);
483
    }
484
485
    /**
486
     * Retrieves the highest page of the groups.
487
     *
488
     * @return int
489
     */
490
    public function getHighestGroupPage()
491
    {
492
        $max = 1;
493
        $path = $this->prefixWithNamespace('groupPage');
494
        $groupPages = $this->argumentsAccessor->get($path);
495
        foreach ($groupPages as $groups) {
496
            if (!is_array($groups)) continue;
497
            foreach ($groups as $groupItemPage) {
498
                if ($groupItemPage > $max) {
499
                    $max = $groupItemPage;
500
                }
501
            }
502
        }
503
504
        return $max;
505
    }
506
507
    /**
508
     * Method to overwrite the query string.
509
     *
510
     * @param string $rawQueryString
511
     * @return SearchRequest
512
     */
513 42
    public function setRawQueryString($rawQueryString)
514
    {
515 42
        $this->stateChanged = true;
516 42
        $path = $this->prefixWithNamespace('q');
517 42
        $this->argumentsAccessor->set($path, $rawQueryString);
518 42
        return $this;
519
    }
520
521
    /**
522
     * Returns the passed rawQueryString.
523
     *
524
     * @return string|null
525
     */
526 46
    public function getRawUserQuery()
527
    {
528 46
        $path = $this->prefixWithNamespace('q');
529 46
        $query = $this->argumentsAccessor->get($path, null);
530 46
        return is_null($query) ? $query : (string)$query;
531
    }
532
533
    /**
534
     * Method to check if the query string is an empty string
535
     * (also empty string or whitespaces only are handled as empty).
536
     *
537
     * When no query string is set (null) the method returns false.
538
     * @return bool
539
     */
540 43
    public function getRawUserQueryIsEmptyString()
541
    {
542 43
        $path = $this->prefixWithNamespace('q');
543 43
        $query = $this->argumentsAccessor->get($path, null);
544
545 43
        if ($query === null) {
546 4
            return false;
547
        }
548
549 39
        if (trim($query) === '') {
0 ignored issues
show
Bug introduced by
$query of type array is incompatible with the type string expected by parameter $str 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

549
        if (trim(/** @scrutinizer ignore-type */ $query) === '') {
Loading history...
550
            return true;
551
        }
552
553 39
        return false;
554
    }
555
556
    /**
557
     * This method returns true when no querystring is present at all.
558
     * Which means no search by the user was triggered
559
     *
560
     * @return bool
561
     */
562 45
    public function getRawUserQueryIsNull()
563
    {
564 45
        $path = $this->prefixWithNamespace('q');
565 45
        $query = $this->argumentsAccessor->get($path, null);
566 45
        return $query === null;
567
    }
568
569
    /**
570
     * Sets the results per page that are used during search.
571
     *
572
     * @param int $resultsPerPage
573
     * @return SearchRequest
574
     */
575 48
    public function setResultsPerPage($resultsPerPage)
576
    {
577 48
        $path = $this->prefixWithNamespace('resultsPerPage');
578 48
        $this->argumentsAccessor->set($path, $resultsPerPage);
579 48
        $this->stateChanged = true;
580
581 48
        return $this;
582
    }
583
584
    /**
585
     * @return bool
586
     */
587 1
    public function getStateChanged()
588
    {
589 1
        return $this->stateChanged;
590
    }
591
592
    /**
593
     * Returns the passed resultsPerPage value
594
     * @return int|null
595
     */
596 47
    public function getResultsPerPage()
597
    {
598 47
        $path = $this->prefixWithNamespace('resultsPerPage');
599 47
        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 null|integer.
Loading history...
600
    }
601
602
    /**
603
     * @return int
604
     */
605 34
    public function getContextSystemLanguageUid()
606
    {
607 34
        return $this->contextSystemLanguageUid;
608
    }
609
610
    /**
611
     * @return int
612
     */
613 34
    public function getContextPageUid()
614
    {
615 34
        return $this->contextPageUid;
616
    }
617
618
    /**
619
     * Get contextTypoScriptConfiguration
620
     *
621
     * @return TypoScriptConfiguration
622
     */
623 55
    public function getContextTypoScriptConfiguration()
624
    {
625 55
        return $this->contextTypoScriptConfiguration;
626
    }
627
628
    /**
629
     * Assigns the last known persistedArguments and restores their state.
630
     *
631
     * @return SearchRequest
632
     */
633 94
    public function reset()
634
    {
635 94
        $this->argumentsAccessor = new ArrayAccessor($this->persistedArguments);
636 94
        $this->stateChanged = false;
637 94
        return $this;
638
    }
639
640
    /**
641
     * This can be used to start a new sub request, e.g. for a faceted search.
642
     *
643
     * @param bool $onlyPersistentArguments
644
     * @return SearchRequest
645
     */
646 41
    public function getCopyForSubRequest($onlyPersistentArguments = true)
647
    {
648 41
        if (!$onlyPersistentArguments) {
649
            // create a new request with all data
650
            $argumentsArray = $this->argumentsAccessor->getData();
651
            return new SearchRequest(
652
                $argumentsArray,
653
                $this->contextPageUid,
654
                $this->contextSystemLanguageUid,
655
                $this->contextTypoScriptConfiguration
656
            );
657
        }
658
659 41
        $arguments = new ArrayAccessor();
660 41
        foreach ($this->persistentArgumentsPaths as $persistentArgumentPath) {
661 41
            if ($this->argumentsAccessor->has($persistentArgumentPath)) {
662 41
                $arguments->set($persistentArgumentPath, $this->argumentsAccessor->get($persistentArgumentPath));
663
            }
664
        }
665
666 41
        return new SearchRequest(
667 41
            $arguments->getData(),
668 41
            $this->contextPageUid,
669 41
            $this->contextSystemLanguageUid,
670 41
            $this->contextTypoScriptConfiguration
671
        );
672
    }
673
674
    /**
675
     * @return string
676
     */
677 26
    public function getArgumentNameSpace()
678
    {
679 26
        return $this->argumentNameSpace;
680
    }
681
682
    /**
683
     * @return array
684
     */
685 51
    public function getAsArray()
686
    {
687 51
        return $this->argumentsAccessor->getData();
688
    }
689
690
    /**
691
     * Returns only the arguments as array.
692
     *
693
     * @return array
694
     */
695 37
    public function getArguments() {
696 37
        return $this->argumentsAccessor->get($this->argumentNameSpace, []);
697
    }
698
}
699