Passed
Push — master ( 5adbd6...5901ba )
by Timo
20:55
created

Faceting::modifyQuery()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5.005

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 16
cts 17
cp 0.9412
rs 8.439
c 0
b 0
f 0
cc 5
eloc 18
nc 9
nop 1
crap 5.005
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
        $keepAllOptionsOnSelection = $typoScriptConfiguration->getSearchFacetingKeepAllFacetsOnSelection();
100 33
        $facetFilters = $this->addFacetQueryFilters($searchArguments, $allFacets, $keepAllOptionsOnSelection);
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 $keepAllOptionsOnSelection
140
     * @return array
141
     */
142 33
    protected function addFacetQueryFilters($resultParameters, $allFacets, $keepAllOptionsOnSelection)
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 5
        foreach ($filtersByFacetName as $facetName => $filterValues) {
152 5
            $facetConfiguration = $allFacets[$facetName . '.'];
153 5
            $type = isset($facetConfiguration['type']) ? $facetConfiguration['type'] : 'options';
154 5
            $filterEncoder = $this->facetRegistry->getPackage($type)->getUrlDecoder();
155
156 5
            if (is_null($filterEncoder)) {
157
                throw new \InvalidArgumentException('No encoder configured for facet ' . htmlspecialchars($facetName));
158
            }
159
160 5
            $tag = '';
161 5
            if ($facetConfiguration['keepAllOptionsOnSelection'] == 1 || $keepAllOptionsOnSelection) {
162 2
                $tag = '{!tag=' . addslashes($facetConfiguration['field']) . '}';
163
            }
164
165 5
            $filterParts = [];
166 5
            foreach ($filterValues as $filterValue) {
167 5
                $filterOptions = $facetConfiguration[$facetConfiguration['type'] . '.'];
168 5
                if (empty($filterOptions)) {
169 5
                    $filterOptions = [];
170
                }
171
172 5
                $filterValue = $filterEncoder->decode($filterValue, $filterOptions);
173 5
                $filterParts[] = $facetConfiguration['field'] . ':' . $filterValue;
174
            }
175
176 5
            $operator = ($facetConfiguration['operator'] == 'OR') ? ' OR ' : ' AND ';
177 5
            $facetFilters[] = $tag . '(' . implode($operator, $filterParts) . ')';
178
        }
179 5
        return $facetFilters;
180
    }
181
182
    /**
183
     * Groups facet values by facet name.
184
     *
185
     * @param array $resultParameters
186
     * @param array $allFacets
187
     * @return array
188
     */
189 5
    protected function getFiltersByFacetName($resultParameters, $allFacets)
190
    {
191
        // format for filter URL parameter:
192
        // tx_solr[filter]=$facetName0:$facetValue0,$facetName1:$facetValue1,$facetName2:$facetValue2
193 5
        $filters = array_map('urldecode', $resultParameters['filter']);
194
        // $filters look like ['name:value1','name:value2','fieldname2:foo']
195 5
        $configuredFacets = $this->getFacetNamesWithConfiguredField($allFacets);
196
        // first group the filters by facetName - so that we can
197
        // decide later whether we need to do AND or OR for multiple
198
        // filters for a certain facet/field
199
        // $filtersByFacetName look like ['name' =>  ['value1', 'value2'], 'fieldname2' => ['foo']]
200 5
        $filtersByFacetName = [];
201 5
        foreach ($filters as $filter) {
202
            // only split by the first colon to allow using colons in the filter value itself
203 5
            list($filterFacetName, $filterValue) = explode(':', $filter, 2);
204 5
            if (in_array($filterFacetName, $configuredFacets)) {
205 5
                $filtersByFacetName[$filterFacetName][] = $filterValue;
206
            }
207
        }
208
209 5
        return $filtersByFacetName;
210
    }
211
212
    /**
213
     * Gets the facets as configured through TypoScript
214
     *
215
     * @param array $allFacets
216
     * @return array An array of facet names as specified in TypoScript
217
     */
218 5
    protected function getFacetNamesWithConfiguredField(array $allFacets)
219
    {
220 5
        $facets = [];
221
222 5
        foreach ($allFacets as $facetName => $facetConfiguration) {
223 5
            $facetName = substr($facetName, 0, -1);
224
225 5
            if (empty($facetConfiguration['field'])) {
226
                // TODO later check for query and date, too
227
                continue;
228
            }
229
230 5
            $facets[] = $facetName;
231
        }
232
233 5
        return $facets;
234
    }
235
}
236