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 AbstractModel 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 AbstractModel, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
14 | abstract class AbstractModel |
||
15 | { |
||
16 | /** |
||
17 | * The model's attributes |
||
18 | * |
||
19 | * @var Attributes |
||
20 | */ |
||
21 | protected $attributes; |
||
22 | |||
23 | /** |
||
24 | * The Model's has-one embeds |
||
25 | * |
||
26 | * @var Embeds\HasOne |
||
27 | */ |
||
28 | protected $hasOneEmbeds; |
||
29 | |||
30 | /** |
||
31 | * The Model's has-many embeds |
||
32 | * |
||
33 | * @var Embeds\HasMany |
||
34 | */ |
||
35 | protected $hasManyEmbeds; |
||
36 | |||
37 | /** |
||
38 | * The metadata that defines this Model. |
||
39 | * |
||
40 | * @var AttributeInterface |
||
41 | */ |
||
42 | protected $metadata; |
||
43 | |||
44 | /** |
||
45 | * The model state. |
||
46 | * |
||
47 | * @var State |
||
48 | */ |
||
49 | protected $state; |
||
50 | |||
51 | /** |
||
52 | * The Model Store for handling lifecycle operations. |
||
53 | * |
||
54 | * @var Store |
||
55 | */ |
||
56 | protected $store; |
||
57 | |||
58 | /** |
||
59 | * Constructor. |
||
60 | * |
||
61 | * @param AttributeInterface $metadata |
||
62 | * @param Store $store |
||
63 | * @param array|null $properties |
||
64 | */ |
||
65 | public function __construct(AttributeInterface $metadata, Store $store, array $properties = null) |
||
72 | |||
73 | /** |
||
74 | * Cloner. |
||
75 | * Ensures sub objects are also cloned. |
||
76 | * |
||
77 | */ |
||
78 | public function __clone() |
||
85 | |||
86 | /** |
||
87 | * Applies an array of raw model properties to the model instance. |
||
88 | * |
||
89 | * @todo Confirm that we want this method. It's currently used for creating and updating via the API adapter. Also see initialize() |
||
90 | * @param array $properties The properties to apply. |
||
91 | * @return self |
||
92 | */ |
||
93 | public function apply(array $properties) |
||
130 | |||
131 | /** |
||
132 | * Clears a property value. |
||
133 | * For an attribute, will set the value to null. |
||
134 | * For collections, will clear the collection contents. |
||
135 | * |
||
136 | * @api |
||
137 | * @param string $key The property key. |
||
138 | * @return self |
||
139 | */ |
||
140 | public function clear($key) |
||
156 | |||
157 | /** |
||
158 | * Creates a new Embed model instance for the provided property key. |
||
159 | * |
||
160 | * @param string $key |
||
161 | * @return Embed |
||
162 | * @throws \RuntimeException |
||
163 | */ |
||
164 | public function createEmbedFor($key) |
||
175 | |||
176 | /** |
||
177 | * Gets a model property. |
||
178 | * Returns null if the property does not exist on the model or is not set. |
||
179 | * |
||
180 | * @api |
||
181 | * @param string $key The property field key. |
||
182 | * @return Model|Model[]|Embed|Collections\EmbedCollection|null|mixed |
||
183 | */ |
||
184 | public function get($key) |
||
193 | |||
194 | /** |
||
195 | * Gets the current change set of properties. |
||
196 | * |
||
197 | * @api |
||
198 | * @return array |
||
199 | */ |
||
200 | public function getChangeSet() |
||
208 | |||
209 | /** |
||
210 | * Gets the metadata for this model. |
||
211 | * |
||
212 | * @api |
||
213 | * @return AttributeInterface |
||
214 | */ |
||
215 | public function getMetadata() |
||
219 | |||
220 | /** |
||
221 | * Gets the model state object. |
||
222 | * |
||
223 | * @todo Should this be public? State setting should likely be locked from the outside world. |
||
224 | * @return State |
||
225 | */ |
||
226 | public function getState() |
||
230 | |||
231 | /** |
||
232 | * Gets the model store. |
||
233 | * |
||
234 | * @api |
||
235 | * @return Store |
||
236 | */ |
||
237 | public function getStore() |
||
241 | |||
242 | /** |
||
243 | * Initializes the model and loads its attributes and relationships. |
||
244 | * |
||
245 | * @todo Made public so collections can initialize models. Not sure if we want this?? |
||
246 | * @param array|null $properties The db properties to apply. |
||
247 | * @return self |
||
248 | */ |
||
249 | public function initialize(array $properties = null) |
||
283 | |||
284 | /** |
||
285 | * Determines if a property key is an attribute. |
||
286 | * |
||
287 | * @api |
||
288 | * @param string $key The property key. |
||
289 | * @return bool |
||
290 | */ |
||
291 | public function isAttribute($key) |
||
295 | |||
296 | /** |
||
297 | * Determines if the model is currently dirty. |
||
298 | * |
||
299 | * @api |
||
300 | * @return bool |
||
301 | */ |
||
302 | public function isDirty() |
||
309 | |||
310 | /** |
||
311 | * Determines if a property key is an embedded property. |
||
312 | * |
||
313 | * @api |
||
314 | * @param string $key The property key. |
||
315 | * @return bool |
||
316 | */ |
||
317 | public function isEmbed($key) |
||
321 | |||
322 | /** |
||
323 | * Determines if a property key is a has-many embed. |
||
324 | * |
||
325 | * @api |
||
326 | * @param string $key The property key. |
||
327 | * @return bool |
||
328 | */ |
||
329 | public function isEmbedHasMany($key) |
||
336 | |||
337 | /** |
||
338 | * Determines if a property key is a has-one embed. |
||
339 | * |
||
340 | * @api |
||
341 | * @param string $key The property key. |
||
342 | * @return bool |
||
343 | */ |
||
344 | public function isEmbedHasOne($key) |
||
351 | |||
352 | /** |
||
353 | * Pushes an Embed into a has-many embed collection. |
||
354 | * This method must be used for has-many embeds. Direct set is not supported. |
||
355 | * To completely replace call clear() first and then pushEmbed() the new Embeds. |
||
356 | * |
||
357 | * @api |
||
358 | * @param string $key |
||
359 | * @param Embed $embed |
||
360 | * @return self |
||
361 | */ |
||
362 | public function pushEmbed($key, Embed $embed) |
||
376 | |||
377 | /** |
||
378 | * Removes a specific Embed from a has-many embed collection. |
||
379 | * |
||
380 | * @api |
||
381 | * @param string $key The has-many embed key. |
||
382 | * @param Embed $embed The embed to remove from the collection. |
||
383 | * @return self |
||
384 | */ |
||
385 | View Code Duplication | public function removeEmbed($key, Embed $embed) |
|
396 | |||
397 | /** |
||
398 | * Rolls back a model to its original values. |
||
399 | * |
||
400 | * @api |
||
401 | * @return self |
||
402 | */ |
||
403 | public function rollback() |
||
411 | |||
412 | /** |
||
413 | * Sets a model property. |
||
414 | * |
||
415 | * @api |
||
416 | * @param string $key The property field key. |
||
417 | * @param Model|Embed|null|mixed The value to set. |
||
418 | * @return self. |
||
419 | */ |
||
420 | public function set($key, $value) |
||
429 | |||
430 | /** |
||
431 | * Determines if the model uses a particlar mixin. |
||
432 | * |
||
433 | * @api |
||
434 | * @param string $name |
||
435 | * @return bool |
||
436 | */ |
||
437 | public function usesMixin($name) |
||
441 | |||
442 | /** |
||
443 | * Applies default attribute values from metadata, if set. |
||
444 | * |
||
445 | * @param array $attributes The attributes to apply the defaults to. |
||
446 | * @return array |
||
447 | */ |
||
448 | protected function applyDefaultAttrValues(array $attributes = []) |
||
459 | |||
460 | /** |
||
461 | * Converts an attribute value to the appropriate data type. |
||
462 | * |
||
463 | * @param string $key |
||
464 | * @param mixed $value |
||
465 | * @return mixed |
||
466 | */ |
||
467 | protected function convertAttributeValue($key, $value) |
||
471 | |||
472 | /** |
||
473 | * Does a dirty check and sets the state to this model. |
||
474 | * |
||
475 | * @return self |
||
476 | */ |
||
477 | protected function doDirtyCheck() |
||
482 | |||
483 | /** |
||
484 | * Removes properties marked as non-saved. |
||
485 | * |
||
486 | * @param array $properties |
||
487 | * @return array |
||
488 | */ |
||
489 | protected function filterNotSavedProperties(array $properties) |
||
499 | |||
500 | /** |
||
501 | * Gets an attribute value. |
||
502 | * |
||
503 | * @param string $key The attribute key (field) name. |
||
504 | * @return mixed |
||
505 | */ |
||
506 | protected function getAttribute($key) |
||
514 | |||
515 | /** |
||
516 | * Gets a calculated attribute value. |
||
517 | * |
||
518 | * @param string $key The attribute key (field) name. |
||
519 | * @return mixed |
||
520 | */ |
||
521 | protected function getCalculatedAttribute($key) |
||
530 | |||
531 | /** |
||
532 | * Gets a data type from an attribute key. |
||
533 | * |
||
534 | * @param string $key The attribute key. |
||
535 | * @return string |
||
536 | */ |
||
537 | protected function getDataType($key) |
||
541 | |||
542 | /** |
||
543 | * Gets an embed value. |
||
544 | * |
||
545 | * @param string $key The embed key (field) name. |
||
546 | * @return Embed|Collections\EmbedCollection|null |
||
547 | */ |
||
548 | protected function getEmbed($key) |
||
560 | |||
561 | /** |
||
562 | * Determines if an attribute key is calculated. |
||
563 | * |
||
564 | * @param string $key The attribute key. |
||
565 | * @return bool |
||
566 | */ |
||
567 | protected function isCalculatedAttribute($key) |
||
574 | |||
575 | /** |
||
576 | * Sets an attribute value. |
||
577 | * Will convert the value to the proper, internal PHP/Modlr data type. |
||
578 | * Will do a dirty check immediately after setting. |
||
579 | * |
||
580 | * @param string $key The attribute key (field) name. |
||
581 | * @param mixed $value The value to apply. |
||
582 | * @return self |
||
583 | */ |
||
584 | protected function setAttribute($key, $value) |
||
595 | |||
596 | /** |
||
597 | * Sets an embed value. |
||
598 | * |
||
599 | * @param string $key |
||
600 | * @param Embed|null $value |
||
601 | * @return self |
||
602 | */ |
||
603 | protected function setEmbed($key, $value) |
||
613 | |||
614 | /** |
||
615 | * Sets a has-one embed. |
||
616 | * |
||
617 | * @param string $key The embed key (field) name. |
||
618 | * @param Embed|null $embed The embed to relate. |
||
619 | * @return self |
||
620 | */ |
||
621 | View Code Duplication | protected function setEmbedHasOne($key, Embed $embed = null) |
|
631 | |||
632 | /** |
||
633 | * Touches the model. |
||
634 | * Must be handled the the extending class. |
||
635 | * |
||
636 | * @param bool $force Whether to force the load, even if the model is currently loaded. |
||
637 | * @return self |
||
638 | */ |
||
639 | protected function touch($force = false) |
||
643 | |||
644 | /** |
||
645 | * Validates that the model type (from a Model or Collection instance) can be set to the relationship field. |
||
646 | * |
||
647 | * @param string $embedKey The embed field key. |
||
648 | * @param string $embedName The embed name that is being set. |
||
649 | * @return self |
||
650 | */ |
||
651 | protected function validateEmbedSet($embedKey, $embedName) |
||
657 | } |
||
658 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.