1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the TYPO3 CMS project. |
5
|
|
|
* |
6
|
|
|
* It is free software; you can redistribute it and/or modify it under |
7
|
|
|
* the terms of the GNU General Public License, either version 2 |
8
|
|
|
* of the License, or any later version. |
9
|
|
|
* |
10
|
|
|
* For the full copyright and license information, please read the |
11
|
|
|
* LICENSE.txt file that was distributed with this source code. |
12
|
|
|
* |
13
|
|
|
* The TYPO3 project - inspiring people to share! |
14
|
|
|
*/ |
15
|
|
|
|
16
|
|
|
namespace ApacheSolrForTypo3\Solr\Domain\Search\ResultSet; |
17
|
|
|
|
18
|
|
|
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\AbstractFacet; |
19
|
|
|
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\FacetRegistry; |
20
|
|
|
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\InvalidFacetPackageException; |
21
|
|
|
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\RequirementsService; |
22
|
|
|
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Sorting\Sorting; |
23
|
|
|
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Spellchecking\Suggestion; |
24
|
|
|
use stdClass; |
25
|
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility; |
26
|
|
|
use TYPO3\CMS\Extbase\Object\ObjectManager; |
27
|
|
|
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; |
28
|
|
|
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; |
29
|
|
|
use UnexpectedValueException; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* This processor is used to transform the solr response into a |
33
|
|
|
* domain object hierarchy that can be used in the application (controller and view). |
34
|
|
|
* |
35
|
|
|
* @author Frans Saris <[email protected]> |
36
|
|
|
* @author Timo Hund <[email protected]> |
37
|
|
|
*/ |
38
|
|
|
class ResultSetReconstitutionProcessor implements SearchResultSetProcessor |
39
|
|
|
{ |
40
|
|
|
/** |
41
|
|
|
* @var ObjectManagerInterface|null |
42
|
|
|
*/ |
43
|
|
|
protected ?ObjectManagerInterface $objectManager = null; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @return ObjectManagerInterface |
47
|
|
|
*/ |
48
|
45 |
|
public function getObjectManager(): ?ObjectManagerInterface |
49
|
|
|
{ |
50
|
45 |
|
if ($this->objectManager === null) { |
51
|
17 |
|
$this->objectManager = GeneralUtility::makeInstance(ObjectManager::class); |
52
|
|
|
} |
53
|
45 |
|
return $this->objectManager; |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @param ObjectManagerInterface $objectManager |
58
|
|
|
* Purpose: PhpUnit |
59
|
|
|
* @todo: Replace with proper DI |
60
|
|
|
*/ |
61
|
28 |
|
public function setObjectManager(ObjectManagerInterface $objectManager) |
62
|
|
|
{ |
63
|
28 |
|
$this->objectManager = $objectManager; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @return FacetRegistry |
68
|
|
|
*/ |
69
|
45 |
|
protected function getFacetRegistry(): FacetRegistry |
70
|
|
|
{ |
71
|
|
|
// @extensionScannerIgnoreLine |
72
|
45 |
|
return $this->getObjectManager()->get(FacetRegistry::class); |
|
|
|
|
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* The implementation can be used to influence a SearchResultSet that is |
77
|
|
|
* created and processed in the SearchResultSetService. |
78
|
|
|
* |
79
|
|
|
* @param SearchResultSet $resultSet |
80
|
|
|
* @return SearchResultSet |
81
|
|
|
* @throws InvalidFacetPackageException |
82
|
|
|
*/ |
83
|
70 |
|
public function process(SearchResultSet $resultSet): SearchResultSet |
84
|
|
|
{ |
85
|
70 |
|
$resultSet = $this->parseSpellCheckingResponseIntoObjects($resultSet); |
86
|
70 |
|
$resultSet = $this->parseSortingIntoObjects($resultSet); |
87
|
|
|
|
88
|
|
|
// here we can reconstitute other domain objects from the solr response |
89
|
70 |
|
return $this->parseFacetsIntoObjects($resultSet); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @param SearchResultSet $resultSet |
94
|
|
|
* @return SearchResultSet |
95
|
|
|
*/ |
96
|
70 |
|
protected function parseSortingIntoObjects(SearchResultSet $resultSet): SearchResultSet |
97
|
|
|
{ |
98
|
70 |
|
$configuration = $resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration(); |
99
|
70 |
|
$activeSortings = $resultSet->getUsedSearchRequest()->getSeperatedSortings(); |
100
|
70 |
|
$hasSorting = $resultSet->getUsedSearchRequest()->getHasSorting(); |
101
|
|
|
|
102
|
|
|
// no configuration available |
103
|
70 |
|
if (!isset($configuration)) { |
104
|
7 |
|
return $resultSet; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
// no sorting enabled |
108
|
63 |
|
if (!$configuration->getSearchSorting()) { |
109
|
61 |
|
return $resultSet; |
110
|
|
|
} |
111
|
2 |
|
foreach ($configuration->getSearchSortingOptionsConfiguration() as $sortingKeyName => $sortingOptions) { |
112
|
2 |
|
$sortingName = rtrim($sortingKeyName, '.'); |
113
|
2 |
|
$selected = false; |
114
|
2 |
|
$direction = $configuration->getSearchSortingDefaultOrderBySortOptionName($sortingName); |
115
|
|
|
|
116
|
|
|
// when we have an active sorting in the request we compare the sortingName and mark is as active and |
117
|
|
|
// use the direction from the request |
118
|
2 |
|
if ($hasSorting && array_key_exists($sortingName, $activeSortings)) { |
119
|
1 |
|
$selected = true; |
120
|
1 |
|
$direction = $activeSortings[$sortingName]; |
121
|
|
|
} |
122
|
|
|
|
123
|
2 |
|
$field = $sortingOptions['field']; |
124
|
2 |
|
$label = $sortingOptions['label']; |
125
|
|
|
|
126
|
2 |
|
$isResetOption = $field === 'relevance'; |
127
|
|
|
|
128
|
|
|
// Allow stdWrap on label: |
129
|
2 |
|
$labelHasSubConfiguration = is_array($sortingOptions['label.'] ?? null); |
130
|
2 |
|
if ($labelHasSubConfiguration) { |
131
|
|
|
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); |
132
|
|
|
$label = $cObj->stdWrap($label, $sortingOptions['label.']); |
133
|
|
|
} |
134
|
|
|
|
135
|
2 |
|
if ($isResetOption && !$hasSorting) { |
136
|
1 |
|
$selected = true; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** @noinspection PhpParamsInspection */ |
140
|
2 |
|
$sorting = $this->getObjectManager()->get( |
|
|
|
|
141
|
2 |
|
Sorting::class, |
142
|
2 |
|
$resultSet, |
143
|
2 |
|
$sortingName, |
144
|
2 |
|
$field, |
145
|
2 |
|
$direction, |
146
|
2 |
|
$label, |
147
|
2 |
|
$selected, |
148
|
2 |
|
$isResetOption |
149
|
2 |
|
); |
150
|
2 |
|
$resultSet->addSorting($sorting); |
151
|
|
|
} |
152
|
|
|
|
153
|
2 |
|
return $resultSet; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* @param SearchResultSet $resultSet |
158
|
|
|
* @return SearchResultSet |
159
|
|
|
*/ |
160
|
70 |
|
private function parseSpellCheckingResponseIntoObjects(SearchResultSet $resultSet): SearchResultSet |
161
|
|
|
{ |
162
|
|
|
//read the response |
163
|
70 |
|
$response = $resultSet->getResponse(); |
164
|
|
|
|
165
|
70 |
|
if (!is_array($response->spellcheck->suggestions ?? null)) { |
166
|
67 |
|
return $resultSet; |
167
|
|
|
} |
168
|
|
|
|
169
|
3 |
|
$misspelledTerm = ''; |
170
|
3 |
|
foreach ($response->spellcheck->suggestions as $key => $suggestionData) { |
171
|
3 |
|
if (is_string($suggestionData)) { |
172
|
3 |
|
$misspelledTerm = $key; |
173
|
3 |
|
continue; |
174
|
|
|
} |
175
|
|
|
|
176
|
3 |
|
if ($misspelledTerm === '') { |
177
|
|
|
throw new UnexpectedValueException('No misspelled term before suggestion'); |
178
|
|
|
} |
179
|
|
|
|
180
|
3 |
|
if (!is_object($suggestionData) && !is_array($suggestionData->suggestion)) { |
181
|
|
|
continue; |
182
|
|
|
} |
183
|
|
|
|
184
|
3 |
|
foreach ($suggestionData->suggestion as $suggestedTerm) { |
185
|
3 |
|
$suggestion = $this->createSuggestionFromResponseFragment($suggestionData, $suggestedTerm, $misspelledTerm); |
186
|
|
|
//add it to the resultSet |
187
|
3 |
|
$resultSet->addSpellCheckingSuggestion($suggestion); |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
191
|
3 |
|
return $resultSet; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* @param stdClass $suggestionData |
196
|
|
|
* @param string $suggestedTerm |
197
|
|
|
* @param string $misspelledTerm |
198
|
|
|
* @return Suggestion |
199
|
|
|
*/ |
200
|
3 |
|
private function createSuggestionFromResponseFragment( |
201
|
|
|
stdClass $suggestionData, |
202
|
|
|
string $suggestedTerm, |
203
|
|
|
string $misspelledTerm |
204
|
|
|
): Suggestion { |
205
|
3 |
|
$numFound = $suggestionData->numFound ?? 0; |
206
|
3 |
|
$startOffset = $suggestionData->startOffset ?? 0; |
207
|
3 |
|
$endOffset = $suggestionData->endOffset ?? 0; |
208
|
|
|
|
209
|
|
|
// by now we avoid to use GeneralUtility::makeInstance, since we only create a value object |
210
|
|
|
// and the usage might be an overhead. |
211
|
3 |
|
return new Suggestion($suggestedTerm, $misspelledTerm, $numFound, $startOffset, $endOffset); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* Parse available facets into objects |
216
|
|
|
* |
217
|
|
|
* @param SearchResultSet $resultSet |
218
|
|
|
* @return SearchResultSet |
219
|
|
|
* @throws InvalidFacetPackageException |
220
|
|
|
*/ |
221
|
70 |
|
private function parseFacetsIntoObjects(SearchResultSet $resultSet): SearchResultSet |
222
|
|
|
{ |
223
|
|
|
// Make sure we can access the facet configuration |
224
|
70 |
|
if (!$resultSet->getUsedSearchRequest() || !$resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()) { |
225
|
7 |
|
return $resultSet; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
// Read the response |
229
|
63 |
|
$response = $resultSet->getResponse(); |
230
|
63 |
|
if (!is_object($response->facet_counts) && !is_object($response->facets)) { |
231
|
18 |
|
return $resultSet; |
232
|
|
|
} |
233
|
|
|
|
234
|
45 |
|
$facetRegistry = $this->getFacetRegistry(); |
235
|
45 |
|
$facetsConfiguration = $resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()->getSearchFacetingFacets(); |
236
|
|
|
|
237
|
45 |
|
foreach ($facetsConfiguration as $name => $options) { |
238
|
43 |
|
if (!is_array($options)) { |
239
|
|
|
continue; |
240
|
|
|
} |
241
|
43 |
|
$facetName = rtrim($name, '.'); |
242
|
43 |
|
$type = !empty($options['type']) ? $options['type'] : ''; |
243
|
|
|
|
244
|
43 |
|
$parser = $facetRegistry->getPackage($type)->getParser(); |
245
|
43 |
|
$facet = $parser->parse($resultSet, $facetName, $options); |
246
|
43 |
|
if ($facet !== null) { |
247
|
43 |
|
$resultSet->addFacet($facet); |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
|
251
|
45 |
|
$this->applyRequirements($resultSet); |
252
|
|
|
|
253
|
45 |
|
return $resultSet; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* @param SearchResultSet $resultSet |
258
|
|
|
*/ |
259
|
45 |
|
protected function applyRequirements(SearchResultSet $resultSet) |
260
|
|
|
{ |
261
|
45 |
|
$requirementsService = $this->getRequirementsService(); |
262
|
45 |
|
$facets = $resultSet->getFacets(); |
263
|
45 |
|
foreach ($facets as $facet) { |
264
|
|
|
/** @var $facet AbstractFacet */ |
265
|
43 |
|
$requirementsMet = $requirementsService->getAllRequirementsMet($facet); |
266
|
43 |
|
$facet->setAllRequirementsMet($requirementsMet); |
267
|
|
|
} |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* @return RequirementsService |
272
|
|
|
*/ |
273
|
45 |
|
protected function getRequirementsService(): RequirementsService |
274
|
|
|
{ |
275
|
|
|
// @extensionScannerIgnoreLine |
276
|
45 |
|
return $this->getObjectManager()->get(RequirementsService::class); |
|
|
|
|
277
|
|
|
} |
278
|
|
|
} |
279
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.