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 |
||
15 | trait MetadataTrait |
||
16 | { |
||
17 | /* |
||
18 | * Array to record mapping between doctrine types and OData types |
||
19 | */ |
||
20 | protected $mapping = [ |
||
21 | 'integer' => EdmPrimitiveType::INT32, |
||
22 | 'string' => EdmPrimitiveType::STRING, |
||
23 | 'datetime' => EdmPrimitiveType::DATETIME, |
||
24 | 'float' => EdmPrimitiveType::SINGLE, |
||
25 | 'decimal' => EdmPrimitiveType::DECIMAL, |
||
26 | 'text' => EdmPrimitiveType::STRING, |
||
27 | 'boolean' => EdmPrimitiveType::BOOLEAN, |
||
28 | 3 | 'blob' => "stream" |
|
29 | ]; |
||
30 | 3 | ||
31 | /* |
||
32 | * Retrieve and assemble this model's metadata for OData packaging |
||
33 | 2 | */ |
|
34 | 2 | public function metadata() |
|
35 | { |
||
36 | 2 | assert($this instanceof Model, get_class($this)); |
|
37 | |||
38 | 2 | // Break these out separately to enable separate reuse |
|
39 | 1 | $connect = $this->getConnection(); |
|
40 | $builder = $connect->getSchemaBuilder(); |
||
41 | |||
42 | 1 | $table = $this->getTable(); |
|
43 | 1 | ||
44 | 1 | if (!$builder->hasTable($table)) { |
|
45 | return []; |
||
46 | 1 | } |
|
47 | |||
48 | 1 | $columns = $builder->getColumnListing($table); |
|
49 | 1 | $mask = $this->metadataMask(); |
|
50 | 1 | $columns = array_intersect($columns, $mask); |
|
51 | |||
52 | 1 | $tableData = []; |
|
53 | 1 | ||
54 | 1 | $rawFoo = $connect->getDoctrineSchemaManager()->listTableColumns($table); |
|
55 | $foo = []; |
||
56 | 1 | $getters = $this->collectGetters(); |
|
57 | |||
58 | 1 | foreach ($rawFoo as $key => $val) { |
|
59 | 1 | // Work around glitch in Doctrine when reading from MariaDB which added ` characters to root key value |
|
60 | 1 | $key = trim($key, '`'); |
|
61 | 1 | $foo[$key] = $val; |
|
62 | 1 | } |
|
63 | 1 | ||
64 | 1 | foreach ($columns as $column) { |
|
65 | // Doctrine schema manager returns columns with lowercased names |
||
66 | 1 | $rawColumn = $foo[strtolower($column)]; |
|
67 | $nullable = !($rawColumn->getNotNull()); |
||
68 | $fillable = in_array($column, $this->getFillable()); |
||
69 | $rawType = $rawColumn->getType(); |
||
70 | $type = $rawType->getName(); |
||
71 | $tableData[$column] = ['type' => $type, 'nullable' => $nullable, 'fillable' => $fillable]; |
||
72 | } |
||
73 | 4 | ||
74 | foreach ($getters as $get) { |
||
75 | 4 | $tableData[$get] = ['type' => 'text', 'nullable' => false, 'fillable' => false]; |
|
76 | } |
||
77 | 4 | ||
78 | 4 | return $tableData; |
|
79 | 4 | } |
|
80 | 2 | ||
81 | 4 | /* |
|
82 | 1 | * Return the set of fields that are permitted to be in metadata |
|
83 | 1 | * - following same visible-trumps-hidden guideline as Laravel |
|
84 | */ |
||
85 | 4 | public function metadataMask() |
|
101 | |||
102 | 4 | /* |
|
103 | 4 | * Get the endpoint name being exposed |
|
104 | 4 | * |
|
105 | 4 | */ |
|
106 | 4 | public function getEndpointName() |
|
117 | |||
118 | 4 | /* |
|
119 | * Assemble this model's OData metadata as xml schema |
||
120 | */ |
||
121 | public function getXmlSchema($MetaNamespace = "Data") |
||
153 | |||
154 | public function hookUpRelationships($entityTypes, $resourceSets) |
||
186 | 3 | ||
187 | 3 | public function getRelationships() |
|
203 | 3 | ||
204 | 3 | protected function getAllAttributes() |
|
229 | 1 | ||
230 | protected function getRelationshipsFromMethods($biDir = false) |
||
231 | 3 | { |
|
232 | 2 | $model = $this; |
|
233 | 2 | $relationships = array( |
|
234 | 3 | "HasOne" => array(), |
|
235 | 3 | "UnknownPolyMorphSide"=>array(), |
|
236 | 3 | "HasMany"=>array(), |
|
237 | 3 | "KnownPolyMorphSide"=>array() |
|
238 | 3 | ); |
|
239 | 3 | $methods = get_class_methods($model); |
|
240 | 3 | if (!empty($methods)) { |
|
241 | foreach ($methods as $method) { |
||
242 | if (!method_exists('Illuminate\Database\Eloquent\Model', $method) |
||
243 | ) { |
||
244 | //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php |
||
245 | $reflection = new \ReflectionMethod($model, $method); |
||
246 | |||
247 | $file = new \SplFileObject($reflection->getFileName()); |
||
248 | $file->seek($reflection->getStartLine()-1); |
||
249 | $code = ''; |
||
250 | while ($file->key() < $reflection->getEndLine()) { |
||
251 | $code .= $file->current(); |
||
252 | $file->next(); |
||
253 | } |
||
254 | |||
255 | $code = trim(preg_replace('/\s\s+/', '', $code)); |
||
256 | assert(false !== stripos($code, 'function'), 'Function definition must have keyword \'function\''); |
||
257 | $begin = strpos($code, 'function('); |
||
258 | $code = substr($code, $begin, strrpos($code, '}')-$begin+1); |
||
259 | $lastCode = $code[strlen($code)-1]; |
||
260 | assert("}" == $lastCode, "Final character of function definition must be closing brace"); |
||
261 | foreach (array( |
||
262 | 'hasMany', |
||
263 | 'hasManyThrough', |
||
264 | 'belongsToMany', |
||
265 | 'hasOne', |
||
266 | 'belongsTo', |
||
267 | 'morphOne', |
||
268 | 'morphTo', |
||
269 | 'morphMany', |
||
270 | 'morphToMany', |
||
271 | 'morphedByMany' |
||
272 | ) as $relation) { |
||
273 | $search = '$this->'.$relation.'('; |
||
274 | if ($pos = stripos($code, $search)) { |
||
275 | //Resolve the relation's model to a Relation object. |
||
276 | $relationObj = $model->$method(); |
||
277 | if ($relationObj instanceof Relation) { |
||
278 | $relatedModel = '\\'.get_class($relationObj->getRelated()); |
||
279 | $relations = [ |
||
280 | 'hasManyThrough', |
||
281 | 'belongsToMany', |
||
282 | 'hasMany', |
||
283 | 'morphMany', |
||
284 | 'morphToMany', |
||
285 | 'morphedByMany' |
||
286 | ]; |
||
287 | if (in_array($relation, $relations)) { |
||
288 | //Collection or array of models (because Collection is Arrayable) |
||
289 | $relationships["HasMany"][$method] = $biDir ? $relationObj : $relatedModel; |
||
290 | } elseif ($relation === "morphTo") { |
||
291 | // Model isn't specified because relation is polymorphic |
||
292 | $relationships["UnknownPolyMorphSide"][$method] = |
||
293 | $biDir ? $relationObj : '\Illuminate\Database\Eloquent\Model|\Eloquent'; |
||
294 | } else { |
||
295 | //Single model is returned |
||
296 | $relationships["HasOne"][$method] = $biDir ? $relationObj : $relatedModel; |
||
297 | } |
||
298 | if (in_array($relation, ["morphMany", "morphOne", "morphedByMany"])) { |
||
299 | $relationships["KnownPolyMorphSide"][$method] = |
||
300 | $biDir ? $relationObj : $relatedModel; |
||
301 | } |
||
302 | if (in_array($relation, ["morphToMany"])) { |
||
303 | $relationships["UnknownPolyMorphSide"][$method] = |
||
304 | $biDir ? $relationObj : $relatedModel; |
||
305 | } |
||
306 | } |
||
307 | } |
||
308 | } |
||
309 | } |
||
310 | } |
||
311 | } |
||
312 | return $relationships; |
||
313 | } |
||
314 | |||
315 | /** |
||
316 | * Get the visible attributes for the model. |
||
317 | * |
||
318 | * @return array |
||
319 | */ |
||
320 | public abstract function getVisible(); |
||
321 | |||
322 | /** |
||
323 | * Get the hidden attributes for the model. |
||
324 | * |
||
325 | * @return array |
||
326 | */ |
||
327 | public abstract function getHidden(); |
||
328 | |||
329 | /** |
||
330 | * Get the primary key for the model. |
||
331 | * |
||
332 | * @return string |
||
333 | */ |
||
334 | public abstract function getKeyName(); |
||
335 | |||
336 | /** |
||
337 | * Get the current connection name for the model. |
||
338 | * |
||
339 | * @return string |
||
340 | */ |
||
341 | public abstract function getConnectionName(); |
||
342 | |||
343 | /** |
||
344 | * Get the database connection for the model. |
||
345 | * |
||
346 | * @return \Illuminate\Database\Connection |
||
347 | */ |
||
348 | public abstract function getConnection(); |
||
349 | |||
350 | /** |
||
351 | * Get all of the current attributes on the model. |
||
352 | * |
||
353 | * @return array |
||
354 | */ |
||
355 | public abstract function getAttributes(); |
||
356 | |||
357 | /** |
||
358 | * Get the table associated with the model. |
||
359 | * |
||
360 | * @return string |
||
361 | */ |
||
362 | public abstract function getTable(); |
||
363 | |||
364 | /** |
||
365 | * Get the fillable attributes for the model. |
||
366 | * |
||
367 | * @return array |
||
368 | */ |
||
369 | public abstract function getFillable(); |
||
370 | |||
371 | /** |
||
372 | * Dig up all defined getters on the model |
||
373 | * |
||
374 | * @return array |
||
375 | */ |
||
376 | protected function collectGetters() |
||
394 | |||
395 | /** |
||
396 | * @param $foo |
||
397 | * @return array |
||
398 | */ |
||
399 | private function polyglotKeyMethodNames($foo, $condition = false) |
||
411 | |||
412 | /** |
||
413 | * @param $hooks |
||
414 | * @param $first |
||
415 | * @param $property |
||
416 | * @param $last |
||
417 | * @param $mult |
||
418 | * @param $targ |
||
419 | */ |
||
420 | private function addRelationsHook(&$hooks, $first, $property, $last, $mult, $targ) |
||
431 | |||
432 | /** |
||
433 | * @param $rels |
||
434 | * @param $hooks |
||
435 | */ |
||
436 | private function getRelationshipsHasMany($rels, &$hooks) |
||
459 | |||
460 | /** |
||
461 | * @param $rels |
||
462 | * @param $hooks |
||
463 | */ |
||
464 | private function getRelationshipsHasOne($rels, &$hooks) |
||
482 | |||
483 | /** |
||
484 | * @param $rels |
||
485 | * @param $hooks |
||
486 | */ |
||
487 | private function getRelationshipsKnownPolyMorph($rels, &$hooks) |
||
508 | |||
509 | /** |
||
510 | * @param $rels |
||
511 | * @param $hooks |
||
512 | */ |
||
513 | private function getRelationshipsUnknownPolyMorph($rels, &$hooks) |
||
534 | } |
||
535 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.