Issues (202)

Classes/Domain/Search/SearchRequest.php (5 issues)

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

562
        if (trim(/** @scrutinizer ignore-type */ $query) === '') {
Loading history...
563
            return true;
564 48
        }
565 48
566 48
        return false;
567
    }
568 48
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 1
     */
575
    public function getRawUserQueryIsNull()
576 1
    {
577
        $path = $this->prefixWithNamespace('q');
578
        $query = $this->argumentsAccessor->get($path, null);
579
        return $query === null;
580
    }
581
582
    /**
583 47
     * Sets the results per page that are used during search.
584
     *
585 47
     * @param int $resultsPerPage
586 47
     * @return SearchRequest
587
     */
588
    public function setResultsPerPage($resultsPerPage)
589
    {
590
        $path = $this->prefixWithNamespace('resultsPerPage');
591
        $this->argumentsAccessor->set($path, $resultsPerPage);
592 34
        $this->stateChanged = true;
593
594 34
        return $this;
595
    }
596
597
    /**
598
     * @return bool
599
     */
600 34
    public function getStateChanged()
601
    {
602 34
        return $this->stateChanged;
603
    }
604
605
    /**
606
     * Returns the passed resultsPerPage value
607
     * @return int|null
608
     */
609
    public function getResultsPerPage()
610 54
    {
611
        $path = $this->prefixWithNamespace('resultsPerPage');
612 54
        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 92
     */
621
    public function setAdditionalFilters($additionalFilters)
622 92
    {
623 92
        $path = $this->prefixWithNamespace('additionalFilters');
624 92
        $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 40
     * @return array
634
     */
635 40
    public function getAdditionalFilters()
636
    {
637
        $path = $this->prefixWithNamespace('additionalFilters');
638
        return $this->argumentsAccessor->get($path, []);
639
    }
640
641
    /**
642
     * @return int
643
     */
644
    public function getContextSystemLanguageUid()
645
    {
646 40
        return $this->contextSystemLanguageUid;
647 40
    }
648 40
649 40
    /**
650
     * @return int
651
     */
652
    public function getContextPageUid()
653 40
    {
654 40
        return $this->contextPageUid;
655 40
    }
656 40
657 40
    /**
658
     * Get contextTypoScriptConfiguration
659
     *
660
     * @return TypoScriptConfiguration
661
     */
662
    public function getContextTypoScriptConfiguration()
663
    {
664 26
        return $this->contextTypoScriptConfiguration;
665
    }
666 26
667
    /**
668
     * Assigns the last known persistedArguments and restores their state.
669
     *
670
     * @return SearchRequest
671
     */
672 50
    public function reset()
673
    {
674 50
        $this->argumentsAccessor = new ArrayAccessor($this->persistedArguments);
675
        $this->stateChanged = false;
676
        return $this;
677
    }
678
679
    /**
680
     * This can be used to start a new sub request, e.g. for a faceted search.
681
     *
682 37
     * @param bool $onlyPersistentArguments
683 37
     * @return SearchRequest
684
     */
685
    public function getCopyForSubRequest($onlyPersistentArguments = true)
686
    {
687
        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
        $arguments = new ArrayAccessor();
699
        foreach ($this->persistentArgumentsPaths as $persistentArgumentPath) {
700
            if ($this->argumentsAccessor->has($persistentArgumentPath)) {
701
                $arguments->set($persistentArgumentPath, $this->argumentsAccessor->get($persistentArgumentPath));
702
            }
703
        }
704
705
        return new SearchRequest(
706
            $arguments->getData(),
707
            $this->contextPageUid,
708
            $this->contextSystemLanguageUid,
709
            $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
    public function getAsArray()
725
    {
726
        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