Passed
Pull Request — main (#3548)
by
unknown
30:10
created

Faceting::getFacetNamesWithConfiguredField()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 16
ccs 2
cts 2
cp 1
rs 10
cc 3
nc 3
nop 1
crap 3
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 31
     * @param FacetRegistry $facetRegistry
56
     */
57 31
    public function __construct(FacetRegistry $facetRegistry)
58
    {
59
        $this->facetRegistry = $facetRegistry;
60
    }
61
62
    /**
63 31
     * @param SearchRequest $searchRequest
64
     */
65 31
    public function setSearchRequest(SearchRequest $searchRequest)
66
    {
67
        $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 30
     * @throws InvalidUrlDecoderException
80
     */
81 30
    public function modifyQuery(Query $query): Query
82 30
    {
83
        $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 30
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 3
        foreach ($facetParameters as $facetParameter => $value) {
89
            if (strtolower($facetParameter) === 'facet.field') {
90 30
                $faceting->setFields($value);
91
            } else {
92
                $faceting->addAdditionalParameter($facetParameter, $value);
93
            }
94 30
        }
95
96 30
        $searchArguments = $this->searchRequest->getArguments();
97 30
98
        $keepAllFacetsOnSelection = $typoScriptConfiguration->getSearchFacetingKeepAllFacetsOnSelection();
99 30
        $facetFilters = $this->addFacetQueryFilters($searchArguments, $keepAllFacetsOnSelection, $allFacets);
100 30
101 30
        $queryBuilder = new QueryBuilder($typoScriptConfiguration);
102
        $queryBuilder->startFrom($query)->useFaceting($faceting)->useFilterArray($facetFilters);
103
        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 30
     * @throws InvalidQueryBuilderException
114
     */
115 30
    protected function buildFacetingParameters($allFacets, TypoScriptConfiguration $typoScriptConfiguration): array
116
    {
117 30
        $facetParameters = [];
118 30
119 30
        foreach ($allFacets as $facetName => $facetConfiguration) {
120 30
            $facetName = substr($facetName, 0, -1);
121
            $type = $facetConfiguration['type'] ?? 'options';
122 30
            $facetParameterBuilder = $this->facetRegistry->getPackage($type)->getQueryBuilder();
123
124
            if (is_null($facetParameterBuilder)) {
125
                throw new InvalidArgumentException('No query build configured for facet ' . htmlspecialchars($facetName));
126 30
            }
127
128
            $facetParameters = array_merge_recursive($facetParameters, $facetParameterBuilder->build($facetName, $typoScriptConfiguration));
129 30
        }
130
131
        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 30
     * @throws InvalidUrlDecoderException
145
     */
146 30
    protected function addFacetQueryFilters(array $resultParameters, bool $keepAllFacetsOnSelection, ?array $allFacets): array
147
    {
148 30
        $facetFilters = [];
149 19
150
        if (!is_array($resultParameters['filter'] ?? null)) {
151
            return $facetFilters;
152 11
        }
153
154 11
        $filtersByFacetName = $this->getFiltersByFacetName($resultParameters, $allFacets);
155 11
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
            if (!empty($filterParts)) {
162 11
                $operator = (($facetConfiguration['operator'] ?? null) === 'OR') ? ' OR ' : ' AND ';
163
                $facetFilters[$facetName] = $tag . '(' . implode($operator, $filterParts) . ')';
164
            }
165
        }
166
167
        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 11
     *
174
     * @param array $facetConfiguration
175 11
     * @param bool $keepAllFacetsOnSelection
176 11
     * @return string
177 6
     */
178
    protected function getFilterTag(array $facetConfiguration, bool $keepAllFacetsOnSelection): string
179
    {
180 11
        $tag = '';
181
        if (($facetConfiguration['keepAllOptionsOnSelection'] ?? null) == 1 || ($facetConfiguration['addFieldAsTag'] ?? null) == 1 || $keepAllFacetsOnSelection) {
182
            $tag = '{!tag=' . addslashes($facetConfiguration['field']) . '}';
183
        }
184
185
        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 11
     * @param array $filterValues
194
     * @return array
195 11
     * @throws InvalidFacetPackageException
196
     * @throws InvalidUrlDecoderException
197 11
     */
198 11
    protected function getFilterParts(array $facetConfiguration, string $facetName, array $filterValues): array
199
    {
200 11
        $filterParts = [];
201
202
        $type = $facetConfiguration['type'] ?? 'options';
203
        $filterEncoder = $this->facetRegistry->getPackage($type)->getUrlDecoder();
204 11
205 11
        if (is_null($filterEncoder)) {
206 11
            throw new InvalidArgumentException('No encoder configured for facet ' . htmlspecialchars($facetName));
207 10
        }
208
209
        foreach ($filterValues as $filterValue) {
210 11
            $filterOptions = isset($facetConfiguration['type']) ? ($facetConfiguration[$facetConfiguration['type'] . '.'] ?? null) : null;
211 11
            if (empty($filterOptions)) {
212
                $filterOptions = [];
213
            }
214 11
215
            $filterValue = $filterEncoder->decode($filterValue, $filterOptions);
216
            if (($facetConfiguration['field'] ?? '') !== '' && $filterValue !== '') {
217
                $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 12
225
        return $filterParts;
226
    }
227
228 12
    /**
229
     * Groups facet values by facet name.
230 12
     *
231
     * @param array $resultParameters
232
     * @param array $allFacets
233
     * @return array
234
     */
235 12
    protected function getFiltersByFacetName(array $resultParameters, array $allFacets): array
236 12
    {
237 1
        // format for filter URL parameter:
238
        // tx_solr[filter]=$facetName0:$facetValue0,$facetName1:$facetValue1,$facetName2:$facetValue2
239 12
        $filters = array_map('rawurldecode', $resultParameters['filter']);
240 12
        // $filters look like ['name:value1','name:value2','fieldname2:foo']
241
        $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 12
        // filters for a certain facet/field
245 12
        // $filtersByFacetName look like ['name' =>  ['value1', 'value2'], 'fieldname2' => ['foo']]
246 12
        $filtersByFacetName = [];
247
        if ($this->searchRequest->getContextTypoScriptConfiguration()->getSearchFacetingUrlParameterStyle() === UrlFacetContainer::PARAMETER_STYLE_ASSOC) {
248
            $filters = array_keys($filters);
249
        }
250 12
        foreach ($filters as $filter) {
251
            if (strpos($filter, ':') === false) {
252
                continue;
253
            }
254
            // only split by the first colon to allow using colons in the filter value itself
255
            list($filterFacetName, $filterValue) = explode(':', $filter, 2);
256
            if (in_array($filterFacetName, $configuredFacets)) {
257
                $filtersByFacetName[$filterFacetName][] = $filterValue;
258
            }
259 12
        }
260
261 12
        return $filtersByFacetName;
262
    }
263 12
264 12
    /**
265
     * Gets the facets as configured through TypoScript
266 12
     *
267
     * @param array $allFacets
268
     * @return array An array of facet names as specified in TypoScript
269
     */
270
    protected function getFacetNamesWithConfiguredField(array $allFacets): array
271 12
    {
272
        $facets = [];
273
274 12
        foreach ($allFacets as $facetName => $facetConfiguration) {
275
            $facetName = substr($facetName, 0, -1);
276
277
            if (empty($facetConfiguration['field'])) {
278
                // TODO later check for query and date, too
279
                continue;
280
            }
281
282
            $facets[] = $facetName;
283
        }
284
285
        return $facets;
286
    }
287
}
288