Passed
Pull Request — main (#3339)
by Rafael
06:51 queued 02:48
created

Faceting   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Test Coverage

Coverage 96.15%

Importance

Changes 0
Metric Value
wmc 28
eloc 73
c 0
b 0
f 0
dl 0
loc 232
ccs 75
cts 78
cp 0.9615
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getFiltersByFacetName() 0 24 4
A addFacetQueryFilters() 0 19 4
A __construct() 0 3 1
A getFacetNamesWithConfiguredField() 0 16 3
A getFilterTag() 0 8 4
A setSearchRequest() 0 3 1
A modifyQuery() 0 23 3
A buildFacetingParameters() 0 17 3
A getFilterParts() 0 22 5
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
33
/**
34
 * Modifies a query to add faceting parameters
35
 *
36
 * @author Ingo Renner <[email protected]>
37
 * @author Daniel Poetzinger <[email protected]>
38
 * @author Sebastian Kurfuerst <[email protected]>
39
 */
40
class Faceting implements Modifier, SearchRequestAware
41
{
42
    /**
43
     * @var FacetRegistry
44
     */
45
    protected FacetRegistry $facetRegistry;
46
47
    /**
48
     * @var SearchRequest|null
49
     */
50
    protected ?SearchRequest $searchRequest = null;
51
52
    /**
53
     * @param FacetRegistry $facetRegistry
54
     */
55 31
    public function __construct(FacetRegistry $facetRegistry)
56
    {
57 31
        $this->facetRegistry = $facetRegistry;
58
    }
59
60
    /**
61
     * @param SearchRequest $searchRequest
62
     */
63 31
    public function setSearchRequest(SearchRequest $searchRequest)
64
    {
65 31
        $this->searchRequest = $searchRequest;
66
    }
67
68
    /**
69
     * Modifies the given query and adds the parameters necessary for faceted
70
     * search.
71
     *
72
     * @param Query $query The query to modify
73
     * @return Query The modified query with faceting parameters
74
     *
75
     * @throws InvalidFacetPackageException
76
     * @throws InvalidQueryBuilderException
77
     * @throws InvalidUrlDecoderException
78
     */
79 30
    public function modifyQuery(Query $query): Query
80
    {
81 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

81
        /** @scrutinizer ignore-call */ 
82
        $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...
82 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

82
        $faceting = FacetingBuilder::fromTypoScriptConfiguration(/** @scrutinizer ignore-type */ $typoScriptConfiguration);
Loading history...
83
84 30
        $allFacets = $typoScriptConfiguration->getSearchFacetingFacets();
85 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

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