Complex classes like Searchable 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
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 Searchable, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 12 | class Searchable extends \DataExtension { |
||
| 13 | |||
| 14 | /** |
||
| 15 | * Counter used to display progress of indexing |
||
| 16 | * @var integer |
||
| 17 | */ |
||
| 18 | public static $index_ctr = 0; |
||
| 19 | |||
| 20 | /** |
||
| 21 | * Everytime progressInterval divides $index_ctr exactly display progress |
||
| 22 | * @var integer |
||
| 23 | */ |
||
| 24 | private static $progressInterval = 0; |
||
| 25 | |||
| 26 | public static $mappings = array( |
||
| 27 | 'Boolean' => 'boolean', |
||
| 28 | 'Decimal' => 'double', |
||
| 29 | 'Currency' => 'double', |
||
| 30 | 'Double' => 'double', |
||
| 31 | 'Enum' => 'string', |
||
| 32 | 'Float' => 'float', |
||
| 33 | 'HTMLText' => 'string', |
||
| 34 | 'HTMLVarchar' => 'string', |
||
| 35 | 'Int' => 'integer', |
||
| 36 | 'Text' => 'string', |
||
| 37 | 'VarChar' => 'string', |
||
| 38 | 'Varchar' => 'string', |
||
| 39 | 'Year' => 'integer', |
||
| 40 | 'Percentage' => 'double', |
||
| 41 | 'Time' => 'date', |
||
| 42 | |||
| 43 | // The 2 different date types will be stored with different formats |
||
| 44 | 'Date' => 'date', |
||
| 45 | 'SS_Datetime' => 'date', |
||
| 46 | 'Datetime' => 'date', |
||
| 47 | 'DBLocale' => 'string' |
||
| 48 | ); |
||
| 49 | |||
| 50 | |||
| 51 | /** |
||
| 52 | * @var ElasticaService associated elastica search service |
||
| 53 | */ |
||
| 54 | protected $service; |
||
| 55 | |||
| 56 | |||
| 57 | /** |
||
| 58 | * Array of fields that need HTML parsed |
||
| 59 | * @var array |
||
| 60 | */ |
||
| 61 | protected $html_fields = array(); |
||
| 62 | |||
| 63 | /** |
||
| 64 | * Store a mapping of relationship name to result type |
||
| 65 | */ |
||
| 66 | protected $relationship_methods = array(); |
||
| 67 | |||
| 68 | |||
| 69 | /** |
||
| 70 | * If importing a large number of items from a fixtures file, or indeed some other source, then |
||
| 71 | * it is quicker to set a flag of value IndexingOff => false. This has the effect of ensuring |
||
| 72 | * no indexing happens, a request is normally made per fixture when loading. One can then run |
||
| 73 | * the reindexing teask to bulk index in one HTTP POST request to Elasticsearch |
||
| 74 | * |
||
| 75 | * @var boolean |
||
| 76 | */ |
||
| 77 | private static $IndexingOff = false; |
||
| 78 | |||
| 79 | |||
| 80 | /** |
||
| 81 | * @see getElasticaResult |
||
| 82 | * @var \Elastica\Result |
||
| 83 | */ |
||
| 84 | protected $elastica_result; |
||
| 85 | |||
| 86 | public function __construct(ElasticaService $service) { |
||
| 87 | $this->service = $service; |
||
| 88 | parent::__construct(); |
||
| 89 | } |
||
| 90 | |||
| 91 | |||
| 92 | /** |
||
| 93 | * Get the elasticsearch type name |
||
| 94 | * |
||
| 95 | * @return string |
||
| 96 | */ |
||
| 97 | public function getElasticaType() { |
||
| 98 | return get_class($this->owner); |
||
| 99 | } |
||
| 100 | |||
| 101 | |||
| 102 | /** |
||
| 103 | * If the owner is part of a search result |
||
| 104 | * the raw Elastica search result is returned |
||
| 105 | * if set via setElasticaResult |
||
| 106 | * |
||
| 107 | * @return \Elastica\Result |
||
| 108 | */ |
||
| 109 | public function getElasticaResult() { |
||
| 110 | return $this->elastica_result; |
||
| 111 | } |
||
| 112 | |||
| 113 | |||
| 114 | /** |
||
| 115 | * Set the raw Elastica search result |
||
| 116 | * |
||
| 117 | * @param \Elastica\Result |
||
| 118 | */ |
||
| 119 | public function setElasticaResult(\Elastica\Result $result) { |
||
| 120 | $this->elastica_result = $result; |
||
| 121 | } |
||
| 122 | |||
| 123 | |||
| 124 | /** |
||
| 125 | * Gets an array of elastic field definitions. |
||
| 126 | * |
||
| 127 | * @return array |
||
| 128 | */ |
||
| 129 | public function getElasticaFields($storeMethodName = false, $recurse = true) { |
||
| 130 | $db = $this->owner->db(); |
||
| 131 | $fields = $this->getAllSearchableFields(); |
||
| 132 | $result = array(); |
||
| 133 | |||
| 134 | foreach($fields as $name => $params) { |
||
| 135 | $spec = array(); |
||
| 136 | $name = str_replace('()', '', $name); |
||
| 137 | |||
| 138 | if(array_key_exists($name, $db)) { |
||
| 139 | $class = $db[$name]; |
||
| 140 | $this->assignSpecForStandardFieldType($class, $spec); |
||
| 141 | } else { |
||
| 142 | // field name is not in the db, it could be a method |
||
| 143 | $has_lists = $this->getListRelationshipMethods(); |
||
| 144 | $has_ones = $this->owner->has_one(); |
||
| 145 | |||
| 146 | // check has_many and many_many relations |
||
| 147 | if(isset($has_lists[$name])) { |
||
| 148 | // the classes returned by the list method |
||
| 149 | $resultType = $has_lists[$name]; |
||
| 150 | $this->assignSpecForManyRelationship($resultType, $spec); |
||
| 151 | } else if(isset($has_ones[$name])) { |
||
| 152 | $resultType = $has_ones[$name]; |
||
| 153 | HERE |
||
| 154 | } |
||
|
|
|||
| 155 | // otherwise fall back to string - Enum is one such category |
||
| 156 | else { |
||
| 157 | $spec["type"] = "string"; |
||
| 158 | } |
||
| 159 | } |
||
| 160 | |||
| 161 | // in the case of a relationship type will not be set |
||
| 162 | if(isset($spec['type'])) { |
||
| 163 | if($spec['type'] == 'string') { |
||
| 164 | $unstemmed = array(); |
||
| 165 | $unstemmed['type'] = "string"; |
||
| 166 | $unstemmed['analyzer'] = "unstemmed"; |
||
| 167 | $unstemmed['term_vector'] = "yes"; |
||
| 168 | $extraFields = array('standard' => $unstemmed); |
||
| 169 | |||
| 170 | $shingles = array(); |
||
| 171 | $shingles['type'] = "string"; |
||
| 172 | $shingles['analyzer'] = "shingles"; |
||
| 173 | $shingles['term_vector'] = "yes"; |
||
| 174 | $extraFields['shingles'] = $shingles; |
||
| 175 | |||
| 176 | //Add autocomplete field if so required |
||
| 177 | $autocomplete = \Config::inst()->get($this->owner->ClassName, 'searchable_autocomplete'); |
||
| 178 | |||
| 179 | if(isset($autocomplete) && in_array($name, $autocomplete)) { |
||
| 180 | $autocompleteField = array(); |
||
| 181 | $autocompleteField['type'] = "string"; |
||
| 182 | $autocompleteField['index_analyzer'] = "autocomplete_index_analyzer"; |
||
| 183 | $autocompleteField['search_analyzer'] = "autocomplete_search_analyzer"; |
||
| 184 | $autocompleteField['term_vector'] = "yes"; |
||
| 185 | $extraFields['autocomplete'] = $autocompleteField; |
||
| 186 | } |
||
| 187 | |||
| 188 | $spec['fields'] = $extraFields; |
||
| 189 | // FIXME - make index/locale specific, get from settings |
||
| 190 | $spec['analyzer'] = 'stemmed'; |
||
| 191 | $spec['term_vector'] = "yes"; |
||
| 192 | } |
||
| 193 | } |
||
| 194 | |||
| 195 | $result[$name] = $spec; |
||
| 196 | } |
||
| 197 | |||
| 198 | if($this->owner->hasMethod('updateElasticHTMLFields')) { |
||
| 199 | $this->html_fields = $this->owner->updateElasticHTMLFields($this->html_fields); |
||
| 200 | } |
||
| 201 | |||
| 202 | return $result; |
||
| 203 | } |
||
| 204 | |||
| 205 | |||
| 206 | private function assignSpecForHasOne($resultType, &$spec) { |
||
| 207 | $resultTypeInstance = \Injector::inst()->create($resultType); |
||
| 208 | |||
| 209 | $resultTypeMapping = array(); |
||
| 210 | |||
| 211 | // get the fields for the result type, but do not recurse |
||
| 212 | if($recurse) { |
||
| 213 | $resultTypeMapping = $resultTypeInstance->getElasticaFields($storeMethodName, false); |
||
| 214 | } |
||
| 215 | |||
| 216 | $resultTypeMapping['ID'] = array('type' => 'integer'); |
||
| 217 | |||
| 218 | if($storeMethodName) { |
||
| 219 | $resultTypeMapping['__method'] = $name; |
||
| 220 | } |
||
| 221 | $spec = array('properties' => $resultTypeMapping); |
||
| 222 | // we now change the name to the result type, not the method name |
||
| 223 | $name = $resultType; |
||
| 224 | } |
||
| 225 | |||
| 226 | private function assignSpecForManyRelationship($resultType, &$spec) { |
||
| 227 | $resultTypeInstance = \Injector::inst()->create($resultType); |
||
| 228 | $resultTypeMapping = array(); |
||
| 229 | |||
| 230 | // get the fields for the result type, but do not recurse |
||
| 231 | if($recurse) { |
||
| 232 | $resultTypeMapping = $resultTypeInstance->getElasticaFields($storeMethodName, false); |
||
| 233 | } |
||
| 234 | |||
| 235 | $resultTypeMapping['ID'] = array('type' => 'integer'); |
||
| 236 | |||
| 237 | if($storeMethodName) { |
||
| 238 | $resultTypeMapping['__method'] = $name; |
||
| 239 | } |
||
| 240 | |||
| 241 | $spec = array('properties' => $resultTypeMapping); |
||
| 242 | |||
| 243 | |||
| 244 | // we now change the name to the result type, not the method name |
||
| 245 | $name = $resultType; |
||
| 246 | } |
||
| 247 | |||
| 248 | |||
| 249 | private function assignSpecForStandardFieldType($class, &$spec) { |
||
| 250 | if(($pos = strpos($class, '('))) { |
||
| 251 | // Valid in the case of Varchar(255) |
||
| 252 | $class = substr($class, 0, $pos); |
||
| 253 | } |
||
| 254 | |||
| 255 | if(array_key_exists($class, self::$mappings)) { |
||
| 256 | $spec['type'] = self::$mappings[$class]; |
||
| 257 | if($spec['type'] === 'date') { |
||
| 258 | if($class == 'Date') { |
||
| 259 | $spec['format'] = 'y-M-d'; |
||
| 260 | } elseif($class == 'SS_Datetime') { |
||
| 261 | $spec['format'] = 'y-M-d H:m:s'; |
||
| 262 | } elseif($class == 'Datetime') { |
||
| 263 | $spec['format'] = 'y-M-d H:m:s'; |
||
| 264 | } elseif($class == 'Time') { |
||
| 265 | $spec['format'] = 'H:m:s'; |
||
| 266 | } |
||
| 267 | } |
||
| 268 | if($class === 'HTMLText' || $class === 'HTMLVarchar') { |
||
| 269 | array_push($this->html_fields, $name); |
||
| 270 | } |
||
| 271 | } |
||
| 272 | // no need for an extra case here as all SS types checked in tests |
||
| 273 | } |
||
| 274 | |||
| 275 | |||
| 276 | /** |
||
| 277 | * Get the elasticsearch mapping for the current document/type |
||
| 278 | * |
||
| 279 | * @return \Elastica\Type\Mapping |
||
| 280 | */ |
||
| 281 | public function getElasticaMapping() { |
||
| 282 | $mapping = new Mapping(); |
||
| 283 | |||
| 284 | $fields = $this->getElasticaFields(false); |
||
| 285 | |||
| 286 | $localeMapping = array(); |
||
| 287 | |||
| 288 | if($this->owner->hasField('Locale')) { |
||
| 289 | $localeMapping['type'] = 'string'; |
||
| 290 | // we wish the locale to be stored as is |
||
| 291 | $localeMapping['index'] = 'not_analyzed'; |
||
| 292 | $fields['Locale'] = $localeMapping; |
||
| 293 | } |
||
| 294 | |||
| 295 | // ADD CUSTOM FIELDS HERE THAT ARE INDEXED BY DEFAULT |
||
| 296 | // add a mapping to flag whether or not class is in SiteTree |
||
| 297 | $fields['IsInSiteTree'] = array('type'=>'boolean'); |
||
| 298 | $fields['Link'] = array('type' => 'string', 'index' => 'not_analyzed'); |
||
| 299 | |||
| 300 | $mapping->setProperties($fields); |
||
| 301 | |||
| 302 | //This concatenates all the fields together into a single field. |
||
| 303 | //Initially added for suggestions compatibility, in that searching |
||
| 304 | //_all field picks up all possible suggestions |
||
| 305 | $mapping->enableAllField(); |
||
| 306 | |||
| 307 | if($this->owner->hasMethod('updateElasticsearchMapping')) { |
||
| 308 | $mapping = $this->owner->updateElasticsearchMapping($mapping); |
||
| 309 | } |
||
| 310 | return $mapping; |
||
| 311 | } |
||
| 312 | |||
| 313 | |||
| 314 | /** |
||
| 315 | * Get an elasticsearch document |
||
| 316 | * |
||
| 317 | * @return \Elastica\Document |
||
| 318 | */ |
||
| 319 | public function getElasticaDocument() { |
||
| 320 | self::$index_ctr++; |
||
| 321 | $fields = $this->getFieldValuesAsArray(); |
||
| 322 | $progress = Controller::curr()->getVar('progress'); |
||
| 323 | if(!empty($progress)) { |
||
| 324 | self::$progressInterval = (int)$progress; |
||
| 325 | } |
||
| 326 | |||
| 327 | if(self::$progressInterval > 0) { |
||
| 328 | if(self::$index_ctr % self::$progressInterval === 0) { |
||
| 329 | ElasticaUtil::message("\t" . $this->owner->ClassName . " - Prepared " . self::$index_ctr . " for indexing..."); |
||
| 330 | } |
||
| 331 | } |
||
| 332 | |||
| 333 | // Optionally update the document |
||
| 334 | $document = new Document($this->owner->ID, $fields); |
||
| 335 | if($this->owner->hasMethod('updateElasticsearchDocument')) { |
||
| 336 | $document = $this->owner->updateElasticsearchDocument($document); |
||
| 337 | } |
||
| 338 | |||
| 339 | // Check if the current classname is part of the site tree or not |
||
| 340 | // Results are cached to save reprocessing the same |
||
| 341 | $classname = $this->owner->ClassName; |
||
| 342 | $inSiteTree = $this->isInSiteTree($classname); |
||
| 343 | |||
| 344 | $document->set('IsInSiteTree', $inSiteTree); |
||
| 345 | |||
| 346 | if($inSiteTree) { |
||
| 347 | $document->set('Link', $this->owner->AbsoluteLink()); |
||
| 348 | } |
||
| 349 | |||
| 350 | if(isset($this->owner->Locale)) { |
||
| 351 | $document->set('Locale', $this->owner->Locale); |
||
| 352 | } |
||
| 353 | |||
| 354 | return $document; |
||
| 355 | } |
||
| 356 | |||
| 357 | |||
| 358 | public function getFieldValuesAsArray($recurse = true) { |
||
| 359 | $fields = array(); |
||
| 360 | $has_ones = $this->owner->has_one(); |
||
| 361 | |||
| 362 | foreach($this->getElasticaFields($recurse) as $field => $config) { |
||
| 363 | if(null === $this->owner->$field && is_callable(get_class($this->owner) . "::" . $field)) { |
||
| 364 | // call a method to get a field value |
||
| 365 | if(in_array($field, $this->html_fields)) { |
||
| 366 | // Parse short codes in HTML, and then convert to text |
||
| 367 | $fields[$field] = $this->owner->$field; |
||
| 368 | $html = ShortcodeParser::get_active()->parse($this->owner->$field()); |
||
| 369 | $txt = \Convert::html2raw($html); |
||
| 370 | $fields[$field] = $txt; |
||
| 371 | } else { |
||
| 372 | // Plain text |
||
| 373 | $fields[$field] = $this->owner->$field(); |
||
| 374 | } |
||
| 375 | |||
| 376 | } else { |
||
| 377 | if(in_array($field, $this->html_fields)) { |
||
| 378 | $fields[$field] = $this->owner->$field; |
||
| 379 | if(gettype($this->owner->$field) !== 'NULL') { |
||
| 380 | $html = ShortcodeParser::get_active()->parse($this->owner->$field); |
||
| 381 | $txt = \Convert::html2raw($html); |
||
| 382 | $fields[$field] = $txt; |
||
| 383 | } |
||
| 384 | } else { |
||
| 385 | if(isset($config['properties']['__method'])) { |
||
| 386 | $methodName = $config['properties']['__method']; |
||
| 387 | $data = $this->owner->$methodName(); |
||
| 388 | $relArray = array(); |
||
| 389 | |||
| 390 | // get the fields of a has_one relational object |
||
| 391 | if(isset($has_ones[$methodName])) { |
||
| 392 | if($data->ID > 0) { |
||
| 393 | $item = $data->getFieldValuesAsArray(false); |
||
| 394 | $relArray = $item; |
||
| 395 | } |
||
| 396 | |||
| 397 | // get the fields for a has_many or many_many relational list |
||
| 398 | } else { |
||
| 399 | foreach($data->getIterator() as $item) { |
||
| 400 | if($recurse) { |
||
| 401 | // populate the subitem but do not recurse any further if more relationships |
||
| 402 | $itemDoc = $item->getFieldValuesAsArray(false); |
||
| 403 | array_push($relArray, $itemDoc); |
||
| 404 | } |
||
| 405 | } |
||
| 406 | } |
||
| 407 | // save the relation as an array (for now) |
||
| 408 | $fields[$methodName] = $relArray; |
||
| 409 | } else { |
||
| 410 | $fields[$field] = $this->owner->$field; |
||
| 411 | } |
||
| 412 | |||
| 413 | } |
||
| 414 | |||
| 415 | } |
||
| 416 | } |
||
| 417 | |||
| 418 | return $fields; |
||
| 419 | } |
||
| 420 | |||
| 421 | |||
| 422 | /** |
||
| 423 | * Returns whether to include the document into the search index. |
||
| 424 | * All documents are added unless they have a field "ShowInSearch" which is set to false |
||
| 425 | * |
||
| 426 | * @return boolean |
||
| 427 | */ |
||
| 428 | public function showRecordInSearch() { |
||
| 429 | return !($this->owner->hasField('ShowInSearch') && false == $this->owner->ShowInSearch); |
||
| 430 | } |
||
| 431 | |||
| 432 | |||
| 433 | /** |
||
| 434 | * Delete the record from the search index if ShowInSearch is deactivated (non-SiteTree). |
||
| 435 | */ |
||
| 436 | public function onBeforeWrite() { |
||
| 437 | if(($this->owner instanceof \SiteTree)) { |
||
| 438 | if($this->owner->hasField('ShowInSearch') && |
||
| 439 | $this->owner->isChanged('ShowInSearch', 2) && false == $this->owner->ShowInSearch) { |
||
| 440 | $this->doDeleteDocument(); |
||
| 441 | } |
||
| 442 | } |
||
| 443 | } |
||
| 444 | |||
| 445 | |||
| 446 | /** |
||
| 447 | * Delete the record from the search index if ShowInSearch is deactivated (SiteTree). |
||
| 448 | */ |
||
| 449 | public function onBeforePublish() { |
||
| 450 | if(false == $this->owner->ShowInSearch) { |
||
| 451 | if($this->owner->isPublished()) { |
||
| 452 | $liveRecord = \Versioned::get_by_stage(get_class($this->owner), 'Live')-> |
||
| 453 | byID($this->owner->ID); |
||
| 454 | if($liveRecord->ShowInSearch != $this->owner->ShowInSearch) { |
||
| 455 | $this->doDeleteDocument(); |
||
| 456 | } |
||
| 457 | } |
||
| 458 | } |
||
| 459 | } |
||
| 460 | |||
| 461 | |||
| 462 | /** |
||
| 463 | * Updates the record in the search index (non-SiteTree). |
||
| 464 | */ |
||
| 465 | public function onAfterWrite() { |
||
| 466 | $this->doIndexDocument(); |
||
| 467 | } |
||
| 468 | |||
| 469 | |||
| 470 | /** |
||
| 471 | * Updates the record in the search index (SiteTree). |
||
| 472 | */ |
||
| 473 | public function onAfterPublish() { |
||
| 474 | $this->doIndexDocument(); |
||
| 475 | } |
||
| 476 | |||
| 477 | |||
| 478 | /** |
||
| 479 | * Updates the record in the search index. |
||
| 480 | */ |
||
| 481 | protected function doIndexDocument() { |
||
| 482 | if($this->showRecordInSearch()) { |
||
| 483 | if(!$this->owner->IndexingOff) { |
||
| 484 | $this->service->index($this->owner); |
||
| 485 | } |
||
| 486 | } |
||
| 487 | } |
||
| 488 | |||
| 489 | |||
| 490 | /** |
||
| 491 | * Removes the record from the search index (non-SiteTree). |
||
| 492 | */ |
||
| 493 | public function onAfterDelete() { |
||
| 494 | $this->doDeleteDocumentIfInSearch(); |
||
| 495 | } |
||
| 496 | |||
| 497 | |||
| 498 | /** |
||
| 499 | * Removes the record from the search index (non-SiteTree). |
||
| 500 | */ |
||
| 501 | public function onAfterUnpublish() { |
||
| 502 | $this->doDeleteDocumentIfInSearch(); |
||
| 503 | } |
||
| 504 | |||
| 505 | |||
| 506 | /** |
||
| 507 | * Removes the record from the search index if the "ShowInSearch" attribute is set to true. |
||
| 508 | */ |
||
| 509 | protected function doDeleteDocumentIfInSearch() { |
||
| 510 | if($this->showRecordInSearch()) { |
||
| 511 | $this->doDeleteDocument(); |
||
| 512 | } |
||
| 513 | } |
||
| 514 | |||
| 515 | |||
| 516 | /** |
||
| 517 | * Removes the record from the search index. |
||
| 518 | */ |
||
| 519 | protected function doDeleteDocument() { |
||
| 520 | try { |
||
| 521 | if(!$this->owner->IndexingOff) { |
||
| 522 | // this goes to elastica service |
||
| 523 | $this->service->remove($this->owner); |
||
| 524 | } |
||
| 525 | } catch (\Elastica\Exception\NotFoundException $e) { |
||
| 526 | trigger_error("Deleted document " . $this->owner->ClassName . " (" . $this->owner->ID . |
||
| 527 | ") not found in search index.", E_USER_NOTICE); |
||
| 528 | } |
||
| 529 | |||
| 530 | } |
||
| 531 | |||
| 532 | |||
| 533 | /** |
||
| 534 | * Return all of the searchable fields defined in $this->owner::$searchable_fields and all the parent classes. |
||
| 535 | * |
||
| 536 | * @param $recuse Whether or not to traverse relationships. First time round yes, subsequently no |
||
| 537 | * @return array searchable fields |
||
| 538 | */ |
||
| 539 | public function getAllSearchableFields($recurse = true) { |
||
| 540 | $fields = \Config::inst()->get(get_class($this->owner), 'searchable_fields'); |
||
| 541 | |||
| 542 | // fallback to default method |
||
| 543 | if(!$fields) { |
||
| 544 | user_error('The field $searchable_fields must be set for the class ' . $this->owner->ClassName); |
||
| 545 | } |
||
| 546 | |||
| 547 | // get the values of these fields |
||
| 548 | $elasticaMapping = $this->fieldsToElasticaConfig($fields); |
||
| 549 | |||
| 550 | if($recurse) { |
||
| 551 | // now for the associated methods and their results |
||
| 552 | $methodDescs = \Config::inst()->get(get_class($this->owner), 'searchable_relationships'); |
||
| 553 | $has_ones = $this->owner->has_one(); |
||
| 554 | $has_lists = $this->getListRelationshipMethods(); |
||
| 555 | |||
| 556 | if(isset($methodDescs) && is_array($methodDescs)) { |
||
| 557 | foreach($methodDescs as $methodDesc) { |
||
| 558 | // split before the brackets which can optionally list which fields to index |
||
| 559 | $splits = explode('(', $methodDesc); |
||
| 560 | $methodName = $splits[0]; |
||
| 561 | |||
| 562 | if(isset($has_lists[$methodName])) { |
||
| 563 | |||
| 564 | $relClass = $has_lists[$methodName]; |
||
| 565 | $fields = \Config::inst()->get($relClass, 'searchable_fields'); |
||
| 566 | if(!$fields) { |
||
| 567 | user_error('The field $searchable_fields must be set for the class ' . $relClass); |
||
| 568 | } |
||
| 569 | $rewrite = $this->fieldsToElasticaConfig($fields); |
||
| 570 | |||
| 571 | // mark as a method, the resultant fields are correct |
||
| 572 | $elasticaMapping[$methodName . '()'] = $rewrite; |
||
| 573 | } else if(isset($has_ones[$methodName])) { |
||
| 574 | $relClass = $has_ones[$methodName]; |
||
| 575 | $fields = \Config::inst()->get($relClass, 'searchable_fields'); |
||
| 576 | if(!$fields) { |
||
| 577 | user_error('The field $searchable_fields must be set for the class ' . $relClass); |
||
| 578 | } |
||
| 579 | $rewrite = $this->fieldsToElasticaConfig($fields); |
||
| 580 | |||
| 581 | // mark as a method, the resultant fields are correct |
||
| 582 | $elasticaMapping[$methodName . '()'] = $rewrite; |
||
| 583 | } else { |
||
| 584 | user_error('The method ' . $methodName . ' not found in class ' . $this->owner->ClassName . |
||
| 585 | ', please check configuration'); |
||
| 586 | } |
||
| 587 | } |
||
| 588 | } |
||
| 589 | } |
||
| 590 | |||
| 591 | return $elasticaMapping; |
||
| 592 | } |
||
| 593 | |||
| 594 | |||
| 595 | /* |
||
| 596 | Evaluate each field, e.g. 'Title', 'Member.Name' |
||
| 597 | */ |
||
| 598 | private function fieldsToElasticaConfig($fields) { |
||
| 599 | // Copied from DataObject::searchableFields() as there is no separate accessible method |
||
| 600 | $rewrite = array(); |
||
| 601 | foreach($fields as $name => $specOrName) { |
||
| 602 | $identifer = (is_int($name)) ? $specOrName : $name; |
||
| 603 | $rewrite[$identifer] = array(); |
||
| 604 | if(!isset($rewrite[$identifer]['title'])) { |
||
| 605 | $rewrite[$identifer]['title'] = (isset($labels[$identifer])) |
||
| 606 | ? $labels[$identifer] : \FormField::name_to_label($identifer); |
||
| 607 | } |
||
| 608 | if(!isset($rewrite[$identifer]['filter'])) { |
||
| 609 | $rewrite[$identifer]['filter'] = 'PartialMatchFilter'; |
||
| 610 | } |
||
| 611 | } |
||
| 612 | |||
| 613 | return $rewrite; |
||
| 614 | } |
||
| 615 | |||
| 616 | |||
| 617 | public function requireDefaultRecords() { |
||
| 618 | parent::requireDefaultRecords(); |
||
| 619 | |||
| 620 | $searchableFields = $this->getElasticaFields(true, true); |
||
| 621 | |||
| 622 | |||
| 623 | $doSC = \SearchableClass::get()->filter(array('Name' => $this->owner->ClassName))->first(); |
||
| 624 | if(!$doSC) { |
||
| 625 | $doSC = new \SearchableClass(); |
||
| 626 | $doSC->Name = $this->owner->ClassName; |
||
| 627 | |||
| 628 | $inSiteTree = $this->isInSiteTree($this->owner->ClassName); |
||
| 629 | $doSC->InSiteTree = $inSiteTree; |
||
| 630 | |||
| 631 | $doSC->write(); |
||
| 632 | } |
||
| 633 | |||
| 634 | foreach($searchableFields as $name => $searchableField) { |
||
| 635 | // check for existence of methods and if they exist use that as the name |
||
| 636 | if(!isset($searchableField['type'])) { |
||
| 637 | $name = $searchableField['properties']['__method']; |
||
| 638 | } |
||
| 639 | |||
| 640 | $filter = array('ClazzName' => $this->owner->ClassName, 'Name' => $name); |
||
| 641 | $doSF = \SearchableField::get()->filter($filter)->first(); |
||
| 642 | |||
| 643 | |||
| 644 | if(!$doSF) { |
||
| 645 | $doSF = new \SearchableField(); |
||
| 646 | $doSF->ClazzName = $this->owner->ClassName; |
||
| 647 | $doSF->Name = $name; |
||
| 648 | |||
| 649 | if(isset($searchableField['type'])) { |
||
| 650 | $doSF->Type = $searchableField['type']; |
||
| 651 | } else { |
||
| 652 | $doSF->Name = $searchableField['properties']['__method']; |
||
| 653 | $doSF->Type = 'relationship'; |
||
| 654 | } |
||
| 655 | $doSF->SearchableClassID = $doSC->ID; |
||
| 656 | |||
| 657 | if(isset($searchableField['fields']['autocomplete'])) { |
||
| 658 | $doSF->Autocomplete = true; |
||
| 659 | } |
||
| 660 | |||
| 661 | $doSF->write(); |
||
| 662 | \DB::alteration_message("Created new searchable editable field " . $name, "changed"); |
||
| 663 | } |
||
| 664 | |||
| 665 | // FIXME deal with deletions |
||
| 666 | } |
||
| 667 | } |
||
| 668 | |||
| 669 | |||
| 670 | private function getListRelationshipMethods() { |
||
| 671 | $has_manys = $this->owner->has_many(); |
||
| 672 | $many_manys = $this->owner->many_many(); |
||
| 673 | |||
| 674 | // array of method name to retuned object ClassName for relationships returning lists |
||
| 675 | $has_lists = $has_manys; |
||
| 676 | foreach(array_keys($many_manys) as $key) { |
||
| 677 | $has_lists[$key] = $many_manys[$key]; |
||
| 678 | } |
||
| 679 | |||
| 680 | return $has_lists; |
||
| 681 | } |
||
| 682 | |||
| 683 | |||
| 684 | private function isInSiteTree($classname) { |
||
| 685 | $inSiteTree = ($classname === 'SiteTree' ? true : false); |
||
| 686 | if(!$inSiteTree) { |
||
| 687 | $class = new \ReflectionClass($this->owner->ClassName); |
||
| 688 | while($class = $class->getParentClass()) { |
||
| 689 | $parentClass = $class->getName(); |
||
| 690 | if($parentClass == 'SiteTree') { |
||
| 691 | $inSiteTree = true; |
||
| 692 | break; |
||
| 693 | } |
||
| 694 | } |
||
| 695 | } |
||
| 696 | return $inSiteTree; |
||
| 697 | } |
||
| 698 | |||
| 699 | |||
| 700 | /* |
||
| 701 | Allow the option of overriding the default template with one of <ClassName>ElasticSearchResult |
||
| 702 | */ |
||
| 703 | public function RenderResult($linkToContainer = '') { |
||
| 704 | $vars = new \ArrayData(array('SearchResult' => $this->owner, 'ContainerLink' => $linkToContainer)); |
||
| 705 | $possibleTemplates = array($this->owner->ClassName . 'ElasticSearchResult', 'ElasticSearchResult'); |
||
| 706 | return $this->owner->customise($vars)->renderWith($possibleTemplates); |
||
| 707 | } |
||
| 708 | |||
| 709 | |||
| 710 | |||
| 711 | public function getTermVectors() { |
||
| 712 | return $this->service->getTermVectors($this->owner); |
||
| 713 | } |
||
| 714 | |||
| 715 | |||
| 716 | public function updateCMSFields(\FieldList $fields) { |
||
| 717 | $isIndexed = false; |
||
| 718 | // SIteTree object must have a live record, ShowInSearch = true |
||
| 719 | if($this->isInSiteTree($this->owner->ClassName)) { |
||
| 720 | $liveRecord = \Versioned::get_by_stage(get_class($this->owner), 'Live')-> |
||
| 721 | byID($this->owner->ID); |
||
| 722 | if($liveRecord->ShowInSearch) { |
||
| 723 | $isIndexed = true; |
||
| 724 | } else { |
||
| 725 | $isIndexed = false; |
||
| 726 | } |
||
| 727 | } else { |
||
| 728 | // In the case of a DataObject we use the ShowInSearchFlag |
||
| 729 | $isIndexed = true; |
||
| 730 | } |
||
| 731 | |||
| 732 | if($isIndexed) { |
||
| 733 | $termVectors = $this->getTermVectors(); |
||
| 734 | $termFields = array_keys($termVectors); |
||
| 735 | sort($termFields); |
||
| 736 | |||
| 737 | foreach($termFields as $field) { |
||
| 738 | $terms = new \ArrayList(); |
||
| 739 | |||
| 740 | foreach(array_keys($termVectors[$field]['terms']) as $term) { |
||
| 741 | $do = new \DataObject(); |
||
| 742 | $do->Term = $term; |
||
| 743 | $stats = $termVectors[$field]['terms'][$term]; |
||
| 744 | if(isset($stats['ttf'])) { |
||
| 745 | $do->TTF = $stats['ttf']; |
||
| 746 | } |
||
| 747 | |||
| 748 | if(isset($stats['doc_freq'])) { |
||
| 749 | $do->DocFreq = $stats['doc_freq']; |
||
| 750 | } |
||
| 751 | |||
| 752 | if(isset($stats['term_freq'])) { |
||
| 753 | $do->TermFreq = $stats['term_freq']; |
||
| 754 | } |
||
| 755 | $terms->push($do); |
||
| 756 | } |
||
| 757 | |||
| 758 | $config = \GridFieldConfig_RecordViewer::create(100); |
||
| 759 | $config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array( |
||
| 760 | 'Term' => 'Term', |
||
| 761 | 'TTF' => 'Total term frequency (how often a term occurs in all documents)', |
||
| 762 | 'DocFreq' => 'n documents with this term', |
||
| 763 | 'TermFreq'=> 'n times this term appears in this field' |
||
| 764 | )); |
||
| 765 | |||
| 766 | $underscored = str_replace('.', '_', $field); |
||
| 767 | |||
| 768 | $gridField = new \GridField( |
||
| 769 | 'TermsFor' . $underscored, // Field name |
||
| 770 | $field . 'TITLE' . $field, // Field title |
||
| 771 | $terms, |
||
| 772 | $config |
||
| 773 | ); |
||
| 774 | $fields->addFieldToTab('Root.ElasticaTerms.' . $underscored, $gridField); |
||
| 775 | } |
||
| 776 | |||
| 777 | } |
||
| 778 | |||
| 784 |