Passed
Push — release-11.5.x ( 0d2d76...f14861 )
by Markus
34:30 queued 29:55
created

Faceting::getFilterParts()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.3229

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
c 1
b 0
f 0
dl 0
loc 28
ccs 13
cts 16
cp 0.8125
rs 8.8333
cc 7
nc 10
nop 3
crap 7.3229
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\Query\Modifier;
19
20
use ApacheSolrForTypo3\Solr\Domain\Search\Query\ParameterBuilder\Faceting as FacetingBuilder;
21
use ApacheSolrForTypo3\Solr\Domain\Search\Query\Query;
22
use ApacheSolrForTypo3\Solr\Domain\Search\Query\QueryBuilder;
23
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\FacetRegistry;
24
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\InvalidFacetPackageException;
25
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\InvalidQueryBuilderException;
26
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\InvalidUrlDecoderException;
27
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\UrlFacetContainer;
28
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
29
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequestAware;
30
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
31
use InvalidArgumentException;
32
use TYPO3\CMS\Core\Log\LogManager;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
35
/**
36
 * Modifies a query to add faceting parameters
37
 *
38
 * @author Ingo Renner <[email protected]>
39
 * @author Daniel Poetzinger <[email protected]>
40
 * @author Sebastian Kurfuerst <[email protected]>
41
 */
