Completed
Push — master ( 5901ba...a00e5e )
by Timo
23:23
created

Faceting   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 225
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 95.06%

Importance

Changes 0
Metric Value
wmc 30
lcom 1
cbo 10
dl 0
loc 225
ccs 77
cts 81
cp 0.9506
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 2
A setSearchRequest() 0 4 1
B modifyQuery() 0 29 5
A buildFacetingParameters() 0 18 4
A addFacetQueryFilters() 0 20 4
A getFilterTag() 0 9 3
B getFilterParts() 0 23 5
A getFiltersByFacetName() 0 22 3
A getFacetNamesWithConfiguredField() 0 17 3
1
<?php
2
namespace ApacheSolrForTypo3\Solr\Query\Modifier;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2009-2015 Ingo Renner <[email protected]>
8
 *  All rights reserved
9
 *
10
 *  This script is part of the TYPO3 project. The TYPO3 project is
11
 *  free software; you can redistribute it and/or modify
12
 *  it under the terms of the GNU General Public License as published by
13
 *  the Free Software Foundation; either version 2 of the License, or
14
 *  (at your option) any later version.
15
 *
16
 *  The GNU General Public License can be found at
17
 *  http://www.gnu.org/copyleft/gpl.html.
18
 *
19
 *  This script is distributed in the hope that it will be useful,
20
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 *  GNU General Public License for more details.
23
 *
24
 *  This copyright notice MUST APPEAR in all copies of the script!
25
 ***************************************************************/
26
27
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\FacetRegistry;
28
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
29
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequestAware;
30
use ApacheSolrForTypo3\Solr\Query;
31
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
32
use TYPO3\CMS\Core\Utility\GeneralUtility;
33
use TYPO3\CMS\Extbase\Object\ObjectManager;
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
    /**
46
     * @var FacetRegistry
47
     */
48
    protected $facetRegistry = null;
49
50
    /**
51
     * @var SearchRequest
52
     */
53
    protected $searchRequest;
54
55
    /**
56
     * @param FacetRegistry $facetRegistry
57
     */
58 33
    public function __construct(FacetRegistry $facetRegistry = null)
59
    {
60 33
        $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
61 33
        $this->facetRegistry = is_null($facetRegistry) ? $objectManager->get(FacetRegistry::class) : $facetRegistry;
62 33
    }
63
64
    /**
65
     * @param SearchRequest $searchRequest
66
     */
67 33
    public function setSearchRequest(SearchRequest $searchRequest)
68
    {
69 33
        $this->searchRequest = $searchRequest;
70 33
    }
71
72
    /**
73
     * Modifies the given query and adds the parameters necessary for faceted
74
     * search.
75
     *
76
     * @param Query $query The query to modify
77
     * @return Query The modified query with faceting parameters
78
     */
79 33
    public function modifyQuery(Query $query)
80
    {
81 33
        $query->getFaceting()->setIsEnabled(true);
82 33
        $typoScriptConfiguration = $this->searchRequest->getContextTypoScriptConfiguration();
83 33
        $allFacets = $typoScriptConfiguration->getSearchFacetingFacets();
84
85 33
        $facetParameters = $this->buildFacetingParameters($allFacets, $typoScriptConfiguration);
86 33
        foreach ($facetParameters as $facetParameter => $value) {
87 33
            if(strtolower($facetParameter) === 'facet.field') {
88 33
                $query->getFaceting()->setFields($value);
89
            } else {
90 33
                $query->getFaceting()->addAdditionalParameter($facetParameter, $value);
91
            }
92
        }
93
94 33
        $searchArguments = $this->searchRequest->getArguments();
95 33
        if (!is_array($searchArguments)) {
96
            return $query;
97
        }
98
99 33
        $keepAllFacetsOnSelection = $typoScriptConfiguration->getSearchFacetingKeepAllFacetsOnSelection();
100 33
        $facetFilters = $this->addFacetQueryFilters($searchArguments, $allFacets, $keepAllFacetsOnSelection);
101
102 33
        foreach ($facetFilters as $filter) {
103 5
            $query->getFilters()->add($filter);
104
        }
105
106 33
        return $query;
107
    }
108
109
    /**
110
     * Delegates the parameter building to specialized functions depending on
111
     * the type of facet to add.
112
     *
113
     */
114 33
    protected function buildFacetingParameters($allFacets, TypoScriptConfiguration $typoScriptConfiguration)
115
    {
116 33
        $facetParameters = [];
117
118 33
        foreach ($allFacets as $facetName => $facetConfiguration) {
119 33
            $facetName = substr($facetName, 0, -1);
120 33
            $type = isset($facetConfiguration['type']) ? $facetConfiguration['type'] : 'options';
121 33
            $facetParameterBuilder = $this->facetRegistry->getPackage($type)->getQueryBuilder();
122
123 33
            if (is_null($facetParameterBuilder)) {
124
                throw new \InvalidArgumentException('No query build configured for facet ' . htmlspecialchars($facetName));
125
            }
126
127 33
            $facetParameters = array_merge_recursive($facetParameters, $facetParameterBuilder->build($facetName, $typoScriptConfiguration));
128
        }
129
130 33
        return $facetParameters;
131
    }
