TsScaffoldTask   F
last analyzed

Complexity

Total Complexity 92

Size/Duplication

Total Lines 516
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 289
c 2
b 0
f 0
dl 0
loc 516
ccs 0
cts 301
cp 0
rs 2
wmc 92

21 Methods

Rating   Name   Duplication   Size   Complexity  
A generateExportsAction() 0 22 3
A getDefinitionsAction() 0 36 1
C runAction() 0 80 12
A createInterfaceOutput() 0 6 1
A getRelatedImportItems() 0 9 3
A createAbstractOutput() 0 10 1
A createServiceOutput() 0 11 1
A getRelatedDefaultItems() 0 9 3
A createModelOutput() 0 16 2
A getRelatedMapItems() 0 9 3
A appendExport() 0 6 1
A getTableName() 0 6 1
A saveFile() 0 15 5
A getRelatedMeta() 0 38 5
A getPropertyItems() 0 10 2
A getModelNameFromClassName() 0 15 1
A getColumnName() 0 6 1
A getModelInstance() 0 8 2
A getRelatedProperties() 0 13 3
D getColumnTsType() 0 52 27
C getDefaultValue() 0 40 14

How to fix   Complexity   

Complex Class

Complex classes like TsScaffoldTask 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.

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 TsScaffoldTask, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file is part of the Zemit Framework.
5
 *
6
 * (c) Zemit Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zemit\Modules\Cli\Tasks;
