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 ModelTask 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 ModelTask, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
28 | class ModelTask extends BakeTask |
||
29 | { |
||
30 | /** |
||
31 | * path to Model directory |
||
32 | * |
||
33 | * @var string |
||
34 | */ |
||
35 | public $pathFragment = 'Model/'; |
||
36 | |||
37 | /** |
||
38 | * tasks |
||
39 | * |
||
40 | * @var array |
||
41 | */ |
||
42 | public $tasks = [ |
||
43 | 'Bake.DbConfig', |
||
44 | 'Bake.Fixture', |
||
45 | 'Bake.BakeTemplate', |
||
46 | 'Bake.Test' |
||
47 | ]; |
||
48 | |||
49 | /** |
||
50 | * Tables to skip when running all() |
||
51 | * |
||
52 | * @var array |
||
53 | */ |
||
54 | public $skipTables = ['i18n', 'cake_sessions', 'phinxlog', 'users_phinxlog']; |
||
55 | |||
56 | /** |
||
57 | * Holds tables found on connection. |
||
58 | * |
||
59 | * @var array |
||
60 | */ |
||
61 | protected $_tables = []; |
||
62 | |||
63 | /** |
||
64 | * Holds the model names |
||
65 | * |
||
66 | * @var array |
||
67 | */ |
||
68 | protected $_modelNames = []; |
||
69 | |||
70 | /** |
||
71 | * Holds validation method map. |
||
72 | * |
||
73 | * @var array |
||
74 | */ |
||
75 | protected $_validations = []; |
||
76 | |||
77 | /** |
||
78 | * Execution method always used for tasks |
||
79 | * |
||
80 | * @param string|null $name The name of the table to bake. |
||
81 | * @return void |
||
82 | */ |
||
83 | View Code Duplication | public function main($name = null) |
|
84 | { |
||
85 | parent::main(); |
||
86 | $name = $this->_getName($name); |
||
87 | |||
88 | if (empty($name)) { |
||
89 | $this->out('Choose a model to bake from the following:'); |
||
90 | foreach ($this->listUnskipped() as $table) { |
||
91 | $this->out('- ' . $this->_camelize($table)); |
||
92 | } |
||
93 | return true; |
||
|
|||
94 | } |
||
95 | |||
96 | $this->bake($this->_camelize($name)); |
||
97 | } |
||
98 | |||
99 | /** |
||
100 | * Generate code for the given model name. |
||
101 | * |
||
102 | * @param string $name The model name to generate. |
||
103 | * @return void |
||
104 | */ |
||
105 | public function bake($name) |
||
106 | { |
||
107 | $table = $this->getTable($name); |
||
108 | $model = $this->getTableObject($name, $table); |
||
109 | |||
110 | $associations = $this->getAssociations($model); |
||
111 | $this->applyAssociations($model, $associations); |
||
112 | |||
113 | $primaryKey = $this->getPrimaryKey($model); |
||
114 | $displayField = $this->getDisplayField($model); |
||
115 | $propertySchema = $this->getEntityPropertySchema($model); |
||
116 | $fields = $this->getFields(); |
||
117 | $validation = $this->getValidation($model, $associations); |
||
118 | $rulesChecker = $this->getRules($model, $associations); |
||
119 | $behaviors = $this->getBehaviors($model); |
||
120 | $connection = $this->connection; |
||
121 | |||
122 | $data = compact( |
||
123 | 'associations', |
||
124 | 'primaryKey', |
||
125 | 'displayField', |
||
126 | 'table', |
||
127 | 'propertySchema', |
||
128 | 'fields', |
||
129 | 'validation', |
||
130 | 'rulesChecker', |
||
131 | 'behaviors', |
||
132 | 'connection' |
||
133 | ); |
||
134 | $this->bakeTable($model, $data); |
||
135 | $this->bakeEntity($model, $data); |
||
136 | $this->bakeFixture($model->alias(), $model->table()); |
||
137 | $this->bakeTest($model->alias()); |
||
138 | } |
||
139 | |||
140 | /** |
||
141 | * Bake all models at once. |
||
142 | * |
||
143 | * @return void |
||
144 | */ |
||
145 | public function all() |
||
146 | { |
||
147 | $tables = $this->listUnskipped(); |
||
148 | foreach ($tables as $table) { |
||
149 | TableRegistry::clear(); |
||
150 | $this->main($table); |
||
151 | } |
||
152 | } |
||
153 | |||
154 | /** |
||
155 | * Get a model object for a class name. |
||
156 | * |
||
157 | * @param string $className Name of class you want model to be. |
||
158 | * @param string $table Table name |
||
159 | * @return \Cake\ORM\Table Table instance |
||
160 | */ |
||
161 | public function getTableObject($className, $table) |
||
162 | { |
||
163 | if (TableRegistry::exists($className)) { |
||
164 | return TableRegistry::get($className); |
||
165 | } |
||
166 | return TableRegistry::get($className, [ |
||
167 | 'name' => $className, |
||
168 | 'table' => $table, |
||
169 | 'connection' => ConnectionManager::get($this->connection) |
||
170 | ]); |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * Get the array of associations to generate. |
||
175 | * |
||
176 | * @param \Cake\ORM\Table $table The table to get associations for. |
||
177 | * @return array |
||
178 | */ |
||
179 | public function getAssociations(Table $table) |
||
180 | { |
||
181 | if (!empty($this->params['no-associations'])) { |
||
182 | return []; |
||
183 | } |
||
184 | $this->out('One moment while associations are detected.'); |
||
185 | |||
186 | $this->listAll(); |
||
187 | |||
188 | $associations = [ |
||
189 | 'belongsTo' => [], |
||
190 | 'hasMany' => [], |
||
191 | 'belongsToMany' => [] |
||
192 | ]; |
||
193 | |||
194 | $primary = $table->primaryKey(); |
||
195 | $associations = $this->findBelongsTo($table, $associations); |
||
196 | |||
197 | if (is_array($primary) && count($primary) > 1) { |
||
198 | $this->err( |
||
199 | '<warning>Bake cannot generate associations for composite primary keys at this time</warning>.' |
||
200 | ); |
||
201 | return $associations; |
||
202 | } |
||
203 | |||
204 | $associations = $this->findHasMany($table, $associations); |
||
205 | $associations = $this->findBelongsToMany($table, $associations); |
||
206 | return $associations; |
||
207 | } |
||
208 | |||
209 | /** |
||
210 | * Sync the in memory table object. |
||
211 | * |
||
212 | * Composer's class cache prevents us from loading the |
||
213 | * newly generated class. Applying associations if we have a |
||
214 | * generic table object means fields will be detected correctly. |
||
215 | * |
||
216 | * @param \Cake\ORM\Table $model The table to apply associations to. |
||
217 | * @param array $associations The associations to append. |
||
218 | * @return void |
||
219 | */ |
||
220 | public function applyAssociations($model, $associations) |
||
233 | |||
234 | /** |
||
235 | * Find belongsTo relations and add them to the associations list. |
||
236 | * |
||
237 | * @param \Cake\ORM\Table $model Database\Table instance of table being generated. |
||
238 | * @param array $associations Array of in progress associations |
||
239 | * @return array Associations with belongsTo added in. |
||
240 | */ |
||
241 | public function findBelongsTo($model, array $associations) |
||
281 | |||
282 | /** |
||
283 | * find the table, if any, actually referenced by the passed key field. |
||
284 | * Search tables in db for keyField; if found search key constraints |
||
285 | * for the table to which it refers. |
||
286 | * |
||
287 | * @param \Cake\Database\Schema\Table $schema The table schema to find a constraint for. |
||
288 | * @param string $keyField The field to check for a constraint. |
||
289 | * @return string|null Either the referenced table or null if the field has no constraints. |
||
290 | */ |
||
291 | public function findTableReferencedBy($schema, $keyField) |
||
307 | |||
308 | /** |
||
309 | * Find the hasMany relations and add them to associations list |
||
310 | * |
||
311 | * @param \Cake\ORM\Table $model Model instance being generated |
||
312 | * @param array $associations Array of in progress associations |
||
313 | * @return array Associations with hasMany added in. |
||
314 | */ |
||
315 | public function findHasMany($model, array $associations) |
||
358 | |||
359 | /** |
||
360 | * Find the BelongsToMany relations and add them to associations list |
||
361 | * |
||
362 | * @param \Cake\ORM\Table $model Model instance being generated |
||
363 | * @param array $associations Array of in-progress associations |
||
364 | * @return array Associations with belongsToMany added in. |
||
365 | */ |
||
366 | public function findBelongsToMany($model, array $associations) |
||
399 | |||
400 | /** |
||
401 | * Get the display field from the model or parameters |
||
402 | * |
||
403 | * @param \Cake\ORM\Table $model The model to introspect. |
||
404 | * @return string |
||
405 | */ |
||
406 | public function getDisplayField($model) |
||
413 | |||
414 | /** |
||
415 | * Get the primary key field from the model or parameters |
||
416 | * |
||
417 | * @param \Cake\ORM\Table $model The model to introspect. |
||
418 | * @return array The columns in the primary key |
||
419 | */ |
||
420 | public function getPrimaryKey($model) |
||
428 | |||
429 | /** |
||
430 | * Returns an entity property "schema". |
||
431 | * |
||
432 | * The schema is an associative array, using the property names |
||
433 | * as keys, and information about the property as the value. |
||
434 | * |
||
435 | * The value part consists of at least two keys: |
||
436 | * |
||
437 | * - `kind`: The kind of property, either `column`, which indicates |
||
438 | * that the property stems from a database column, or `association`, |
||
439 | * which identifies a property that is generated for an associated |
||
440 | * table. |
||
441 | * - `type`: The type of the property value. For the `column` kind |
||
442 | * this is the database type associated with the column, and for the |
||
443 | * `association` type it's the FQN of the entity class for the |
||
444 | * associated table. |
||
445 | * |
||
446 | * For `association` properties an additional key will be available |
||
447 | * |
||
448 | * - `association`: Holds an instance of the corresponding association |
||
449 | * class. |
||
450 | * |
||
451 | * @param \Cake\ORM\Table $model The model to introspect. |
||
452 | * @return array The property schema |
||
453 | */ |
||
454 | public function getEntityPropertySchema(Table $model) |
||
491 | |||
492 | /** |
||
493 | * Evaluates the fields and no-fields options, and |
||
494 | * returns if, and which fields should be made accessible. |
||
495 | * |
||
496 | * @return array|bool|null Either an array of fields, `false` in |
||
497 | * case the no-fields option is used, or `null` if none of the |
||
498 | * field options is used. |
||
499 | */ |
||
500 | public function getFields() |
||
511 | |||
512 | /** |
||
513 | * Get the hidden fields from a model. |
||
514 | * |
||
515 | * Uses the hidden and no-hidden options. |
||
516 | * |
||
517 | * @param \Cake\ORM\Table $model The model to introspect. |
||
518 | * @return array The columns to make accessible |
||
519 | */ |
||
520 | public function getHiddenFields($model) |
||
534 | |||
535 | /** |
||
536 | * Generate default validation rules. |
||
537 | * |
||
538 | * @param \Cake\ORM\Table $model The model to introspect. |
||
539 | * @param array $associations The associations list. |
||
540 | * @return array The validation rules. |
||
541 | */ |
||
542 | public function getValidation($model, $associations = []) |
||
573 | |||
574 | /** |
||
575 | * Does individual field validation handling. |
||
576 | * |
||
577 | * @param \Cake\Database\Schema\Table $schema The table schema for the current field. |
||
578 | * @param string $fieldName Name of field to be validated. |
||
579 | * @param array $metaData metadata for field |
||
580 | * @param string $primaryKey The primary key field |
||
581 | * @return array Array of validation for the field. |
||
582 | */ |
||
583 | public function fieldValidation($schema, $fieldName, array $metaData, $primaryKey) |
||
640 | |||
641 | /** |
||
642 | * Generate default rules checker. |
||
643 | * |
||
644 | * @param \Cake\ORM\Table $model The model to introspect. |
||
645 | * @param array $associations The associations for the model. |
||
646 | * @return array The rules to be applied. |
||
647 | */ |
||
648 | public function getRules($model, array $associations) |
||
686 | |||
687 | /** |
||
688 | * Get behaviors |
||
689 | * |
||
690 | * @param \Cake\ORM\Table $model The model to generate behaviors for. |
||
691 | * @return array Behaviors |
||
692 | */ |
||
693 | public function getBehaviors($model) |
||
718 | |||
719 | /** |
||
720 | * Get CounterCaches |
||
721 | * |
||
722 | * @param \Cake\ORM\Table $model The table to get counter cache fields for. |
||
723 | * @return array CounterCache configurations |
||
724 | */ |
||
725 | public function getCounterCache($model) |
||
748 | |||
749 | /** |
||
750 | * Bake an entity class. |
||
751 | * |
||
752 | * @param \Cake\ORM\Table $model Model name or object |
||
753 | * @param array $data An array to use to generate the Table |
||
754 | * @return string |
||
755 | */ |
||
756 | public function bakeEntity($model, array $data = []) |
||
789 | |||
790 | /** |
||
791 | * Bake a table class. |
||
792 | * |
||
793 | * @param \Cake\ORM\Table $model Model name or object |
||
794 | * @param array $data An array to use to generate the Table |
||
795 | * @return string |
||
796 | */ |
||
797 | public function bakeTable($model, array $data = []) |
||
846 | |||
847 | /** |
||
848 | * Outputs the a list of possible models or controllers from database |
||
849 | * |
||
850 | * @return array |
||
851 | */ |
||
852 | public function listAll() |
||
865 | |||
866 | /** |
||
867 | * Outputs the a list of unskipped models or controllers from database |
||
868 | * |
||
869 | * @return array |
||
870 | */ |
||
871 | public function listUnskipped() |
||
876 | |||
877 | /** |
||
878 | * Get an Array of all the tables in the supplied connection |
||
879 | * will halt the script if no tables are found. |
||
880 | * |
||
881 | * @return array Array of tables in the database. |
||
882 | * @throws \InvalidArgumentException When connection class |
||
883 | * does not have a schemaCollection method. |
||
884 | */ |
||
885 | protected function _getAllTables() |
||
903 | |||
904 | /** |
||
905 | * Get the table name for the model being baked. |
||
906 | * |
||
907 | * Uses the `table` option if it is set. |
||
908 | * |
||
909 | * @param string $name Table name |
||
910 | * @return string |
||
911 | */ |
||
912 | public function getTable($name) |
||
919 | |||
920 | /** |
||
921 | * Gets the option parser instance and configures it. |
||
922 | * |
||
923 | * @return \Cake\Console\ConsoleOptionParser |
||
924 | */ |
||
925 | public function getOptionParser() |
||
979 | |||
980 | /** |
||
981 | * Interact with FixtureTask to automatically bake fixtures when baking models. |
||
982 | * |
||
983 | * @param string $className Name of class to bake fixture for |
||
984 | * @param string|null $useTable Optional table name for fixture to use. |
||
985 | * @return void |
||
986 | * @see FixtureTask::bake |
||
987 | */ |
||
988 | View Code Duplication | public function bakeFixture($className, $useTable = null) |
|
997 | |||
998 | /** |
||
999 | * Assembles and writes a unit test file |
||
1000 | * |
||
1001 | * @param string $className Model class name |
||
1002 | * @return string |
||
1003 | */ |
||
1004 | View Code Duplication | public function bakeTest($className) |
|
1013 | } |
||
1014 |
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.