Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like MetadataTrait 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 MetadataTrait, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 16 | trait MetadataTrait |
||
| 17 | { |
||
| 18 | protected static $relationHooks = []; |
||
| 19 | protected static $relationCategories = []; |
||
| 20 | protected static $methodPrimary = []; |
||
| 21 | protected static $methodAlternate = []; |
||
| 22 | protected $loadEagerRelations = []; |
||
| 23 | |||
| 24 | /* |
||
| 25 | * Array to record mapping between doctrine types and OData types |
||
| 26 | */ |
||
| 27 | protected $mapping = [ |
||
| 28 | 3 | 'integer' => EdmPrimitiveType::INT32, |
|
| 29 | 'string' => EdmPrimitiveType::STRING, |
||
| 30 | 3 | 'datetime' => EdmPrimitiveType::DATETIME, |
|
| 31 | 'float' => EdmPrimitiveType::SINGLE, |
||
| 32 | 'decimal' => EdmPrimitiveType::DECIMAL, |
||
| 33 | 2 | 'text' => EdmPrimitiveType::STRING, |
|
| 34 | 2 | 'boolean' => EdmPrimitiveType::BOOLEAN, |
|
| 35 | 'blob' => 'stream' |
||
| 36 | 2 | ]; |
|
| 37 | |||
| 38 | 2 | /* |
|
| 39 | 1 | * Retrieve and assemble this model's metadata for OData packaging |
|
| 40 | */ |
||
| 41 | public function metadata() |
||
| 96 | |||
| 97 | /* |
||
| 98 | 4 | * Return the set of fields that are permitted to be in metadata |
|
| 99 | * - following same visible-trumps-hidden guideline as Laravel |
||
| 100 | 4 | */ |
|
| 101 | public function metadataMask() |
||
| 117 | |||
| 118 | 4 | /* |
|
| 119 | * Get the endpoint name being exposed |
||
| 120 | * |
||
| 121 | */ |
||
| 122 | public function getEndpointName() |
||
| 133 | |||
| 134 | /* |
||
| 135 | * Assemble this model's OData metadata as xml schema |
||
| 136 | * |
||
| 137 | * @return ResourceEntityType |
||
| 138 | */ |
||
| 139 | public function getXmlSchema() |
||
| 194 | 3 | ||
| 195 | 3 | /** |
|
| 196 | 3 | * @param $entityTypes |
|
| 197 | 3 | * @param $resourceSets |
|
| 198 | 3 | * @return array |
|
| 199 | 3 | */ |
|
| 200 | 3 | public function hookUpRelationships($entityTypes, $resourceSets) |
|
| 232 | 2 | ||
| 233 | 2 | /** |
|
| 234 | 3 | * Get model's relationships |
|
| 235 | 3 | * |
|
| 236 | 3 | * @return array |
|
| 237 | 3 | */ |
|
| 238 | 3 | public function getRelationships() |
|
| 258 | |||
| 259 | protected function getAllAttributes() |
||
| 284 | |||
| 285 | /** |
||
| 286 | * @param bool $biDir |
||
| 287 | * @return array |
||
| 288 | */ |
||
| 289 | protected function getRelationshipsFromMethods($biDir = false) |
||
| 290 | { |
||
| 291 | $biDirVal = intval($biDir); |
||
| 292 | $isCached = isset(static::$relationCategories[$biDirVal]) && !empty(static::$relationCategories[$biDirVal]); |
||
| 293 | if (!$isCached) { |
||
| 294 | $model = $this; |
||
| 295 | $relationships = [ |
||
| 296 | 'HasOne' => [], |
||
| 297 | 'UnknownPolyMorphSide' => [], |
||
| 298 | 'HasMany' => [], |
||
| 299 | 'KnownPolyMorphSide' => [] |
||
| 300 | ]; |
||
| 301 | $methods = get_class_methods($model); |
||
| 302 | if (!empty($methods)) { |
||
| 303 | foreach ($methods as $method) { |
||
| 304 | if (!method_exists('Illuminate\Database\Eloquent\Model', $method) |
||
| 305 | ) { |
||
| 306 | //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php |
||
| 307 | $reflection = new \ReflectionMethod($model, $method); |
||
| 308 | |||
| 309 | $file = new \SplFileObject($reflection->getFileName()); |
||
| 310 | $file->seek($reflection->getStartLine() - 1); |
||
| 311 | $code = ''; |
||
| 312 | while ($file->key() < $reflection->getEndLine()) { |
||
| 313 | $code .= $file->current(); |
||
| 314 | $file->next(); |
||
| 315 | } |
||
| 316 | |||
| 317 | $code = trim(preg_replace('/\s\s+/', '', $code)); |
||
| 318 | assert( |
||
| 319 | false !== stripos($code, 'function'), |
||
| 320 | 'Function definition must have keyword \'function\'' |
||
| 321 | ); |
||
| 322 | $begin = strpos($code, 'function('); |
||
| 323 | $code = substr($code, $begin, strrpos($code, '}') - $begin + 1); |
||
| 324 | $lastCode = $code[strlen($code) - 1]; |
||
| 325 | assert('}' == $lastCode, 'Final character of function definition must be closing brace'); |
||
| 326 | foreach ([ |
||
| 327 | 'hasMany', |
||
| 328 | 'hasManyThrough', |
||
| 329 | 'belongsToMany', |
||
| 330 | 'hasOne', |
||
| 331 | 'belongsTo', |
||
| 332 | 'morphOne', |
||
| 333 | 'morphTo', |
||
| 334 | 'morphMany', |
||
| 335 | 'morphToMany', |
||
| 336 | 'morphedByMany' |
||
| 337 | ] as $relation) { |
||
| 338 | $search = '$this->' . $relation . '('; |
||
| 339 | if ($pos = stripos($code, $search)) { |
||
| 340 | //Resolve the relation's model to a Relation object. |
||
| 341 | $relationObj = $model->$method(); |
||
| 342 | if ($relationObj instanceof Relation) { |
||
| 343 | $relObject = $relationObj->getRelated(); |
||
| 344 | $relatedModel = '\\' . get_class($relObject); |
||
| 345 | if (in_array(MetadataTrait::class, class_uses($relatedModel))) { |
||
| 346 | $relations = [ |
||
| 347 | 'hasManyThrough', |
||
| 348 | 'belongsToMany', |
||
| 349 | 'hasMany', |
||
| 350 | 'morphMany', |
||
| 351 | 'morphToMany', |
||
| 352 | 'morphedByMany' |
||
| 353 | ]; |
||
| 354 | if (in_array($relation, $relations)) { |
||
| 355 | //Collection or array of models (because Collection is Arrayable) |
||
| 356 | $relationships['HasMany'][$method] = $biDir ? $relationObj : $relatedModel; |
||
| 357 | } elseif ('morphTo' === $relation) { |
||
| 358 | // Model isn't specified because relation is polymorphic |
||
| 359 | $relationships['UnknownPolyMorphSide'][$method] = |
||
| 360 | $biDir ? $relationObj : '\Illuminate\Database\Eloquent\Model|\Eloquent'; |
||
| 361 | } else { |
||
| 362 | //Single model is returned |
||
| 363 | $relationships['HasOne'][$method] = $biDir ? $relationObj : $relatedModel; |
||
| 364 | } |
||
| 365 | if (in_array($relation, ['morphMany', 'morphOne', 'morphToMany'])) { |
||
| 366 | $relationships['KnownPolyMorphSide'][$method] = |
||
| 367 | $biDir ? $relationObj : $relatedModel; |
||
| 368 | } |
||
| 369 | if (in_array($relation, ['morphedByMany'])) { |
||
| 370 | $relationships['UnknownPolyMorphSide'][$method] = |
||
| 371 | $biDir ? $relationObj : $relatedModel; |
||
| 372 | } |
||
| 373 | } |
||
| 374 | } |
||
| 375 | } |
||
| 376 | } |
||
| 377 | } |
||
| 378 | } |
||
| 379 | } |
||
| 380 | static::$relationCategories[$biDirVal] = $relationships; |
||
| 381 | } |
||
| 382 | return static::$relationCategories[$biDirVal]; |
||
| 383 | } |
||
| 384 | |||
| 385 | /** |
||
| 386 | * Get the visible attributes for the model. |
||
| 387 | * |
||
| 388 | * @return array |
||
| 389 | */ |
||
| 390 | abstract public function getVisible(); |
||
| 391 | |||
| 392 | /** |
||
| 393 | * Get the hidden attributes for the model. |
||
| 394 | * |
||
| 395 | * @return array |
||
| 396 | */ |
||
| 397 | abstract public function getHidden(); |
||
| 398 | |||
| 399 | /** |
||
| 400 | * Get the primary key for the model. |
||
| 401 | * |
||
| 402 | * @return string |
||
| 403 | */ |
||
| 404 | abstract public function getKeyName(); |
||
| 405 | |||
| 406 | /** |
||
| 407 | * Get the current connection name for the model. |
||
| 408 | * |
||
| 409 | * @return string |
||
| 410 | */ |
||
| 411 | abstract public function getConnectionName(); |
||
| 412 | |||
| 413 | /** |
||
| 414 | * Get the database connection for the model. |
||
| 415 | * |
||
| 416 | * @return \Illuminate\Database\Connection |
||
| 417 | */ |
||
| 418 | abstract public function getConnection(); |
||
| 419 | |||
| 420 | /** |
||
| 421 | * Get all of the current attributes on the model. |
||
| 422 | * |
||
| 423 | * @return array |
||
| 424 | */ |
||
| 425 | abstract public function getAttributes(); |
||
| 426 | |||
| 427 | /** |
||
| 428 | * Get the table associated with the model. |
||
| 429 | * |
||
| 430 | * @return string |
||
| 431 | */ |
||
| 432 | abstract public function getTable(); |
||
| 433 | |||
| 434 | /** |
||
| 435 | * Get the fillable attributes for the model. |
||
| 436 | * |
||
| 437 | * @return array |
||
| 438 | */ |
||
| 439 | abstract public function getFillable(); |
||
| 440 | |||
| 441 | /** |
||
| 442 | * Dig up all defined getters on the model |
||
| 443 | * |
||
| 444 | * @return array |
||
| 445 | */ |
||
| 446 | protected function collectGetters() |
||
| 447 | { |
||
| 448 | $getterz = []; |
||
| 449 | $methods = get_class_methods($this); |
||
| 450 | foreach ($methods as $method) { |
||
| 451 | if (12 < strlen($method) && 'get' == substr($method, 0, 3)) { |
||
| 452 | if ('Attribute' == substr($method, -9)) { |
||
| 453 | $getterz[] = $method; |
||
| 454 | } |
||
| 455 | } |
||
| 456 | } |
||
| 457 | $methods = []; |
||
| 458 | |||
| 459 | foreach ($getterz as $getter) { |
||
| 460 | $residual = substr($getter, 3); |
||
| 461 | $residual = substr($residual, 0, -9); |
||
| 462 | $methods[] = $residual; |
||
| 463 | } |
||
| 464 | return $methods; |
||
| 465 | } |
||
| 466 | |||
| 467 | /** |
||
| 468 | * @param $foo |
||
| 469 | * @return array |
||
| 470 | */ |
||
| 471 | private function polyglotKeyMethodNames($foo, $condition = false) |
||
| 472 | { |
||
| 473 | $fkList = ['getQualifiedForeignKeyName', 'getForeignKey']; |
||
| 474 | $rkList = ['getQualifiedRelatedKeyName', 'getOtherKey', 'getOwnerKey']; |
||
| 475 | |||
| 476 | $fkMethodName = null; |
||
| 477 | $rkMethodName = null; |
||
| 478 | if ($condition) { |
||
| 479 | if (array_key_exists(get_class($foo), static::$methodPrimary)) { |
||
| 480 | $line = static::$methodPrimary[get_class($foo)]; |
||
| 481 | $fkMethodName = $line['fk']; |
||
| 482 | $rkMethodName = $line['rk']; |
||
| 483 | } else { |
||
| 484 | $methodList = get_class_methods(get_class($foo)); |
||
| 485 | $fkMethodName = 'getQualifiedForeignPivotKeyName'; |
||
| 486 | foreach ($fkList as $option) { |
||
| 487 | if (in_array($option, $methodList)) { |
||
| 488 | $fkMethodName = $option; |
||
| 489 | break; |
||
| 490 | } |
||
| 491 | } |
||
| 492 | assert(in_array($fkMethodName, $methodList), 'Selected method, '.$fkMethodName.', not in method list'); |
||
| 493 | $rkMethodName = 'getQualifiedRelatedPivotKeyName'; |
||
| 494 | foreach ($rkList as $option) { |
||
| 495 | if (in_array($option, $methodList)) { |
||
| 496 | $rkMethodName = $option; |
||
| 497 | break; |
||
| 498 | } |
||
| 499 | } |
||
| 500 | assert(in_array($rkMethodName, $methodList), 'Selected method, '.$rkMethodName.', not in method list'); |
||
| 501 | $line = ['fk' => $fkMethodName, 'rk' => $rkMethodName]; |
||
| 502 | static::$methodPrimary[get_class($foo)] = $line; |
||
| 503 | } |
||
| 504 | } |
||
| 505 | return [$fkMethodName, $rkMethodName]; |
||
| 506 | } |
||
| 507 | |||
| 508 | private function polyglotKeyMethodBackupNames($foo, $condition = false) |
||
| 509 | { |
||
| 510 | $fkList = ['getForeignKey', 'getForeignKeyName']; |
||
| 511 | $rkList = ['getOtherKey', 'getQualifiedParentKeyName']; |
||
| 512 | |||
| 513 | $fkMethodName = null; |
||
| 514 | $rkMethodName = null; |
||
| 515 | if ($condition) { |
||
| 516 | if (array_key_exists(get_class($foo), static::$methodAlternate)) { |
||
| 517 | $line = static::$methodAlternate[get_class($foo)]; |
||
| 518 | $fkMethodName = $line['fk']; |
||
| 519 | $rkMethodName = $line['rk']; |
||
| 520 | } else { |
||
| 521 | $methodList = get_class_methods(get_class($foo)); |
||
| 522 | $fkCombo = array_values(array_intersect($fkList, $methodList)); |
||
| 523 | assert(1 <= count($fkCombo), 'Expected at least 1 element in foreign-key list, got '.count($fkCombo)); |
||
| 524 | $fkMethodName = $fkCombo[0]; |
||
| 525 | assert(in_array($fkMethodName, $methodList), 'Selected method, '.$fkMethodName.', not in method list'); |
||
| 526 | $rkCombo = array_values(array_intersect($rkList, $methodList)); |
||
| 527 | assert(1 <= count($rkCombo), 'Expected at least 1 element in related-key list, got '.count($rkCombo)); |
||
| 528 | $rkMethodName = $rkCombo[0]; |
||
| 529 | assert(in_array($rkMethodName, $methodList), 'Selected method, '.$rkMethodName.', not in method list'); |
||
| 530 | $line = ['fk' => $fkMethodName, 'rk' => $rkMethodName]; |
||
| 531 | static::$methodAlternate[get_class($foo)] = $line; |
||
| 532 | } |
||
| 533 | } |
||
| 534 | return [$fkMethodName, $rkMethodName]; |
||
| 535 | } |
||
| 536 | |||
| 537 | /** |
||
| 538 | * @param $hooks |
||
| 539 | * @param $first |
||
| 540 | * @param $property |
||
| 541 | * @param $last |
||
| 542 | * @param $mult |
||
| 543 | * @param $targ |
||
| 544 | * @param string|null $targ |
||
| 545 | */ |
||
| 546 | private function addRelationsHook(&$hooks, $first, $property, $last, $mult, $targ, $type = null) |
||
| 547 | { |
||
| 548 | if (!isset($hooks[$first])) { |
||
| 549 | $hooks[$first] = []; |
||
| 550 | } |
||
| 551 | if (!isset($hooks[$first][$targ])) { |
||
| 552 | $hooks[$first][$targ] = []; |
||
| 553 | } |
||
| 554 | $hooks[$first][$targ][$property] = [ |
||
| 555 | 'property' => $property, |
||
| 556 | 'local' => $last, |
||
| 557 | 'multiplicity' => $mult, |
||
| 558 | 'type' => $type |
||
| 559 | ]; |
||
| 560 | } |
||
| 561 | |||
| 562 | /** |
||
| 563 | * @param $rels |
||
| 564 | * @param $hooks |
||
| 565 | */ |
||
| 566 | private function getRelationshipsHasMany($rels, &$hooks) |
||
| 590 | |||
| 591 | /** |
||
| 592 | * @param $rels |
||
| 593 | * @param $hooks |
||
| 594 | */ |
||
| 595 | private function getRelationshipsHasOne($rels, &$hooks) |
||
| 619 | |||
| 620 | /** |
||
| 621 | * @param $rels |
||
| 622 | * @param $hooks |
||
| 623 | */ |
||
| 624 | private function getRelationshipsKnownPolyMorph($rels, &$hooks) |
||
| 646 | |||
| 647 | /** |
||
| 648 | * @param $rels |
||
| 649 | * @param $hooks |
||
| 650 | */ |
||
| 651 | private function getRelationshipsUnknownPolyMorph($rels, &$hooks) |
||
| 673 | |||
| 674 | /** |
||
| 675 | * SUpplemental function to retrieve cast array for Laravel versions that do not supply hasCasts |
||
| 676 | * |
||
| 677 | * @return array |
||
| 678 | */ |
||
| 679 | public function retrieveCasts() |
||
| 683 | |||
| 684 | /** |
||
| 685 | * Return list of relations to be eager-loaded by Laravel query provider |
||
| 686 | * |
||
| 687 | * @return array |
||
| 688 | */ |
||
| 689 | public function getEagerLoad() |
||
| 694 | |||
| 695 | /** |
||
| 696 | * Set list of relations to be eager-loaded |
||
| 697 | * |
||
| 698 | * @param array $relations |
||
| 699 | */ |
||
| 700 | public function setEagerLoad(array $relations) |
||
| 706 | |||
| 707 | /* |
||
| 708 | * Is this model the known side of at least one polymorphic relation? |
||
| 709 | */ |
||
| 710 | public function isKnownPolymorphSide() |
||
| 711 | { |
||
| 712 | // isKnownPolymorph needs to be checking KnownPolymorphSide results - if you're checking UnknownPolymorphSide, |
||
| 713 | // you're turned around |
||
| 714 | $rels = $this->getRelationshipsFromMethods(); |
||
| 715 | return !empty($rels['KnownPolyMorphSide']); |
||
| 716 | } |
||
| 717 | |||
| 718 | /* |
||
| 719 | * Is this model on the unknown side of at least one polymorphic relation? |
||
| 720 | */ |
||
| 721 | public function isUnknownPolymorphSide() |
||
| 728 | |||
| 729 | /** |
||
| 730 | * Extract entity gubbins detail for later downstream use |
||
| 731 | * |
||
| 732 | * @return EntityGubbins |
||
| 733 | */ |
||
| 734 | public function extractGubbins() |
||
| 787 | } |
||
| 788 |