| 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 |