Completed
Pull Request — master (#66)
by
unknown
01:44
created

ResourcesCommand::buildResources()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 46
rs 8.5559
c 0
b 0
f 0
cc 6
nc 17
nop 1
1
<?php namespace Wn\Generators\Commands;
2
3
use InvalidArgumentException;
4
use Symfony\Component\Yaml\Yaml;
5
6
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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)
322
    {
323
        $name = $field['name'];
324
        $schema = $this->convertArray(str_replace(':', '.', $field['schema']), ' ', ':');
325
        $rules = (isset($field['rules'])) ? $this->convertArray(trim($field['rules']), ' ', '|') : '';
326
        $tags = !empty($field['tags']) ? $this->convertArray($field['tags'], ' ', ',') : '';
327
328
        $string = "{$name};{$schema};{$rules};{$tags}";
329
330
        if(isset($field['factory']) && !empty($field['factory'])){
331
            $string .= ';' . $field['factory'];
332
        }
333
334
        return $string;
335
    }
336
337
    protected function convertArray($list, $old, $new)
338
    {
339
        return implode($new, array_filter(explode($old, $list), function($item){
340
            return !empty($item);
341
        }));
342
    }
343
344
    private function sortDependencies($nodes) {
345
        $load_order = array();
346
        $seen       = array();
347
348
        foreach($nodes as $key => $item) {
349
            $tmp = $this->getDependencies($nodes, $key, $seen);
350
351
            // if($tmp[2] === false) {
352
            $load_order = array_merge($load_order, $tmp[0]);
353
            $seen       = $tmp[1];
354
            // }
355
        }
356
357
        return $load_order;
358
    }
359
360
    private function getDependencies($nodes, $key, $seen = array()) {
361
        if(array_key_exists($key, $seen) === true) {
362
            return array(array(), $seen);
363
        }
364
365
366
        if(!empty($nodes[$key])) {
367
            $order = array();
368
            // $failed         = array();
369
370
            if($nodes[$key]['belongsTo']) {
371
                $deps = $this->getArgumentParser('relations')->parse($nodes[$key]['belongsTo']);
372
                foreach($deps as $dependency) {
373
                    if(! $dependency['model']){
374
	                    $dependency['model'] = $dependency['name'];
375
	                } else if(strpos($dependency['model'], '\\') !== false ){
376
	                    $dependency['model'] = substr($dependency['model'], strpos($dependency['model'], '\\')+1); // Cut offs first namespace part
377
	                }
378
                    $dependency['model'] = studly_case(str_singular($dependency['model']));
379
                    if ($dependency['model'] != $key) {
380
                        $tmp = $this->getDependencies($nodes, $dependency['model'], $seen);
381
382
                        $order  = array_merge($order, $tmp[0]);
383
                        $seen   = $tmp[1];
384
                    }
385
386
                    // if($tmp[2] !== false) {
387
                    //     $failed = array_merge($tmp[2], $failed);
388
                    // }
389
                }
390
            }
391
            $seen[$key]  = true;
392
            $order[$key] = $nodes[$key];
393
            // $failed     = (count($failed) > 0) ? $failed : false;
394
395
            return array($order, $seen);//, $failed
396
        }
397
398
        return array(array(), $seen);//, array($item)
399
    }
400
401
    protected function checkError($message, $model = "", $file = "") {
402
        $this->checkErrors[] = array("message" => $message, "model" => $model, "file" => $file);
403
        $this->checkedErrors++;
404
    }
405
406
    protected function checkInfo($message, $model = "", $file = "") {
407
        $this->checkInfo[] = array("message" => $message, "model" => $model, "file" => $file);
408
    }
409
410
    protected function printErrors() {
411
        foreach ($this->checkErrors as $error) {
412
            $this->error($error['message']);
413
        }
414
        foreach ($this->checkInfo as $info) {
415
            $this->info($info['message']);
416
        }
417
    }
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
            }
434
        }
435
    }
436
437
    protected function checkPivotRelations($nodes, $relations, $relationType) {
438
        if ($relations) {
439
            foreach($relations as $relation) {
440
                $relation['0'] = studly_case(str_singular($relation['0']));
441
                $relation['1'] = studly_case(str_singular($relation['1']));
442
                $relation['2'] = studly_case(str_singular($relation['2']));
443
444
                $this->checkRelation($nodes, $relationType, $relation['0'], $relation['2']);
445
                if ($relationType == "pivot") {
446
                    $this->checkRelation($nodes, $relationType, $relation['1'], $relation['2']);
447
                }
448
            }
449
        }
450
    }
451
452
    protected function checkRelation($nodes, $relationType, $relation, $model) {
453
        if (empty($nodes[$relation]) && !class_exists($this->getNamespace() . '\\' . $relation) && !class_exists('App\\' . $relation)) {
454
            $this->checkError($relation . ": undefined (used in " . $relationType . "-based relationship of model " . $model . " in file " . $nodes[$model]['filename'] . ")");
455
        } else if (class_exists($this->getNamespace() . '\\' . ucwords(camel_case($relation)))) {
456
            $this->checkInfo(studly_case(str_singular($relation)) . ": already defined in Namespace " . $this->getNamespace() . " (used in " . $relationType . "-based relationship of model " . $model . " in file " . $nodes[$model]['filename'] . ")");
457
        } else if (class_exists('App\\' . ucwords(camel_case($relation)))) {
458
            $this->checkInfo(studly_case(str_singular($relation)) . ": already defined in Namespace App\\ (used in " . $relationType . "-based relationship of model " . $model . " in file " . $nodes[$model]['filename'] . ")");
459
        }
460
    }
461
462
}
463