42
class Faceting implements Modifier, SearchRequestAware
43
{
44
    /**
45
     * @var FacetRegistry
46
     */
47
    protected FacetRegistry $facetRegistry;
48
49
    /**
50
     * @var SearchRequest|null
51
     */
52
    protected ?SearchRequest $searchRequest = null;
53
54
    /**
55
     * @param FacetRegistry $facetRegistry
56
     */
57 31
    public function __construct(FacetRegistry $facetRegistry)
58
    {
59 31
        $this->facetRegistry = $facetRegistry;
60
    }
61
62
    /**
63
     * @param SearchRequest $searchRequest
64
     */
65 31
    public function setSearchRequest(SearchRequest $searchRequest)
66
    {
67 31
        $this->searchRequest = $searchRequest;
68
    }
69
70
    /**
71
     * Modifies the given query and adds the parameters necessary for faceted
72
     * search.
73
     *
74
     * @param Query $query The query to modify
75
     * @return Query The modified query with faceting parameters
76
     *
77
     * @throws InvalidFacetPackageException
78
     * @throws InvalidQueryBuilderException
79
     * @throws InvalidUrlDecoderException
80
     */
81 30
    public function modifyQuery(Query $query): Query
82
    {
83 30
        $typoScriptConfiguration = $this->searchRequest->getContextTypoScriptConfiguration();
0 ignored issues
show
Bug introduced by
The method getContextTypoScriptConfiguration() 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

83
        /** @scrutinizer ignore-call */ 
84
        $typoScriptConfiguration = $this->searchRequest->getContextTypoScriptConfiguration();

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...
84 30
        $faceting = FacetingBuilder::fromTypoScriptConfiguration($typoScriptConfiguration);
0 ignored issues
show
Bug introduced by
It seems like $typoScriptConfiguration can also be of type null; however, parameter $solrConfiguration of ApacheSolrForTypo3\Solr\...poScriptConfiguration() does only seem to accept ApacheSolrForTypo3\Solr\...TypoScriptConfiguration, maybe add an additional type check? ( Ignorable by Annotation )

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

84
        $faceting = FacetingBuilder::fromTypoScriptConfiguration(/** @scrutinizer ignore-type */ $typoScriptConfiguration);
Loading history...
85
86 30
        $allFacets = $typoScriptConfiguration->getSearchFacetingFacets();
87 30
        $facetParameters = $this->buildFacetingParameters($allFacets, $typoScriptConfiguration);
0 ignored issues
show
Bug introduced by
It seems like $typoScriptConfiguration can also be of type null; however, parameter $typoScriptConfiguration of ApacheSolrForTypo3\Solr\...ildFacetingParameters() does only seem to accept ApacheSolrForTypo3\Solr\...TypoScriptConfiguration, maybe add an additional type check? ( Ignorable by Annotation )

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

87
        $facetParameters = $this->buildFacetingParameters($allFacets, /** @scrutinizer ignore-type */ $typoScriptConfiguration);
Loading history...
88 30
        foreach ($facetParameters as $facetParameter => $value) {
89 30
            if (strtolower($facetParameter) === 'facet.field') {
90 3
                $faceting->setFields($value);
91
            } else {
92 30
                $faceting->addAdditionalParameter($facetParameter, $value);
93
            }
94
        }
95
96 30
        $searchArguments = $this->searchRequest->getArguments();
97
98 30
        $keepAllFacetsOnSelection = $typoScriptConfiguration->getSearchFacetingKeepAllFacetsOnSelection();
99 30
        $facetFilters = $this->addFacetQueryFilters($searchArguments, $keepAllFacetsOnSelection, $allFacets);
100
101 30
        $queryBuilder = new QueryBuilder($typoScriptConfiguration);
102 30
        $queryBuilder->startFrom($query)->useFaceting($faceting)->useFilterArray($facetFilters);
103 30
        return $query;
104
    }
105
106
    /**
107
     * Delegates the parameter building to specialized functions depending on
108
     * the type of facet to add.
109
     * @param $allFacets
110
     * @param TypoScriptConfiguration $typoScriptConfiguration
111
     * @return array
112
     * @throws InvalidFacetPackageException
113
     * @throws InvalidQueryBuilderException
114
     */
115 30
    protected function buildFacetingParameters($allFacets, TypoScriptConfiguration $typoScriptConfiguration): array
116
    {
117 30
        $facetParameters = [];
118
119 30
        foreach ($allFacets as $facetName => $facetConfiguration) {
120 30
            $facetName = substr($facetName, 0, -1);
121 30
            $type = $facetConfiguration['type'] ?? 'options';
122 30
            $facetParameterBuilder = $this->facetRegistry->getPackage($type)->getQueryBuilder();
123
124 30
            if (is_null($facetParameterBuilder)) {
125
                throw new InvalidArgumentException('No query build configured for facet ' . htmlspecialchars($facetName));
126
            }
127
128 30
            $facetParameters = array_merge_recursive($facetParameters, $facetParameterBuilder->build($facetName, $typoScriptConfiguration));
129
        }
130
131 30
        return $facetParameters;
132
    }
133
134
    /**
135
     * Adds filters specified through HTTP GET as filter query parameters to
136
     * the Solr query.
137
     *
138
     * @param array $resultParameters
139
     * @param bool $keepAllFacetsOnSelection
140
     * @param array|null $allFacets
141
     * @return array
142
     *
143
     * @throws InvalidFacetPackageException
144
     * @throws InvalidUrlDecoderException
145
     */
146 30
    protected function addFacetQueryFilters(array $resultParameters, bool $keepAllFacetsOnSelection, ?array $allFacets): array
147
    {
148 30
        $facetFilters = [];
149
150 30
        if (!is_array($resultParameters['filter'] ?? null)) {
151 19
            return $facetFilters;
152
        }
153
154 11
        $filtersByFacetName = $this->getFiltersByFacetName($resultParameters, $allFacets);
155
156 11
        foreach ($filtersByFacetName as $facetName => $filterValues) {
157 11
            $facetConfiguration = $allFacets[$facetName . '.'];
158 11
            $tag = $this->getFilterTag($facetConfiguration, $keepAllFacetsOnSelection);
159 11
            $filterParts = $this->getFilterParts($facetConfiguration, $facetName, $filterValues);
160
161 11
            if (!empty($filterParts)) {
162 11
                $operator = (($facetConfiguration['operator'] ?? null) === 'OR') ? ' OR ' : ' AND ';
163 11
                $facetFilters[$facetName] = $tag . '(' . implode($operator, $filterParts) . ')';
164
            }
165
        }
166
167 11
        return $facetFilters;
168
    }
169
170
    /**
171
     * Builds the tag part of the query depending on the keepAllOptionsOnSelection configuration or the global configuration
172
     * keepAllFacetsOnSelection.
173
     *
174
     * @param array $facetConfiguration
175
     * @param bool $keepAllFacetsOnSelection
176
     * @return string
177
     */
178 11
    protected function getFilterTag(array $facetConfiguration, bool $keepAllFacetsOnSelection): string
179
    {
180 11
        $tag = '';
181 11
        if (($facetConfiguration['keepAllOptionsOnSelection'] ?? null) == 1 || ($facetConfiguration['addFieldAsTag'] ?? null) == 1 || $keepAllFacetsOnSelection) {
182 6
            $tag = '{!tag=' . addslashes($facetConfiguration['field']) . '}';
183
        }
184
185 11
        return $tag;
186
    }
187
188
    /**
189
     * This method is used to build the filter parts of the query.
190
     *
191
     * @param array $facetConfiguration
192
     * @param string $facetName
193
     * @param array $filterValues
194
     * @return array
195
     * @throws InvalidFacetPackageException
196
     * @throws InvalidUrlDecoderException
197
     */
198 11
    protected function getFilterParts(array $facetConfiguration, string $facetName, array $filterValues): array
199
    {
200 11
        $filterParts = [];
201
202 11
        $type = $facetConfiguration['type'] ?? 'options';
203 11
        $filterEncoder = $this->facetRegistry->getPackage($type)->getUrlDecoder();
204
205 11
        if (is_null($filterEncoder)) {
206
            throw new InvalidArgumentException('No encoder configured for facet ' . htmlspecialchars($facetName));
207
        }
208
209 11
        foreach ($filterValues as $filterValue) {
210 11
            $filterOptions = isset($facetConfiguration['type']) ? ($facetConfiguration[$facetConfiguration['type'] . '.'] ?? null) : null;
211 11
            if (empty($filterOptions)) {
212 10
                $filterOptions = [];
213
            }
214
215 11
            $filterValue = $filterEncoder->decode($filterValue, $filterOptions);
216 11
            if (($facetConfiguration['field'] ?? '') !== '' && $filterValue !== '') {
217 11
                $filterParts[] = $facetConfiguration['field'] . ':' . $filterValue;
218
            } else {
219
                /** @var $logger \TYPO3\CMS\Core\Log\Logger */
220
                $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
221
                $logger->warning('Invalid filter options found, skipping.', ['facet' => $facetName, 'configuration' => $facetConfiguration]);
222
            }
223
        }
224
225 11
        return $filterParts;
226
    }
227
228
    /**
229
     * Groups facet values by facet name.
230
     *
231
     * @param array $resultParameters
232
     * @param array $allFacets
233
     * @return array
234
     */
235 12
    protected function getFiltersByFacetName(array $resultParameters, array $allFacets): array
236
    {
237
        // format for filter URL parameter:
238
        // tx_solr[filter]=$facetName0:$facetValue0,$facetName1:$facetValue1,$facetName2:$facetValue2
239 12
        $filters = array_map('rawurldecode', $resultParameters['filter']);
240
        // $filters look like ['name:value1','name:value2','fieldname2:foo']
241 12
        $configuredFacets = $this->getFacetNamesWithConfiguredField($allFacets);
242
        // first group the filters by facetName - so that we can
243
        // decide later whether we need to do AND or OR for multiple
244
        // filters for a certain facet/field
245
        // $filtersByFacetName look like ['name' =>  ['value1', 'value2'], 'fieldname2' => ['foo']]
246 12
        $filtersByFacetName = [];
247 12
        if ($this->searchRequest->getContextTypoScriptConfiguration()->getSearchFacetingUrlParameterStyle() === UrlFacetContainer::PARAMETER_STYLE_ASSOC) {
248 1
            $filters = array_keys($filters);
249
        }
250 12
        foreach ($filters as $filter) {
251 12
            if (strpos($filter, ':') === false) {
252
                continue;
253
            }
254
            // only split by the first colon to allow using colons in the filter value itself
255 12
            list($filterFacetName, $filterValue) = explode(':', $filter, 2);
256 12
            if (in_array($filterFacetName, $configuredFacets)) {
257 12
                $filtersByFacetName[$filterFacetName][] = $filterValue;
258
            }
259
        }
260
261 12
        return $filtersByFacetName;
262
    }
263
264
    /**
265
     * Gets the facets as configured through TypoScript
266
     *
267
     * @param array $allFacets
268
     * @return array An array of facet names as specified in TypoScript
269
     */
270 12
    protected function getFacetNamesWithConfiguredField(array $allFacets): array
271
    {
272 12
        $facets = [];
273
274 12
        foreach ($allFacets as $facetName => $facetConfiguration) {
275 12
            $facetName = substr($facetName, 0, -1);
276
277 12
            if (empty($facetConfiguration['field'])) {
278
                // TODO later check for query and date, too
279
                continue;
280
            }
281
282 12
            $facets[] = $facetName;
283
        }
284
285 12
        return $facets;
286
    }
287
}
288