13
14
use Phalcon\Db\Column;
15
use Phalcon\Db\ColumnInterface;
16
use Phalcon\Mvc\Model\Relation;
17
use Zemit\Modules\Cli\Task;
18
use Zemit\Mvc\Model;
19
use Zemit\Support\Helper;
20
use Zemit\Support\Slug;
21
22
class TsScaffoldTask extends Task
23
{
24
    
25
    public string $cliDoc = <<<DOC
26
Usage:
27
  zemit cli ts-scaffold <action> [<params>...] [--force] [--table=<table>] [--directory=<directory>]
28
29
Options:
30
  --force                       Overwrite existing files
31
  --path=<directory>            Directory path to generate new files
32
  --table=<table>               Comma seperated list of table to generate
33
34
35
DOC;
36
    
37
    public string $path = '../sdk/src/';
38
    public string $servicesPath = 'services/';
39
    public string $modelsPath = 'models/';
40
    public string $abstractsPath = 'abstracts/';
41
    public string $interfacesPath = 'interfaces/';
42
    
43
    public function getDefinitionsAction(string $name): array
44
    {
45
        $definitions = [];
46
        
47
        $definitions['table'] = $this->getTableName($name);
48
        $definitions['slug'] = Slug::generate(Helper::uncamelize($name));
49
        
50
        // backend model
51
        $definitions['backend'] = 'Zemit\\Models\\' . $definitions['table'];
52
        
53
        // model
54
        $definitions['model']['name'] = $definitions['table'] . 'Model';
55
        $definitions['model']['file'] = $definitions['model']['name'] . '.ts';
56
        $definitions['model']['export'] = trim($this->modelsPath, '/') . '.ts';
57
        $definitions['model']['path'] = $this->modelsPath;
58
        
59
        // service
60
        $definitions['service']['name'] = $definitions['table'] . 'Service';
61
        $definitions['service']['file'] = $definitions['service']['name'] . '.ts';
62
        $definitions['service']['export'] = trim($this->servicesPath, '/') . '.ts';
63
        $definitions['service']['path'] = $this->servicesPath;
64
        
65
        // service
66
        $definitions['interface']['name'] = $definitions['table'] . 'ModelInterface';
67
        $definitions['interface']['file'] = $definitions['interface']['name'] . '.ts';
68
        $definitions['interface']['export'] = trim($this->interfacesPath, '/') . '.ts';
69
        $definitions['interface']['path'] = $this->interfacesPath;
70
        
71
        // abstract
72
        $definitions['abstract']['name'] = $definitions['table'] . 'ModelAbstract';
73
        $definitions['abstract']['file'] = $definitions['abstract']['name'] . '.ts';
74
        $definitions['abstract']['export'] = trim($this->abstractsPath, '/') . '.ts';
75
        $definitions['abstract']['path'] = $this->abstractsPath;
76
        
77
        
78
        return $definitions;
79
    }
80
    
81
    public function generateExportsAction(): array
82
    {
83
        $ret = [];
84
        
85
        $directory = $this->dispatcher->getParam('directory');
86
        $files = glob($directory . '*.ts');
87
        
88
        $exports = [];
89
        foreach ($files as $file) {
90
            $fileName = pathinfo($file, PATHINFO_FILENAME);
91
            if ($fileName !== 'index') {
92
                $exports [] = "export {{$fileName}} from './{$fileName}'";
93
            }
94
        }
95
        
96
        $interfacesExportPath = $directory . 'index.ts';
97
        
98
        $ret ['exports'] = $exports;
99
        $ret ['filePath'] = $interfacesExportPath;
100
        $ret ['saved'] = $this->saveFile($interfacesExportPath, implode("\n", $exports));
101
        
102
        return $ret;
103
    }
104
    
105
    public function runAction(): array
106
    {
107
        $ret = [];
108
        
109
        $exports = [
110
            'models' => [],
111
            'services' => [],
112
            'interfaces' => [],
113
            'abstracts' => [],
114
        ];
115
        
116
        $force = $this->dispatcher->getParam('force') ?? false;
117
        $whitelisted = array_filter(explode(',', $this->dispatcher->getParam('table') ?? ''));
118
        $tables = $this->db->listTables();
119
        foreach ($tables as $table) {
120
            if (!empty($whitelisted) && !in_array($table, $whitelisted)) {
121
                continue;
122
            }
123
            
124
            $columns = $this->db->describeColumns($table);
125
            $definitions = $this->getDefinitionsAction($table);
126
            $related = $this->getRelatedMeta($definitions['backend']);
127
            
128
            // Save Interface File
129
            $savePath = $this->path . $this->modelsPath . $this->abstractsPath . $this->interfacesPath . $definitions['interface']['file'];
130
            if (!file_exists($savePath) || $force) {
131
                $columns = $this->db->describeColumns($table);
132
                $this->saveFile($savePath, $this->createInterfaceOutput($definitions, $columns), $force);
133
                $ret [] = 'Interface `' . $definitions['interface']['file'] . '` created';
134
            }
135
            
136
            // Abstract
137
            $savePath = $this->path . $this->modelsPath . $this->abstractsPath . $definitions['abstract']['file'];
138
            if (!file_exists($savePath) || $force) {
139
                $this->saveFile($savePath, $this->createAbstractOutput($definitions, $columns), $force);
140
                $ret [] = 'Abstract `' . $definitions['abstract']['file'] . '` created';
141
            }
142
            
143
            // Model
144
            $savePath = $this->path . $this->modelsPath . $definitions['model']['file'];
145
            if (!file_exists($savePath) || $force) {
146
                $this->saveFile($savePath, $this->createModelOutput($definitions, $related), $force);
147
                $ret [] = 'Model ' . $definitions['model']['file'] . ' created';
148
            }
149
            
150
            // Create Service
151
            $savePath = $this->path . $this->servicesPath . $definitions['service']['file'];
152
            if (!file_exists($savePath) || $force) {
153
                $this->saveFile($savePath, $this->createServiceOutput($definitions), $force);
154
                $ret [] = 'Service `' . $definitions['service']['file'] . '` created';
155
            }
156
            
157
            $this->appendExport($definitions, $exports);
158
        }
159
        
160
        // interfaces
161
        $exportDirectory = $this->path . $this->modelsPath . $this->abstractsPath . $this->interfacesPath;
162
        $this->dispatcher->setParam('directory', $exportDirectory);
163
        $this->generateExportsAction();
164
        $ret [] = 'Interfaces Export `index.ts` created';
165
        
166
        // abstracts
167
        $exportDirectory = $this->path . $this->modelsPath . $this->abstractsPath;
168
        $this->dispatcher->setParam('directory', $exportDirectory);
169
        $this->generateExportsAction();
170
        $ret [] = 'Abstracts Export `index.ts` created';
171
        
172
        // models
173
        $exportDirectory = $this->path . $this->modelsPath;
174
        $this->dispatcher->setParam('directory', $exportDirectory);
175
        $this->generateExportsAction();
176
        $ret [] = 'Models Export `index.ts` created';
177
        
178
        // services
179
        $exportDirectory = $this->path . $this->servicesPath;
180
        $this->dispatcher->setParam('directory', $exportDirectory);
181
        $this->generateExportsAction();
182
        $ret [] = 'Services Export `index.ts` created';
183
        
184
        return $ret;
185
    }
186
    
187
    public function appendExport(array $definitions, array &$exports)
188
    {
189
        $exports['models'] [] = "export {{$definitions['model']['name']}} from './{$definitions['model']['name']}'";
190
        $exports['interfaces'] [] = "export {{$definitions['interface']['name']}} from './{$definitions['interface']['name']}'";
191
        $exports['services'] [] = "export {{$definitions['service']['name']}} from './{$definitions['service']['name']}'";
192
        $exports['abstracts'] [] = "export {{$definitions['abstract']['name']}} from './{$definitions['abstract']['name']}'";
193
    }
194
    
195
    public function createInterfaceOutput(array $definitions, array $columns): string
196
    {
197
        $propertyItems = str_replace('!: ', ': ', $this->getPropertyItems($columns));
198
        return <<<EOT
199
export interface {$definitions['interface']['name']} {
200
{$propertyItems}
201
}
202
EOT;
203
    }
204
    
205
    /**
206
     * Creates a typescript abstract model output based on the given definitions.
207
     */
208
    public function createAbstractOutput(array $definitions, array $columns): string
209
    {
210
        $from = './' . $this->interfacesPath . $definitions['interface']['name'];
211
        $propertyItems = $this->getPropertyItems($columns);
212
        return <<<EOT
213
import { AbstractModel } from '../AbstractModel';
214
import { {$definitions['interface']['name']} } from '$from';
215
216
export class {$definitions['abstract']['name']} extends AbstractModel implements {$definitions['interface']['name']} {
217
{$propertyItems}
218
}
219
EOT;
220
    }
221
    
222
    /**
223
     * Creates a typescript model output based on the given definitions.
224
     */
225
    public function createModelOutput(array $definitions, array $related): string
226
    {
227
        $from = './' . $this->abstractsPath . $definitions['abstract']['name'];
228
        $relatedImportItems = $this->getRelatedImportItems($related);
229
//        $relatedDefaultItems = $this->getRelatedDefaultItems($related);
230
        $relatedPropertyItems = $this->getRelatedProperties($related);
231
        $importTypeClassTransformer = !empty($relatedPropertyItems) ?
232
            "import { Type } from 'class-transformer';" : '';
233
        return <<<EOT
234
import 'reflect-metadata';
235
{$importTypeClassTransformer}
236
import { {$definitions['abstract']['name']} } from '$from';
237
{$relatedImportItems}
238
239
export class {$definitions['model']['name']} extends {$definitions['abstract']['name']} {
240
{$relatedPropertyItems}
241
}
242
EOT;
243
    }
244
    
245
    /**
246
     *
247
     */
248
    public function createServiceOutput(array $definitions)
249
    {
250
        
251
        $from = '../' . $this->modelsPath . $definitions['model']['name'];
252
        return <<<EOT
253
import { AbstractService } from './AbstractService';
254
import { {$definitions['model']['name']} } from '$from';
255
256
export class {$definitions['service']['name']} extends AbstractService {
257
    modelUrl = '{$definitions['slug']}';
258
    model = {$definitions['model']['name']};
259
}
260
EOT;
261
    }
262
    
263
    public function getRelatedImportItems(array $related): string
264
    {
265
        $relatedImportItems = [];
266
        if (!empty($related['import'])) {
267
            foreach ($related['import'] as $key => $value) {
268
                $relatedImportItems [] = 'import { ' . $key . ' } from \'./' . $key . '\';';
269
            }
270
        }
271
        return implode("\n", $relatedImportItems);
272
    }
273
    
274
    /**
275
     * Returns a formatted string representation of the related default items.
276
     */
277
    public function getRelatedDefaultItems(array $related): string
278
    {
279
        $relatedMapItems = [];
280
        if (!empty($related['default'])) {
281
            foreach ($related['default'] as $key => $value) {
282
                $relatedMapItems [] = '  ' . $key . ' = ' . $value . ',';
283
            }
284
        }
285
        return implode("\n", $relatedMapItems);
286
    }
287
    
288
    /**
289
     * Returns a formatted string representation of the related map items.
290
     */
291
    public function getRelatedMapItems(array $related): string
292
    {
293
        $relatedMapItems = [];
294
        if (!empty($related['map'])) {
295
            foreach ($related['map'] as $key => $value) {
296
                $relatedMapItems [] = '  ' . $key . '!: ' . $value . ';';
297
            }
298
        }
299
        return implode("\n", $relatedMapItems);
300
    }
301
    
302
    /**
303
     * Returns a formatted string representation of the related map items.
304
     */
305
    public function getRelatedProperties(array $related): string
306
    {
307
        $relatedMapItems = [];
308
        if (!empty($related['data'])) {
309
            foreach ($related['data'] as $key => $value) {
310
                $type = str_replace('[]', '', $value);
311
                assert(is_string($type));
312
                $relatedMapItems [] = '';
313
                $relatedMapItems [] = '  @Type(() => ' . $type . ')';
314
                $relatedMapItems [] = '  ' . $key . '!: ' . $value . ';';
315
            }
316
        }
317
        return implode("\n", $relatedMapItems);
318
    }
319
    
320
    /**
321
     * Returns a formatted string representation of the property items.
322
     *
323
     * @param ColumnInterface[] $columns An array of column objects.
324
     * @return string A string representation of the property items.
325
     */
326
    public function getPropertyItems(array $columns): string
327
    {
328
        $propertyItems = [];
329
        foreach ($columns as $column) {
330
            $columnName = $this->getColumnName($column->getName());
331
            $columnType = $this->getColumnTsType($column);
332
//            $defaultValue = $this->getDefaultValue($column, $columnType);
333
            $propertyItems[] = "  $columnName!: $columnType;";
334
        }
335
        return implode("\n", $propertyItems);
336
    }
337
    
338
    public function getRelatedMeta(string $modelClassName): array
339
    {
340
        $related = [
341
            'import' => [],
342
            'map' => [],
343
            'default' => [],
344
            'data' => [],
345
        ];
346
        
347
        $modelInstance = $this->getModelInstance($modelClassName);
348
        $modelManager = $modelInstance->getModelsManager();
349
        $relations = $modelManager->getRelations($modelClassName);
350
        foreach ($relations as $relation) {
351
            $relationAlias = $relation->getOption('alias');
352
            $relationModelName = $this->getModelNameFromClassName($relation->getReferencedModel());
353
            // do not import itself
354
            $modelName = basename(str_replace('\\', '/', $modelClassName)) . 'Model';
355
            if ($relationModelName !== $modelName) {
356
                // import related model
357
                $related['import'][$relationModelName] = '';
358
            }
359
            // add related map entry
360
            $related['map'][$relationAlias] = $relationModelName;
361
            
362
            if (
363
                $relation->getType() === Relation::HAS_MANY ||
364
                $relation->getType() === Relation::HAS_MANY_THROUGH
365
            ) {
366
                // set default value to empty array
367
                $related['default'][$relationAlias] = '[]';
368
                $related['data'][$relationAlias] = $relationModelName . '[]';
369
            }
370
            else {
371
                $related['data'][$relationAlias] = $relationModelName;
372
            }
373
        }
374
        
375
        return $related;
376
    }
377
    
378
    public function getColumnTsType(ColumnInterface $column): string
379
    {
380
        $tsType = 'null';
381
        
382
        if ($column->isNumeric()) {
383
            $tsType = 'number';
384
        }
385
        
386
        switch ($column->getType()) {
387
            case Column::TYPE_TIMESTAMP:
388
            case Column::TYPE_BIGINTEGER:
389
            case Column::TYPE_MEDIUMINTEGER:
390
            case Column::TYPE_SMALLINTEGER:
391
//            case Column::TYPE_TINYINTEGER: // conflict with TYPE_BINARY
392
            case Column::TYPE_INTEGER:
393
            case Column::TYPE_DECIMAL:
394
            case Column::TYPE_DOUBLE:
395
            case Column::TYPE_FLOAT:
396
            case Column::TYPE_BIT:
397
                $tsType = 'number';
398
                break;
399
            
400
            case Column::TYPE_ENUM:
401
            case Column::TYPE_VARCHAR:
402
            case Column::TYPE_CHAR:
403
            case Column::TYPE_TEXT:
404
            case Column::TYPE_TINYTEXT:
405
            case Column::TYPE_MEDIUMTEXT:
406
            case Column::TYPE_BLOB:
407
            case Column::TYPE_TINYBLOB:
408
            case Column::TYPE_LONGBLOB:
409
            case Column::TYPE_DATETIME:
410
            case Column::TYPE_DATE:
411
            case Column::TYPE_TIME:
412
            case Column::TYPE_BINARY:
413
                $tsType = 'string';
414
                break;
415
            
416
            case Column::TYPE_JSON:
417
            case Column::TYPE_JSONB:
418
                $tsType = 'object';
419
                break;
420
            
421
            case Column::TYPE_BOOLEAN:
422
                $tsType = 'boolean';
423
                break;
424
            
425
            default:
426
                break;
427
        }
428
        
429
        return $tsType;
430
    }
431
    
432
    public function getDefaultValue(ColumnInterface $column, string $type): ?string
433
    {
434
        $default = null;
435
        $columnDefault = $column->getDefault();
436
        switch (getType(strtolower($columnDefault ?? ''))) {
437
            case 'boolean':
438
            case 'integer':
439
            case 'double':
440
            case 'null':
441
                $default = $columnDefault;
442
                break;
443
            
444
            case 'string':
445
                if ($type === 'string') {
446
                    $default = !empty($columnDefault) ? '"' . addslashes($columnDefault) . '"' : '';
447
                }
448
                if ($type === 'number') {
449
                    $default = !empty($columnDefault) ? $columnDefault : '';
450
                }
451
                if ($type === 'array') {
452
                    $default = '[]';
453
                }
454
                if ($type === 'object') {
455
                    $default = '{}';
456
                }
457
                break;
458
            
459
            case 'array':
460
                $default = '[]';
461
                break;
462
            
463
            case 'object':
464
                $default = '{}';
465
                break;
466
            
467
            default:
468
                break;
469
        }
470
        
471
        return $default;
472
    }
473
    
474
    public function getColumnName(string $name)
475
    {
476
        return lcfirst(
477
            Helper::camelize(
478
                Helper::uncamelize(
479
                    $name
480
                )
481
            )
482
        );
483
    }
484
    
485
    public function getTableName(string $name)
486
    {
487
        return ucfirst(
488
            Helper::camelize(
489
                Helper::uncamelize(
490
                    $name
491
                )
492
            )
493
        );
494
    }
495
    
496
    public function getModelInstance(string $modelClassName): Model
497
    {
498
        if (class_exists($modelClassName)) {
499
            $modelInstance = new $modelClassName();
500
            assert($modelInstance instanceof Model);
501
            return $modelInstance;
502
        }
503
        return new Model();
504
    }
505
    
506
    public function getModelNameFromClassName(string $className)
507
    {
508
        return ucfirst(
509
            Helper::camelize(
510
                Helper::uncamelize(
511
                    basename(
512
                        str_replace(
513
                            '\\',
514
                            '/',
515
                            $className
516
                        )
517
                    )
518
                )
519
            )
520
        ) . 'Model';
521
    }
522
    
523
    public function saveFile(string $file, string $text, bool $force = false): bool
524
    {
525
        if (!$force && file_exists($file)) {
526
            return false;
527
        }
528
        
529
        $directory = dirname($file);
530
        
531
        // Create the directory if it doesn't exist
532
        if (!is_dir($directory)) {
533
            mkdir($directory, 0755, true);
534
        }
535
        
536
        $file = fopen($file, 'w');
537
        return fwrite($file, $text) && fclose($file);
538
    }
539
}
540