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 ResourcesCommand 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 ResourcesCommand, and based on these observations, apply Extract Interface, too.
1 | <?php namespace Wn\Generators\Commands; |
||
7 | class ResourcesCommand extends BaseCommand { |
||
8 | |||
9 | protected $signature = 'wn:resources |
||
10 | {files* : Paths to the files containing resources declarations} |
||
11 | {--path= : where to store the model files.} |
||
12 | {--routes= : where to store the routes.} |
||
13 | {--no-routes : do not add routes.} |
||
14 | {--controllers= : where to store the controllers.} |
||
15 | {--no-controllers : do not generate controllers.} |
||
16 | {--no-migration : do not migrate.} |
||
17 | {--check-only : only check supplied files for valide relationships.} |
||
18 | {--skip-check : skip validity check before processing.} |
||
19 | {--force= : override the existing files} |
||
20 | {--force-redefine : Force model redefinition.} |
||
21 | {--laravel= : Use Laravel style route definitions} |
||
22 | '; |
||
23 | |||
24 | protected $description = 'Generates multiple resources from a couple of files'; |
||
25 | |||
26 | protected $pivotTables = []; |
||
27 | protected $morphTables = []; |
||
28 | |||
29 | private $checkedErrors = 0; |
||
30 | private $checkErrors = []; |
||
31 | private $checkInfo = []; |
||
32 | |||
33 | public function handle() |
||
34 | { |
||
35 | $files = $this->argument('files'); |
||
36 | $nodes = []; |
||
37 | |||
38 | if (empty($files)) { |
||
39 | $this->error("No resource file(s) supplied!"); |
||
40 | return; |
||
41 | } |
||
42 | if (is_string($files)) { |
||
43 | $files = [ $files ]; |
||
44 | } |
||
45 | |||
46 | foreach ($files as $file) { |
||
47 | $nodes = $this->mergeNodes($nodes, $this->readFile($file), $this->option('force-redefine')); |
||
48 | } |
||
49 | |||
50 | $this->line(''); |
||
51 | $this->info('Bringing models to order...'); |
||
52 | |||
53 | $nodes = $this->sortDependencies($nodes); |
||
54 | $pivotTables = $this->uniqueArray($this->getTables($nodes, 'pivotTables')); |
||
55 | $morphTables = $this->uniqueArray($this->getTables($nodes, 'morphTables')); |
||
56 | |||
57 | if (! $this->option('skip-check')) { |
||
58 | $this->info('Checking Relationships...'); |
||
59 | $keys = array_keys($nodes); |
||
60 | foreach ($nodes as $model => $i) { |
||
61 | $this->checkRelations($i['belongsTo'], 'belongsTo', $i['filename'], $i['uniquename'], $keys); |
||
62 | // $this->checkRelations($i['hasManyThrough'], 'hasManyThrough', $file, $model); |
||
63 | } |
||
64 | $this->checkPivotRelations($nodes, $pivotTables, 'pivot'); |
||
65 | $this->checkPivotRelations($nodes, $morphTables, 'morph'); |
||
66 | } |
||
67 | |||
68 | View Code Duplication | if ($this->checkedErrors > 0) { |
|
|
|||
69 | $this->line(''); |
||
70 | if ($this->option('check-only')) { |
||
71 | $this->info('Checking only, we have found ' . $this->checkedErrors . ' errors.'); |
||
72 | } |
||
73 | $this->printErrors(); |
||
74 | } |
||
75 | |||
76 | $proceed = (! $this->option('check-only') && $this->checkedErrors == 0) || $this->option('skip-check'); |
||
77 | View Code Duplication | if (! $this->option('check-only') && $this->checkedErrors > 0) { |
|
78 | $this->line(''); |
||
79 | $proceed = $this->confirm("We have found " . $this->checkedErrors . " errors. Are you sure you want to continue?"); |
||
80 | } |
||
81 | if ($proceed) { |
||
82 | $this->buildResources($nodes); |
||
83 | |||
84 | // if (!$this->option('no-migration')) { |
||
85 | // $this->call('migrate'); // actually needed for pivot seeders ! |
||
86 | // } |
||
87 | |||
88 | $this->line(''); |
||
89 | $this->buildTables('Pivot-Table', 'wn:pivot-table', 'model1', 'model2', $pivotTables); |
||
90 | |||
91 | $this->line(''); |
||
92 | $this->buildTables('Morph-Table', 'wn:morph-table', 'model', 'morphable', $morphTables); |
||
93 | |||
94 | if (!$this->option('no-migration')) { |
||
95 | $this->call('migrate'); |
||
96 | } |
||
97 | } |
||
98 | } |
||
99 | |||
100 | protected function uniqueArray($array) |
||
101 | { |
||
102 | return array_map( |
||
103 | 'unserialize', |
||
104 | array_unique(array_map('serialize', $array)) |
||
105 | ); |
||
106 | } |
||
107 | |||
108 | protected function readFile($file) |
||
109 | { |
||
110 | $this->info("Reading file ".$file); |
||
111 | |||
112 | $content = $this->fs->get($file); |
||
113 | $content = Yaml::parse($content); |
||
114 | |||
115 | $nodes = []; |
||
116 | |||
117 | foreach ($content as $model => $i){ |
||
118 | /* |
||
119 | $i['modelname'] = as originally in YAML defined |
||
120 | $i['name'] = as originally defined in snake_case |
||
121 | $i['uniquename']= for key in singular studly_case |
||
122 | */ |
||
123 | $i['filename'] = $file; |
||
124 | $i['modelname'] = $model; |
||
125 | $model = studly_case(str_singular($model)); |
||
126 | $i['uniquename'] = $model; |
||
127 | |||
128 | $nodes[] = $this->getResourceParams($i); |
||
129 | } |
||
130 | |||
131 | return $nodes; |
||
132 | } |
||
133 | |||
134 | protected function mergeNodes($nodes, $toMerge, $forceRedefinition = false) { |
||
135 | foreach($toMerge as $node) { |
||
136 | $nodes = $this->mergeNode($nodes, $node, $forceRedefinition); |
||
137 | } |
||
138 | |||
139 | return $nodes; |
||
140 | } |
||
141 | |||
142 | protected function mergeNode($nodes, $toMerge, $forceRedefinition = false) { |
||
143 | if (empty($nodes[$toMerge['uniquename']]) || $forceRedefinition) { |
||
144 | if (!empty($nodes[$toMerge['uniquename']])) { |
||
145 | $this->checkError($toMerge['uniquename'] . ": forced to redefine (in file " . $nodes[$toMerge['uniquename']]['filename'] . ", redefined from file ".$toMerge['filename'].")"); |
||
146 | } |
||
147 | $nodes[$toMerge['uniquename']] = $toMerge; |
||
148 | } else { |
||
149 | $this->checkError($toMerge['uniquename'] . ": already defined (in file " . $nodes[$toMerge['uniquename']]['filename'] . ", trying to redefine from file ".$toMerge['filename']."; Use --force-redefine to force redefinition)"); |
||
150 | } |
||
151 | |||
152 | return $nodes; |
||
153 | } |
||
154 | |||
155 | protected function getTables($nodes, $key) { |
||
156 | $tables = []; |
||
157 | foreach($nodes as $node) { |
||
158 | if (!empty($node[$key])) { |
||
159 | $tables = array_merge($tables, $node[$key]); |
||
160 | } |
||
161 | } |
||
162 | |||
163 | return $tables; |
||
164 | } |
||
165 | |||
166 | protected function buildResources($nodes) |
||
167 | { |
||
168 | $modelIndex = 0; |
||
169 | $migrationIdLength = strlen((string)count($nodes)); |
||
170 | foreach ($nodes as $i) { |
||
171 | $migrationName = 'Create' . ucwords(str_plural($i['name'])); |
||
172 | $migrationFile = date('Y_m_d_His') . '-' . str_pad($modelIndex , $migrationIdLength, 0, STR_PAD_LEFT) . '_' . snake_case($migrationName) . '_table'; |
||
173 | |||
174 | $this->line(''); |
||
175 | $this->info('Building Model ' . $i['uniquename']); |
||
176 | |||
177 | $options = [ |
||
178 | 'name' => $i['name'], |
||
179 | 'fields' => $i['fields'], |
||
180 | '--add' => $i['add'], |
||
181 | '--has-many' => $i['hasMany'], |
||
182 | '--has-one' => $i['hasOne'], |
||
183 | '--belongs-to' => $i['belongsTo'], |
||
184 | '--belongs-to-many' => $i['belongsToMany'], |
||
185 | '--has-many-through' => $i['hasManyThrough'], |
||
186 | '--morph-to' => $i['morphTo'], |
||
187 | '--morph-many' => $i['morphMany'], |
||
188 | '--morph-to-many' => $i['morphToMany'], |
||
189 | '--morphed-by-many' => $i['morphedByMany'], |
||
190 | '--no-routes' => $this->option('no-routes'), |
||
191 | '--no-controller' => $this->option('no-controllers'), |
||
192 | '--force' => $this->option('force'), |
||
193 | '--migration-file' => $migrationFile, |
||
194 | ]; |
||
195 | if ($this->option('laravel')) { |
||
196 | $options['--laravel'] = true; |
||
197 | } |
||
198 | if ($this->option('routes')) { |
||
199 | $options['--routes'] = $this->option('routes'); |
||
200 | } |
||
201 | if ($this->option('controllers')) { |
||
202 | $options['--controller'] = $this->option('controllers'); |
||
203 | } |
||
204 | if ($this->option('path')) { |
||
205 | $options['--path'] = $this->option('path'); |
||
206 | } |
||
207 | |||
208 | $this->call('wn:resource', $options); |
||
209 | $modelIndex++; |
||
210 | } |
||
211 | } |
||
212 | |||
213 | protected function buildTables($type, $command, $model1, $model2, $tableAssignment) |
||
214 | { |
||
215 | foreach ($tableAssignment as $tables) { |
||
216 | $this->info('Building '.$type.' ' . $tables[0] . ' - ' . $tables[1]); |
||
217 | $this->call($command, [ |
||
218 | $model1 => $tables[0], |
||
219 | $model2 => $tables[1], |
||
220 | '--force' => $this->option('force') |
||
221 | ]); |
||
222 | |||
223 | // $this->call('wn:pivot-seeder', [ |
||
224 | // 'model1' => $tables[0], |
||
225 | // 'model2' => $tables[1], |
||
226 | // '--force' => $this->option('force') |
||
227 | // ]); |
||
228 | } |
||
229 | } |
||
230 | |||
231 | protected function getResourceParams($i) |
||
232 | { |
||
233 | $modelName = $i['modelname']; |
||
234 | |||
235 | $i['filename'] = $i['filename']; |
||
236 | $i['name'] = snake_case($modelName); |
||
237 | $i['modelname'] = $i['modelname']; |
||
238 | $i['uniquename'] = $i['uniquename']; |
||
239 | |||
240 | foreach(['hasMany', 'hasOne', 'add', 'belongsTo', 'belongsToMany', 'hasManyThrough', 'morphTo', 'morphMany', 'morphToMany', 'morphedByMany'] as $relation){ |
||
241 | if(isset($i[$relation])){ |
||
242 | $i[$relation] = $this->convertArray($i[$relation], ' ', ','); |
||
243 | } else { |
||
244 | $i[$relation] = false; |
||
245 | } |
||
246 | } |
||
247 | |||
248 | if($i['belongsToMany']){ |
||
249 | $i['pivotTables'] = $this->belongsTo($i['name'], $modelName, $i['belongsToMany']); |
||
250 | } |
||
251 | |||
252 | if($i['morphToMany']){ |
||
253 | $i['morphTables'] = $this->morphToMany($modelName, $i['morphToMany']); |
||
254 | } |
||
255 | |||
256 | if($i['morphedByMany']){ |
||
257 | $i['morphTables'] = array_merge($i['morphTables'], $this->morphedByMany($i['name'], $modelName, $i['morphedByMany'])); |
||
258 | } |
||
259 | |||
260 | $fields = []; |
||
261 | foreach($i['fields'] as $name => $value) { |
||
262 | $value['name'] = $name; |
||
263 | $fields[] = $this->serializeField($value); |
||
264 | } |
||
265 | $i['fields'] = implode(' ', $fields); |
||
266 | |||
267 | return $i; |
||
268 | } |
||
269 | |||
270 | protected function parseRelations($parser, $relations, $callback) |
||
271 | { |
||
272 | $parsedRelations = []; |
||
273 | $relations = $this->getArgumentParser($parser)->parse($relations); |
||
274 | foreach ($relations as $relation){ |
||
275 | $parsedRelations[] = $callback($relation); |
||
276 | } |
||
277 | |||
278 | return $parsedRelations; |
||
279 | } |
||
280 | |||
281 | protected function getConditionalTableName($condition, $then, $else) |
||
282 | { |
||
283 | if($condition){ |
||
284 | return snake_case($this->extractClassName($then)); |
||
285 | } else { |
||
286 | return snake_case($this->extractClassName($else)); |
||
287 | } |
||
288 | } |
||
289 | |||
290 | protected function belongsTo($name, $modelName, $belongsTo) |
||
291 | { |
||
292 | return $this->parseRelations('relations', $belongsTo, function($relation) use ($name, $modelName) { |
||
293 | $table = $this->getConditionalTableName(! $relation['model'], $relation['name'], $relation['model']); |
||
294 | $tables = [ str_singular($table), $name ]; |
||
295 | sort($tables); |
||
296 | $tables[] = $modelName; |
||
297 | return $tables; |
||
298 | }); |
||
299 | } |
||
300 | |||
301 | protected function morphToMany($modelName, $morphToMany) |
||
302 | { |
||
303 | return $this->parseRelations('relations-morphMany', $morphToMany, function($relation) use ($modelName) { |
||
304 | $name = $this->getConditionalTableName(! $relation['through'], $relation['name'], $relation['model']); |
||
305 | return $this->getMorphableRelation($relation, $name, $modelName); |
||
306 | }); |
||
307 | } |
||
308 | |||
309 | protected function morphedByMany($name, $modelName, $morphedByMany) |
||
310 | { |
||
311 | return $this->parseRelations('relations-morphMany', $morphedByMany, function($relation) use ($name, $modelName) { |
||
312 | return $this->getMorphableRelation($relation, $name, $modelName); |
||
313 | }); |
||
314 | } |
||
315 | |||
316 | protected function getMorphableRelation($relation, $relationName, $modelName) { |
||
317 | $morphable = $this->getConditionalTableName(! $relation['through'], $relation['model'], $relation['through']); |
||
318 | return [ str_singular($relationName), str_singular($morphable), $modelName ]; |
||
319 | } |
||
320 | |||
321 | protected function serializeField($field) |
||
336 | |||
337 | protected function convertArray($list, $old, $new) |
||
343 | |||
344 | private function sortDependencies($nodes) { |
||
359 | |||
360 | private function getDependencies($nodes, $key, $seen = array()) { |
||
400 | |||
401 | protected function checkError($message, $model = "", $file = "") { |
||
405 | |||
406 | protected function checkInfo($message, $model = "", $file = "") { |
||
409 | |||
410 | protected function printErrors() { |
||
418 | |||
419 | protected function checkRelations($relations, $type, $file, $model, $keys) { |
||
420 | if ($relations) { |
||
421 | $position = array_search($model, $keys); |
||
422 | $relations = $this->getArgumentParser('relations')->parse($relations); |
||
423 | foreach($relations as $relation) { |
||
424 | $rModel = $relation['model'] ? $relation['model'] : $relation['name']; |
||
425 | $search = array_search(studly_case(str_singular($rModel)), $keys); |
||
426 | if (($search === false || $search > $position) && !class_exists($this->prependNamespace($rModel)) && !class_exists($this->prependNamespace($rModel, 'App'))) { |
||
427 | $this->checkError(studly_case(str_singular($rModel)) . ": undefined (used in " . $type . "-relationship of model " . $model . " in file " . $file . ")"); |
||
428 | } else if (class_exists($this->prependNamespace($rModel))) { |
||
429 | $this->checkInfo(studly_case(str_singular($rModel)) . ": already defined in Namespace " . $this->getNamespace() . " (used in " . $type . "-relationship of model " . $model . " in file " . $file . ")"); |
||
430 | } else if (class_exists($this->prependNamespace($rModel, 'App'))) { |
||
431 | $this->checkInfo(studly_case(str_singular($rModel)) . ": already defined in Namespace App\\ (used in " . $type . "-relationship of model " . $model . " in file " . $file . ")"); |
||
432 | } |
||
433 | } |
||
436 | |||
437 | protected function checkPivotRelations($nodes, $relations, $relationType) { |
||
451 | |||
452 | protected function checkRelation($nodes, $relationType, $relation, $model) { |
||
461 | |||
462 | } |
||
463 |
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.