132
133
    /**
134
     * Adds filters specified through HTTP GET as filter query parameters to
135
     * the Solr query.
136
     *
137
     * @param array $resultParameters
138
     * @param array $allFacets
139
     * @param bool $keepAllFacetsOnSelection
140
     * @return array
141
     */
142 33
    protected function addFacetQueryFilters($resultParameters, $allFacets, $keepAllFacetsOnSelection)
143
    {
144 33
        $facetFilters = [];
145
146 33
        if (!is_array($resultParameters['filter'])) {
147 28
            return $facetFilters;
148
        }
149
150 5
        $filtersByFacetName = $this->getFiltersByFacetName($resultParameters, $allFacets);
151
152 5
        foreach ($filtersByFacetName as $facetName => $filterValues) {
153 5
            $facetConfiguration = $allFacets[$facetName . '.'];
154 5
            $tag = $this->getFilterTag($facetConfiguration, $keepAllFacetsOnSelection);
155 5
            $filterParts = $this->getFilterParts($facetConfiguration, $facetName, $filterValues);
156 5
            $operator = ($facetConfiguration['operator'] == 'OR') ? ' OR ' : ' AND ';
157 5
            $facetFilters[] = $tag . '(' . implode($operator, $filterParts) . ')';
158
        }
159
160 5
        return $facetFilters;
161
    }
162
163
    /**
164
     * Builds the tag part of the query depending on the keepAllOptionsOnSelection configuration or the global configuration
165
     * keepAllFacetsOnSelection.
166
     *
167
     * @param array $facetConfiguration
168
     * @param boolean $keepAllFacetsOnSelection
169
     * @return string
170
     */
171 5
    protected function getFilterTag($facetConfiguration, $keepAllFacetsOnSelection)
172
    {
173 5
        $tag = '';
174 5
        if ($facetConfiguration['keepAllOptionsOnSelection'] == 1 || $keepAllFacetsOnSelection) {
175 2
            $tag = '{!tag=' . addslashes($facetConfiguration['field']) . '}';
176
        }
177
178 5
        return $tag;
179
    }
180
181
    /**
182
     * This method is used to build the filter parts of the query.
183
     *
184
     * @param array $facetConfiguration
185
     * @param string $facetName
186
     * @param array $filterValues
187
     * @return array
188
     */
189 5
    protected function getFilterParts($facetConfiguration, $facetName, $filterValues)
190
    {
191 5
        $filterParts = [];
192
193 5
        $type = isset($facetConfiguration['type']) ? $facetConfiguration['type'] : 'options';
194 5
        $filterEncoder = $this->facetRegistry->getPackage($type)->getUrlDecoder();
195
196 5
        if (is_null($filterEncoder)) {
197
            throw new \InvalidArgumentException('No encoder configured for facet ' . htmlspecialchars($facetName));
198
        }
199
200 5
        foreach ($filterValues as $filterValue) {
201 5
            $filterOptions = $facetConfiguration[$facetConfiguration['type'] . '.'];
202 5
            if (empty($filterOptions)) {
203 5
                $filterOptions = [];
204
            }
205
206 5
            $filterValue = $filterEncoder->decode($filterValue, $filterOptions);
207 5
            $filterParts[] = $facetConfiguration['field'] . ':' . $filterValue;
208
        }
209
210 5
        return $filterParts;
211
    }
212
213
    /**
214
     * Groups facet values by facet name.
215
     *
216
     * @param array $resultParameters
217
     * @param array $allFacets
218
     * @return array
219
     */
220 5
    protected function getFiltersByFacetName($resultParameters, $allFacets)
221
    {
222
        // format for filter URL parameter:
223
        // tx_solr[filter]=$facetName0:$facetValue0,$facetName1:$facetValue1,$facetName2:$facetValue2
224 5
        $filters = array_map('urldecode', $resultParameters['filter']);
225
        // $filters look like ['name:value1','name:value2','fieldname2:foo']
226 5
        $configuredFacets = $this->getFacetNamesWithConfiguredField($allFacets);
227
        // first group the filters by facetName - so that we can
228
        // decide later whether we need to do AND or OR for multiple
229
        // filters for a certain facet/field
230
        // $filtersByFacetName look like ['name' =>  ['value1', 'value2'], 'fieldname2' => ['foo']]
231 5
        $filtersByFacetName = [];
232 5
        foreach ($filters as $filter) {
233
            // only split by the first colon to allow using colons in the filter value itself
234 5
            list($filterFacetName, $filterValue) = explode(':', $filter, 2);
235 5
            if (in_array($filterFacetName, $configuredFacets)) {
236 5
                $filtersByFacetName[$filterFacetName][] = $filterValue;
237
            }
238
        }
239
240 5
        return $filtersByFacetName;
241
    }
242
243
    /**
244
     * Gets the facets as configured through TypoScript
245
     *
246
     * @param array $allFacets
247
     * @return array An array of facet names as specified in TypoScript
248
     */
249 5
    protected function getFacetNamesWithConfiguredField(array $allFacets)
250
    {
251 5
        $facets = [];
252
253 5
        foreach ($allFacets as $facetName => $facetConfiguration) {
254 5
            $facetName = substr($facetName, 0, -1);
255
256 5
            if (empty($facetConfiguration['field'])) {
257
                // TODO later check for query and date, too
258
                continue;
259
            }
260
261 5
            $facets[] = $facetName;
262
        }
263
264 5
        return $facets;
265
    }
266
}
267