Total Complexity | 47 |
Total Lines | 382 |
Duplicated Lines | 0 % |
Changes | 3 | ||
Bugs | 0 | Features | 0 |
Complex classes like QueryComponentFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use QueryComponentFactory, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
18 | class QueryComponentFactory |
||
19 | { |
||
20 | /** |
||
21 | * @var BaseQuery |
||
22 | */ |
||
23 | protected $query; |
||
24 | |||
25 | /** |
||
26 | * @var Query |
||
27 | */ |
||
28 | protected $clientQuery; |
||
29 | |||
30 | /** |
||
31 | * @var Helper |
||
32 | */ |
||
33 | protected $helper; |
||
34 | |||
35 | /** |
||
36 | * @var array |
||
37 | */ |
||
38 | protected $boostTerms; |
||
39 | |||
40 | /** |
||
41 | * @var array |
||
42 | */ |
||
43 | protected $queryArray; |
||
44 | |||
45 | /** |
||
46 | * @var BaseIndex |
||
47 | */ |
||
48 | protected $index; |
||
49 | |||
50 | /** |
||
51 | * Build the full query |
||
52 | * @return Query |
||
53 | */ |
||
54 | public function buildQuery() |
||
55 | { |
||
56 | $this->buildTerms(); |
||
57 | $this->buildViewFilter(); |
||
58 | // Build class filtering |
||
59 | $this->buildClassFilter(); |
||
60 | // Add filters |
||
61 | $this->buildFilters(); |
||
62 | // And excludes |
||
63 | $this->buildExcludes(); |
||
64 | // Setup the facets |
||
65 | $this->buildFacets(); |
||
66 | // Build the facet filters |
||
67 | $this->buildFacetQuery(); |
||
68 | // Add spellchecking |
||
69 | $this->buildSpellcheck(); |
||
70 | // Set the start |
||
71 | $this->clientQuery->setStart($this->query->getStart()); |
||
72 | // Double the rows in case something has been deleted, but not from Solr |
||
73 | $this->clientQuery->setRows($this->query->getRows() * 2); |
||
74 | // Add highlighting before adding boosting |
||
75 | $this->clientQuery->getHighlighting()->setFields($this->query->getHighlight()); |
||
76 | // Add boosting |
||
77 | $this->buildBoosts(); |
||
78 | |||
79 | // Filter out the fields we want to see if they're set |
||
80 | if (count($this->query->getFields())) { |
||
81 | $this->clientQuery->setFields($this->query->getFields()); |
||
82 | } |
||
83 | |||
84 | return $this->clientQuery; |
||
85 | } |
||
86 | |||
87 | |||
88 | /** |
||
89 | * @return array |
||
90 | */ |
||
91 | protected function buildTerms(): array |
||
92 | { |
||
93 | $terms = $this->query->getTerms(); |
||
94 | |||
95 | $boostTerms = $this->index->getBoostTerms(); |
||
|
|||
96 | |||
97 | foreach ($terms as $search) { |
||
98 | $term = $search['text']; |
||
99 | $term = $this->escapeSearch($term, $this->helper); |
||
100 | $postfix = $this->isFuzzy($search); |
||
101 | // We can add the same term multiple times with different boosts |
||
102 | // Not ideal, but it might happen, so let's add the term itself only once |
||
103 | if (!in_array($term, $this->queryArray, true)) { |
||
104 | $this->queryArray[] = $term . $postfix; |
||
105 | } |
||
106 | // If boosting is set, add the fields to boost |
||
107 | if ($search['boost'] > 1) { |
||
108 | $boost = $this->buildQueryBoost($search, $term, $boostTerms); |
||
109 | $this->boostTerms = array_merge($boostTerms, $boost); |
||
110 | } |
||
111 | } |
||
112 | } |
||
113 | |||
114 | /** |
||
115 | * @param string $searchTerm |
||
116 | * @param Helper $helper |
||
117 | * @return string |
||
118 | */ |
||
119 | public function escapeSearch($searchTerm, Helper $helper): string |
||
120 | { |
||
121 | $term = []; |
||
122 | // Escape special characters where needed. Except for quoted parts, those should be phrased |
||
123 | preg_match_all('/"[^"]*"|\S+/', $searchTerm, $parts); |
||
124 | foreach ($parts[0] as $part) { |
||
125 | // As we split the parts, everything with two quotes is a phrase |
||
126 | if (substr_count($part, '"') === 2) { |
||
127 | $term[] = $helper->escapePhrase($part); |
||
128 | } else { |
||
129 | $term[] = $helper->escapeTerm($part); |
||
130 | } |
||
131 | } |
||
132 | |||
133 | return implode(' ', $term); |
||
134 | } |
||
135 | |||
136 | /** |
||
137 | * @param $search |
||
138 | * @return string |
||
139 | */ |
||
140 | protected function isFuzzy($search): string |
||
141 | { |
||
142 | $postfix = ''; // When doing fuzzy search, postfix, otherwise, don't |
||
143 | if ($search['fuzzy']) { |
||
144 | $postfix = '~'; |
||
145 | if (is_numeric($search['fuzzy'])) { |
||
146 | $postfix .= $search['fuzzy']; |
||
147 | } |
||
148 | } |
||
149 | |||
150 | return $postfix; |
||
151 | } |
||
152 | |||
153 | /** |
||
154 | * Set boosting at Query time |
||
155 | * |
||
156 | * @param array $search |
||
157 | * @param string $term |
||
158 | * @param array $searchQuery |
||
159 | * @return array |
||
160 | */ |
||
161 | protected function buildQueryBoost($search, string $term, array $searchQuery): array |
||
162 | { |
||
163 | foreach ($search['fields'] as $boostField) { |
||
164 | $boostField = str_replace('.', '_', $boostField); |
||
165 | $criteria = Criteria::where($boostField) |
||
166 | ->is($term) |
||
167 | ->boost($search['boost']); |
||
168 | $searchQuery[] = $criteria->getQuery(); |
||
169 | } |
||
170 | |||
171 | return $searchQuery; |
||
172 | } |
||
173 | |||
174 | /** |
||
175 | * |
||
176 | */ |
||
177 | protected function buildViewFilter(): void |
||
178 | { |
||
179 | // Filter by what the user is allowed to see |
||
180 | $viewIDs = ['1-null']; // null is always an option as that means publicly visible |
||
181 | $currentUser = Security::getCurrentUser(); |
||
182 | if ($currentUser && $currentUser->exists()) { |
||
183 | $viewIDs[] = '1-' . $currentUser->ID; |
||
184 | } |
||
185 | /** Add canView criteria. These are based on {@link DataObjectExtension::ViewStatus()} */ |
||
186 | $query = Criteria::where('ViewStatus')->in($viewIDs); |
||
187 | |||
188 | $this->clientQuery->createFilterQuery('ViewStatus') |
||
189 | ->setQuery($query->getQuery()); |
||
190 | } |
||
191 | |||
192 | /** |
||
193 | * Add filtered queries based on class hierarchy |
||
194 | * We only need the class itself, since the hierarchy will take care of the rest |
||
195 | */ |
||
196 | protected function buildClassFilter(): void |
||
197 | { |
||
198 | if (count($this->query->getClasses())) { |
||
199 | $classes = $this->query->getClasses(); |
||
200 | $criteria = Criteria::where('ClassHierarchy')->in($classes); |
||
201 | $this->clientQuery->createFilterQuery('classes') |
||
202 | ->setQuery($criteria->getQuery()); |
||
203 | } |
||
204 | } |
||
205 | |||
206 | /** |
||
207 | * |
||
208 | */ |
||
209 | protected function buildFilters(): void |
||
210 | { |
||
211 | $filters = $this->query->getFilter(); |
||
212 | foreach ($filters as $field => $value) { |
||
213 | $value = is_array($value) ? $value : [$value]; |
||
214 | $criteria = Criteria::where($field)->in($value); |
||
215 | $this->clientQuery->createFilterQuery('filter-' . $field) |
||
216 | ->setQuery($criteria->getQuery()); |
||
217 | } |
||
218 | } |
||
219 | |||
220 | /** |
||
221 | * |
||
222 | */ |
||
223 | protected function buildExcludes(): void |
||
224 | { |
||
225 | $filters = $this->query->getExclude(); |
||
226 | foreach ($filters as $field => $value) { |
||
227 | $value = is_array($value) ? $value : [$value]; |
||
228 | $criteria = Criteria::where($field) |
||
229 | ->in($value) |
||
230 | ->not(); |
||
231 | $this->clientQuery->createFilterQuery('exclude-' . $field) |
||
232 | ->setQuery($criteria->getQuery()); |
||
233 | } |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * |
||
238 | */ |
||
239 | protected function buildFacets(): void |
||
240 | { |
||
241 | $facets = $this->clientQuery->getFacetSet(); |
||
242 | // Facets should be set from the index configuration |
||
243 | foreach ($this->index->getFacetFields() as $class => $config) { |
||
244 | $facets->createFacetField('facet-' . $config['Title'])->setField($config['Field']); |
||
245 | } |
||
246 | // Count however, comes from the query |
||
247 | $facets->setMinCount($this->query->getFacetsMinCount()); |
||
248 | } |
||
249 | |||
250 | /** |
||
251 | * |
||
252 | */ |
||
253 | protected function buildFacetQuery() |
||
254 | { |
||
255 | $filterFacets = []; |
||
256 | if (Controller::has_curr()) { |
||
257 | $filterFacets = Controller::curr()->getRequest()->requestVars(); |
||
258 | } |
||
259 | foreach ($this->index->getFacetFields() as $class => $config) { |
||
260 | if (array_key_exists($config['Title'], $filterFacets)) { |
||
261 | $filter = array_filter($filterFacets[$config['Title']], 'strlen'); |
||
262 | if (count($filter)) { |
||
263 | $criteria = Criteria::where($config['Field'])->in($filter); |
||
264 | $this->clientQuery |
||
265 | ->createFilterQuery('facet-' . $config['Title']) |
||
266 | ->setQuery($criteria->getQuery()); |
||
267 | } |
||
268 | } |
||
269 | } |
||
270 | } |
||
271 | |||
272 | /** |
||
273 | * |
||
274 | */ |
||
275 | protected function buildSpellcheck(): void |
||
276 | { |
||
277 | // Assuming the first term is the term entered |
||
278 | $queryString = implode(' ', $this->queryArray); |
||
279 | // Arbitrarily limit to 5 if the config isn't set |
||
280 | $count = BaseIndex::config()->get('spellcheckCount') ?: 5; |
||
281 | $spellcheck = $this->clientQuery->getSpellcheck(); |
||
282 | $spellcheck->setQuery($queryString); |
||
283 | $spellcheck->setCount($count); |
||
284 | $spellcheck->setBuild(true); |
||
285 | $spellcheck->setCollate(true); |
||
286 | $spellcheck->setExtendedResults(true); |
||
287 | $spellcheck->setCollateExtendedResults('true'); |
||
288 | } |
||
289 | |||
290 | /** |
||
291 | * Add the index-time boosting to the query |
||
292 | */ |
||
293 | protected function buildBoosts(): void |
||
294 | { |
||
295 | $boosts = $this->query->getBoostedFields(); |
||
296 | $queries = $this->getQueryArray(); |
||
297 | foreach ($boosts as $field => $boost) { |
||
298 | foreach ($queries as $term) { |
||
299 | $booster = Criteria::where($field) |
||
300 | ->is($term) |
||
301 | ->boost($boost); |
||
302 | $this->queryArray[] = $booster->getQuery(); |
||
303 | } |
||
304 | } |
||
305 | } |
||
306 | |||
307 | /** |
||
308 | * @return array |
||
309 | */ |
||
310 | public function getQueryArray(): array |
||
311 | { |
||
312 | return array_merge($this->queryArray, $this->boostTerms); |
||
313 | } |
||
314 | |||
315 | /** |
||
316 | * @param array $queryArray |
||
317 | * @return QueryComponentFactory |
||
318 | */ |
||
319 | public function setQueryArray(array $queryArray): QueryComponentFactory |
||
320 | { |
||
321 | $this->queryArray = $queryArray; |
||
322 | |||
323 | return $this; |
||
324 | } |
||
325 | |||
326 | /** |
||
327 | * @return BaseQuery |
||
328 | */ |
||
329 | public function getQuery(): BaseQuery |
||
330 | { |
||
331 | return $this->query; |
||
332 | } |
||
333 | |||
334 | /** |
||
335 | * @param BaseQuery $query |
||
336 | * @return QueryComponentFactory |
||
337 | */ |
||
338 | public function setQuery(BaseQuery $query): QueryComponentFactory |
||
339 | { |
||
340 | $this->query = $query; |
||
341 | |||
342 | return $this; |
||
343 | } |
||
344 | |||
345 | /** |
||
346 | * @return Query |
||
347 | */ |
||
348 | public function getClientQuery(): Query |
||
349 | { |
||
350 | return $this->clientQuery; |
||
351 | } |
||
352 | |||
353 | /** |
||
354 | * @param Query $clientQuery |
||
355 | * @return QueryComponentFactory |
||
356 | */ |
||
357 | public function setClientQuery(Query $clientQuery): QueryComponentFactory |
||
358 | { |
||
359 | $this->clientQuery = $clientQuery; |
||
360 | |||
361 | return $this; |
||
362 | } |
||
363 | |||
364 | /** |
||
365 | * @return Helper |
||
366 | */ |
||
367 | public function getHelper(): Helper |
||
370 | } |
||
371 | |||
372 | /** |
||
373 | * @param Helper $helper |
||
374 | * @return QueryComponentFactory |
||
375 | */ |
||
376 | public function setHelper(Helper $helper): QueryComponentFactory |
||
377 | { |
||
378 | $this->helper = $helper; |
||
379 | |||
380 | return $this; |
||
381 | } |
||
382 | |||
383 | /** |
||
384 | * @return BaseIndex |
||
385 | */ |
||
386 | public function getIndex(): BaseIndex |
||
387 | { |
||
388 | return $this->index; |
||
389 | } |
||
390 | |||
391 | /** |
||
392 | * @param BaseIndex $index |
||
393 | * @return QueryComponentFactory |
||
394 | */ |
||
395 | public function setIndex(BaseIndex $index): QueryComponentFactory |
||
400 | } |
||
401 | } |
||
402 |