ScaffoldTask::getPropertyDefinitions()   F
last analyzed

Complexity

Conditions 12
Paths 480

Size

Total Lines 40
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 0
Metric Value
cc 12
eloc 27
nc 480
nop 1
dl 0
loc 40
ccs 0
cts 28
cp 0
crap 156
rs 3.5222
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Zemit\Modules\Cli\Task;
17
use Zemit\Modules\Cli\Tasks\Traits\DescribesTrait;
18
use Zemit\Modules\Cli\Tasks\Traits\ScaffoldTrait;
19
use Zemit\Support\Helper;
20
use Zemit\Support\Slug;
21
22
class ScaffoldTask extends Task
23
{
24
    use ScaffoldTrait;
25
    use DescribesTrait;
26
    
27
    public string $cliDoc = <<<DOC
28
Usage:
29
  zemit cli scaffold <action> [--force] [--directory=<directory>] [--namespace=<namespace>]
30
                              [--table=<table>] [--exclude=<exclude>] [--license=<license>]
31
                              [--src-dir=<controllers-dir>] [--controllers-dir=<controllers-dir>]
32
                              [--interfaces-dir=<interfaces-dir>] [--abstracts-dir=<abstracts-dir>]
33
                              [--models-dir=<models-dir>] [--enums-dir=<enums-dir>] [--tests-dir=<tests-dir>]
34
                              [--models-extend=<models-extend>] [--interfaces-extend=<interface-extend>]
35
                              [--controllers-extend=<controllers-extend>] [--tests-extend=<tests-extend>]
36
                              [--no-controllers] [--no-interfaces] [--no-abstracts] [--no-models] [--no-enums] [--no-tests]
37
                              [--no-strict-types] [--no-license] [--no-comments] [--no-get-set-methods]
38
                              [--no-validations] [--no-relationships] [--no-column-map] [--no-set-source]
39
                              [--no-typings] [--granular-typings] [--add-raw-value-type] [--protected-properties]
40
41
Actions:
42
  models
43
  abstracts
44
  controllers
45
  enums
46
47
Options:
48
  --force                                     Overwrite existing files
49
  --table=<table>                             Comma seperated list of table to generate
50
  --exclude=<table>                           Comma seperated list of table to exclude
51
  --namespace=<namespace>                     Root namespace of the project (Default to "App")
52
  --license=<license>                         Set your own license stamp (PHP Comment)
53
54
  --directory=<directory>                     Root directory path to generate new files (Default to "./")
55
  --src-dir=<src-dir>                         Source directory path to generate new files (Default to "src/")
56
  --controllers-dir=<controllers-dir>         Set your own controllers directory (Default: "Controllers")
57
  --interfaces-dir=<interfaces-dir>           Set your own interfaces directory (Default: "Interfaces")
58
  --abstracts-dir=<abstracts-dir>             Set your own abstract directory (Default: "Abstracts")
59
  --models-dir=<models-dir>                   Set your own models directory (Default: "Models")
60
  --enums-dir=<enums-dir>                     Set your own enums directory (Default: "Enums")
61
  --tests-dir=<tests-dir>                     Set your own tests directory (Default: "Tests")
62
63
  --models-extend=<models-extend>             Extend models with this base class (Default: "\Zemit\Models\ModelAbstract")
64
  --interfaces-extend=<interface-extend>      Extend models interfaces with this base interface (Default: "\Zemit\Models\ModelInterface")
65
  --controllers-extend=<controllers-extends>  Extend controllers with this base class (Default: "Zemit\Mvc\Controller\Rest")
66
  --tests-extend=<tests-extends>              Extend tests with this base class (Default: "Zemit\Tests\Unit\AbstractUnit")
67
68
  --no-controllers                            Do not generate controllers
69
  --no-interfaces                             Do not generate interfaces
70
  --no-abstracts                              Do not generate abstracts
71
  --no-models                                 Do not generate models
72
  --no-enums                                  Do not generate enums
73
  --no-tests                                  Do not generate tests
74
75
  --no-strict-types                           Do not generate declare(strict_types=1);
76
  --no-license                                Do not generate license stamps
77
  --no-comments                               Do not generate comments
78
79
  --no-get-set-methods                        Do not generate getter and setter methods in models
80
  --no-validations                            Do not generate default validations in models
81
  --no-relationships                          Do not generate default relationships in models
82
  --no-column-map                             Do not generate column map in models
83
  --no-set-source                             Do not call setSource() in models
84
  --no-typings                                Do not generate typings for properties in models
85
  --granular-typings                          Force the properties to `mixed` in models
86
  --add-raw-value-type                        Add the `RawValue` type to every property in models
87
  --protected-properties                      Make the properties `protected` in models
88
DOC;
89
    
90
    public function getDefinitionsAction(string $name): array
91
    {
92
        $definitions = [];
93
        
94
        $tableName = $this->getTableName($name);
95
        $definitions['table'] = $tableName;
96
        $definitions['slug'] = Slug::generate(Helper::uncamelize($name));
97
        
98
        // enums
99
        $definitions['enums']['name'] = $tableName;
100
        $definitions['enums']['file'] = $definitions['enums']['name'] . '.php';
101
        
102
        // controllers
103
        $definitions['controller']['name'] = $tableName . 'Controller';
104
        $definitions['controller']['file'] = $definitions['controller']['name'] . '.php';
105
        
106
        // controllers interfaces
107
        $definitions['controllerInterface']['name'] = $definitions['controller']['name'] . 'Interface';
108
        $definitions['controllerInterface']['file'] = $definitions['controllerInterface']['name'] . '.php';
109
        
110
        // abstracts
111
        $definitions['abstract']['name'] = $tableName . 'Abstract';
112
        $definitions['abstract']['file'] = $definitions['abstract']['name'] . '.php';
113
        
114
        // abstracts interfaces
115
        $definitions['abstractInterface']['name'] = $definitions['abstract']['name'] . 'Interface';
116
        $definitions['abstractInterface']['file'] = $definitions['abstractInterface']['name'] . '.php';
117
        
118
        // models
119
        $definitions['model']['name'] = $tableName;
120
        $definitions['model']['file'] = $definitions['model']['name'] . '.php';
121
        
122
        // models interfaces
123
        $definitions['modelInterface']['name'] = $definitions['model']['name'] . 'Interface';
124
        $definitions['modelInterface']['file'] = $definitions['modelInterface']['name'] . '.php';
125
        
126
        // models tests
127
        $definitions['modelTest']['name'] = $definitions['model']['name'] . 'Test';
128
        $definitions['modelTest']['file'] = $definitions['modelTest']['name'] . '.php';
129
        
130
        return $definitions;
131
    }
132
    
133
    public function runAction(): array
134
    {
135
        $ret = [];
136
        
137
        $force = $this->dispatcher->getParam('force') ?? false;
138
        
139
        $tables = $this->db->listTables();
140
        foreach ($tables as $table) {
141
            
142
            // filter excluded tables
143
            if ($this->isExcludedTable($table)) {
144
                continue;
145
            }
146
            
147
            // filter whitelisted tables
148
            if (!$this->isWhitelistedTable($table)) {
149
                continue;
150
            }
151
            
152
            $columns = $this->describeColumns($table);
153
            $definitions = $this->getDefinitionsAction($table);
154
            $relationships = $this->getRelationshipItems($table, $columns, $tables);
155
            
156
            // Controller
157
//            $savePath = $this->getAbstractsDirectory($definitions['controller']['file']);
158
//            if (!file_exists($savePath) || $force) {
159
//                $this->saveFile($savePath, $this->createControllerOutput($definitions, $columns, $relationships), $force);
160
//                $ret [] = 'Controller API `' . $definitions['controller']['file'] . '` created';
161
//            }
162
            
163
            // Abstract
164
            $savePath = $this->getAbstractsDirectory($definitions['abstract']['file']);
165
            if (!file_exists($savePath) || $force) {
166
                $this->saveFile($savePath, $this->createAbstractOutput($definitions, $columns, $relationships), $force);
167
                $ret [] = 'Abstract Model `' . $definitions['abstract']['file'] . '` created at `' . $savePath . '`';
168
            }
169
            
170
            // Abstract Interfaces
171
            $savePath = $this->getAbstractsInterfacesDirectory($definitions['abstractInterface']['file']);
172
            if (!file_exists($savePath) || $force) {
173
                $this->saveFile($savePath, $this->createAbstractInterfaceOutput($definitions, $columns, $relationships), $force);
174
                $ret [] = 'Abstract Model Interface `' . $definitions['abstractInterface']['file'] . '` created at `' . $savePath . '`';
175
            }
176
            
177
            // Model Enums
178
            foreach ($columns as $column) {
179
                assert($column instanceof Column);
180
                if ($column->getType() === Column::TYPE_ENUM) {
181
                    $enumValues = $column->getSize();
0 ignored issues
show
Unused Code introduced by
The assignment to $enumValues is dead and can be removed.
Loading history...
182
                    
183
                    $enumName = $definitions['enums']['name'] . Helper::camelize($column->getName());
184
                    $definitionsEnumFile = $enumName . '.php';
185
                    $savePath = $this->getEnumsDirectory($definitionsEnumFile);
186
                    if (!file_exists($savePath) || $force) {
187
                        $this->saveFile($savePath, $this->createEnumOutput($enumName, $column), $force);
188
                        $ret [] = 'Enum `' . $definitionsEnumFile . '` created at `' . $savePath . '`';
189
                    }
190
                }
191
            }
192
            
193
            // Model
194
            $savePath = $this->getModelsDirectory($definitions['model']['file']);
195
            if (!file_exists($savePath) || $force) {
196
                $this->saveFile($savePath, $this->createModelOutput($definitions), $force);
197
                $ret [] = 'Model `' . $definitions['model']['file'] . '` created at `' . $savePath . '`';
198
            }
199
            
200
            // Model Interfaces
201
            $savePath = $this->getModelsInterfacesDirectory($definitions['modelInterface']['file']);
202
            if (!file_exists($savePath) || $force) {
203
                $this->saveFile($savePath, $this->createModelInterfaceOutput($definitions), $force);
204
                $ret [] = 'Model Interface `' . $definitions['modelInterface']['file'] . '` created at `' . $savePath . '`';
205
            }
206
            
207
            // Model Test
208
            $savePath = $this->getModelsTestsDirectory($definitions['modelTest']['file']);
209
            if (!file_exists($savePath) || $force) {
210
                $this->saveFile($savePath, $this->createModelTestOutput($definitions, $columns), $force);
211
                $ret [] = 'Model Test `' . $definitions['modelTest']['file'] . '` created at `' . $savePath . '`';
212
            }
213
        }
214
        
215
        return $ret;
216
    }
217
    
218
    public function createControllerOutput(array $definitions, array $columns, array $relationships): string
0 ignored issues
show
Unused Code introduced by
The parameter $columns is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

218
    public function createControllerOutput(array $definitions, /** @scrutinizer ignore-unused */ array $columns, array $relationships): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
219
    {
220
        return <<<PHP
221
<?php
222
{$this->getLicenseStamp()}
223
{$this->getStrictTypes()}
224
namespace {$this->getAbstractsInterfacesNamespace()};
225
226
use Phalcon\Db\RawValue;
227
use Zemit\Mvc\ModelInterface;
228
229
/**
230
{$relationships['interfaceInjectableItems']}
231
 */
232
interface {$definitions['controller']['name']} extends AbstractController
233
{
234
}
235
236
PHP;
237
    }
238
    
239
    /**
240
     * Generates the output for a model interface.
241
     *
242
     * @param array $definitions The definitions for generating the model interface.
243
     *
244
     * @return string The generated model interface output as a string.
245
     */
246
    public function createModelInterfaceOutput(array $definitions): string
247
    {
248
        return <<<PHP
249
<?php
250
{$this->getLicenseStamp()}
251
{$this->getStrictTypes()}
252
namespace {$this->getModelsInterfacesNamespace()};
253
254
use {$this->getAbstractsInterfacesNamespace()}\\{$definitions['abstractInterface']['name']};
255
256
interface {$definitions['modelInterface']['name']} extends {$definitions['abstractInterface']['name']}
257
{
258
}
259
260
PHP;
261
    }
262
    
263
    /**
264
     * Generates the abstract interface output based on the given definitions and columns.
265
     *
266
     * @param array $definitions The definitions for the abstract interface.
267
     * @param array $columns The columns for which to generate getter and setter methods.
268
     * @param array $relationships The columns for which to generate getter and setter methods.
269
     *
270
     * @return string The generated abstract interface output as a string.
271
     */
272
    public function createAbstractInterfaceOutput(array $definitions, array $columns, array $relationships): string
273
    {
274
        $getSetInterfaceItems = $this->getGetSetMethods($columns, 'interface');
275
        return <<<PHP
276
<?php
277
{$this->getLicenseStamp()}
278
{$this->getStrictTypes()}
279
namespace {$this->getAbstractsInterfacesNamespace()};
280
281
use Phalcon\Db\RawValue;
282
use Zemit\Mvc\ModelInterface;
283
284
/**
285
{$relationships['interfaceInjectableItems']}
286
 */
287
interface {$definitions['abstractInterface']['name']} extends ModelInterface
288
{
289
    {$getSetInterfaceItems}
290
}
291
292
PHP;
293
    }
294
    
295
    /**
296
     * Generates an abstract class output for the given definitions, table, columns, and tables.
297
     *
298
     * @param array $definitions The definitions for the abstract output.
299
     * @param array $columns The columns.
300
     * @param array $relationships The relationship items.
301
     *
302
     * @return string The abstract output as a string.
303
     */
304
    public function createAbstractOutput(array $definitions, array $columns, array $relationships): string
305
    {
306
        $propertyItems = $this->getPropertyItems($columns);
307
        $getSetMethods = $this->getGetSetMethods($columns);
308
        $columnMapMethod = $this->getColumnMapMethod($columns);
309
        $validationItems = $this->getValidationItems($columns);
310
        
311
        return <<<PHP
312
<?php
313
{$this->getLicenseStamp()}
314
{$this->getStrictTypes()}
315
namespace {$this->getAbstractsNamespace()};
316
317
use Phalcon\Db\RawValue;
318
use Zemit\Filter\Validation;
319
use Zemit\Models\AbstractModel;
320
{$relationships['useItems']}
321
use {$this->getAbstractsInterfacesNamespace()}\\{$definitions['abstractInterface']['name']};
322
323
/**
324
 * Class {$definitions['abstract']['name']}
325
 *
326
 * This class defines a {$definitions['model']['name']} abstract model that extends the AbstractModel class and implements the {$definitions['abstractInterface']['name']}.
327
 * It provides properties and methods for managing {$definitions['model']['name']} data.
328
 * 
329
{$relationships['injectableItems']}
330
 */
331
abstract class {$definitions['abstract']['name']} extends AbstractModel implements {$definitions['abstractInterface']['name']}
332
{
333
    {$propertyItems}
334
    
335
    {$getSetMethods}
336
337
    /**
338
     * Adds the default relationships to the model.
339
     * @return void
340
     */
341
    public function addDefaultRelationships(): void
342
    {
343
        {$relationships['items']}
344
    }
345
    
346
    /**
347
     * Adds the default validations to the model.
348
     * @param Validation|null \$validator
349
     * @return Validation
350
     */
351
    public function addDefaultValidations(?Validation \$validator = null): Validation
352
    {
353
        \$validator ??= new Validation();
354
    
355
        {$validationItems}
356
        
357
        return \$validator;
358
    }
359
360
    {$columnMapMethod}
361
}
362
363
PHP;
364
    }
365
    
366
    public function createEnumOutput(string $enumName, Column $column): string
367
    {
368
        $size = $column->getSize();
369
        $list = explode(',',str_replace('\'', '', $size));
370
        $enumValues = [];
371
        foreach ($list as $item) {
372
            $enumValues[] = '    case ' . Helper::upper(Helper::uncamelize($item)) . ' = \'' . $item . '\';';
373
        }
374
        $enumValues = implode(PHP_EOL, $enumValues);
375
        return <<<PHP
376
<?php
377
{$this->getLicenseStamp()}
378
{$this->getStrictTypes()}
379
namespace {$this->getEnumsNamespace()};
380
381
enum {$enumName}: string {
382
{$enumValues}
383
}
384
PHP;
385
386
    }
0 ignored issues
show
Coding Style introduced by
Function closing brace must go on the next line following the body; found 1 blank lines before brace
Loading history...
387
    
388
    /**
389
     * Generates a comment for the createModelOutput method.
390
     *
391
     * @param array $definitions The array of model definitions.
392
     * @return string The generated comment.
393
     */
394
    public function createModelOutput(array $definitions): string
395
    {
396
        return <<<PHP
397
<?php
398
{$this->getLicenseStamp()}
399
{$this->getStrictTypes()}
400
namespace {$this->getModelsNamespace()};
401
402
use {$this->getAbstractsNamespace()}\\{$definitions['abstract']['name']};
403
use {$this->getModelsInterfacesNamespace()}\\{$definitions['modelInterface']['name']};
404
{$this->getModelClassComments($definitions)}
405
class {$definitions['model']['name']} extends {$definitions['abstract']['name']} implements {$definitions['modelInterface']['name']}
406
{
407
    public function initialize(): void
408
    {
409
        parent::initialize();
410
        \$this->addDefaultRelationships();
411
    }
412
413
    public function validation(): bool
414
    {
415
        \$validator = \$this->genericValidation();
416
        \$this->addDefaultValidations(\$validator);
417
        return \$this->validate(\$validator);
418
    }
419
}
420
421
PHP;
422
    }
423
    
424
    public function createModelTestOutput(array $definitions, array $columns): string
425
    {
426
        $property = lcfirst($definitions['model']['name']);
427
        $getSetTestItems = $this->getGetSetMethods($columns, 'test', $property);
428
        return <<<PHP
429
<?php
430
{$this->getLicenseStamp()}
431
{$this->getStrictTypes()}
432
namespace {$this->getModelsTestsNamespace()};
433
434
use {$this->getAbstractsNamespace()}\\{$definitions['abstract']['name']};
435
use {$this->getAbstractsInterfacesNamespace()}\\{$definitions['abstractInterface']['name']};
436
use {$this->getModelsNamespace()}\\{$definitions['model']['name']};
437
use {$this->getModelsInterfacesNamespace()}\\{$definitions['modelInterface']['name']};
438
439
/**
440
 * Class {$definitions['modelTest']['name']}
441
 *
442
 * This class contains unit tests for the User class.
443
 */
444
class {$definitions['modelTest']['name']} extends \Zemit\Tests\Unit\AbstractUnit
445
{
446
    public {$definitions['modelInterface']['name']} \${$property};
447
    
448
    protected function setUp(): void
449
    {
450
        \$this->{$property} = new {$definitions['model']['name']}();
451
    }
452
    
453
    public function testInstanceOf(): void
454
    {
455
        // Model
456
        \$this->assertInstanceOf({$definitions['model']['name']}::class, \$this->{$property});
457
        \$this->assertInstanceOf({$definitions['modelInterface']['name']}::class, \$this->{$property});
458
    
459
        // Abstract
460
        \$this->assertInstanceOf({$definitions['abstract']['name']}::class, \$this->{$property});
461
        \$this->assertInstanceOf({$definitions['abstractInterface']['name']}::class, \$this->{$property});
462
        
463
        // Zemit
464
        \$this->assertInstanceOf(\Zemit\Mvc\ModelInterface::class, \$this->{$property});
465
        \$this->assertInstanceOf(\Zemit\Mvc\Model::class, \$this->{$property});
466
        
467
        // Phalcon
468
        \$this->assertInstanceOf(\Phalcon\Mvc\ModelInterface::class, \$this->{$property});
469
        \$this->assertInstanceOf(\Phalcon\Mvc\Model::class, \$this->{$property});
470
    }
471
    
472
    {$getSetTestItems}
473
    
474
    public function testGetColumnMapShouldBeAnArray(): void
475
    {
476
        \$this->assertIsArray(\$this->{$property}->getColumnMap());
477
    }
478
}
479
480
PHP;
481
    }
482
    
483
    public function getModelClassComments(array $definitions): string
484
    {
485
        if ($this->isNoComments()) {
486
            return '';
487
        }
488
        
489
        return <<<PHP
490
491
/**
492
 * Class {$definitions['model']['name']}
493
 *
494
 * This class represents a {$definitions['model']['name']} object.
495
 * It extends the {$definitions['abstract']['name']} class and implements the {$definitions['modelInterface']['name']}.
496
 */
497
PHP;
498
    }
499
    
500
    /**
501
     * Generates a string containing validation items for each column in the provided array.
502
     *
503
     * @param array $columns An array of ColumnInterface objects.
504
     *
505
     * @return string The generated validation items string.
506
     */
507
    public function getValidationItems(array $columns): string
508
    {
509
        if ($this->isNoValidations()) {
510
            return '';
511
        }
512
        
513
        $validationItems = [];
514
        foreach ($columns as $column) {
515
            assert($column instanceof ColumnInterface);
516
            $columnType = $column->getType();
517
            $columnName = $column->getName();
518
            
519
            $propertyType = $this->getColumnType($column);
520
            $propertyName = $this->getPropertyName($columnName);
521
            
522
            $minSize = 0;
523
            $maxSize = is_int($column->getSize())? $column->getSize() : 0;
524
            
525
            $allowEmpty = $column->isNotNull() && !$column->isAutoIncrement()? 'false' : 'true';
526
            
527
            if ($columnType === Column::TYPE_DATE) {
528
                $validationItems [] = <<<PHP
529
        \$this->addDateValidation(\$validator, '{$propertyName}', {$allowEmpty});
530
PHP;
531
            }
532
            
533
            if ($columnType === Column::TYPE_JSON) {
534
                $validationItems [] = <<<PHP
535
        \$this->addJsonValidation(\$validator, '{$propertyName}', {$allowEmpty});
536
PHP;
537
            }
538
            
539
            if ($columnType === Column::TYPE_DATETIME) {
540
                $validationItems [] = <<<PHP
541
        \$this->addDateTimeValidation(\$validator, '{$propertyName}', {$allowEmpty});
542
PHP;
543
            }
544
            
545
            if ($columnType === Column::TYPE_ENUM) {
546
                $enumValues = $column->getSize();
547
                $validationItems [] = <<<PHP
548
        \$this->addInclusionInValidation(\$validator, '{$propertyName}', [{$enumValues}], {$allowEmpty});
549
PHP;
550
            }
551
            
552
            // String
553
            if ($propertyType === 'string' && $maxSize) {
554
                $validationItems []= <<<PHP
555
        \$this->addStringLengthValidation(\$validator, '{$propertyName}', {$minSize}, {$maxSize}, {$allowEmpty});
556
PHP;
557
            }
558
            
559
            // Int
560
            if ($propertyType === 'int' && $column->isUnsigned()) {
561
                $validationItems []= <<<PHP
562
        \$this->addUnsignedIntValidation(\$validator, '{$propertyName}', {$allowEmpty});
563
PHP;
564
            }
565
        }
566
        return trim(implode("\n", $validationItems));
567
    }
568
    
569
    /**
570
     * Generates relationship items for a given table.
571
     *
572
     * @param string $table The name of the table.
573
     * @param array $columns The array of column objects.
574
     * @param array $tables The array of table names.
575
     *
576
     * @return array An array containing the generated relationship items.
577
     */
578
    public function getRelationshipItems(string $table, array $columns, array $tables): array
579
    {
580
        if ($this->isNoRelationships()) {
581
            return [
582
                ' *',
583
                '',
584
                ''
585
            ];
586
        }
587
        
588
        $modelNamespace = 'Zemit\\Models\\';
589
        
590
        $useModels = [];
591
        $relationshipUseItems = [];
592
        $relationshipItems = [];
593
        $relationshipInjectableItems = [];
594
        
595
        $interfaceInjectableItems = [];
596
        $interfaceUseItems = [];
597
        $useInterfaces = [];
598
            
599
        // Has Many
600
        foreach ($tables as $otherTable) {
601
            
602
            // skip the current table
603
            if ($otherTable === $table) {
604
                continue;
605
            }
606
            
607
            $otherTableColumns = $this->describeColumns($otherTable);
608
            $relationName = $this->getTableName($otherTable);
609
            $relationClass = $relationName . '::class';
610
            $relationAlias = $relationName . 'List';
611
            $relationEager = strtolower($relationAlias);
612
            $relationInterface = $relationName . 'AbstractInterface';
613
            
614
            // foreach columns of that other table
615
            foreach ($otherTableColumns as $otherTableColumn) {
616
                $otherColumnName = $otherTableColumn->getName();
617
                $otherPropertyName = $this->getPropertyName($otherColumnName);
618
                
619
                // if the column name starts with the current table name
620
                if (str_starts_with($otherColumnName, $table . '_')) {
621
                    
622
                    // foreach column of the current table
623
                    foreach ($columns as $column) {
624
                        assert($column instanceof ColumnInterface);
625
                        $columnName = $column->getName();
626
                        
627
                        // if the field is matching
628
                        if ($otherColumnName === $table . '_' . $columnName) {
629
                            $propertyName = $this->getPropertyName($columnName);
630
                            
631
                            $useInterfaces[$relationInterface] = true;
632
                            $interfaceInjectableItems []= <<<PHP
633
 * @property {$relationInterface}[] \${$relationEager}
634
 * @property {$relationInterface}[] \${$relationAlias}
635
 * @method {$relationInterface}[] get{$relationAlias}(?array \$params = null)
636
PHP;
637
                            
638
                            $useModels[$relationName] = true;
639
                            $relationshipInjectableItems []= <<<PHP
640
 * @property {$relationName}[] \${$relationEager}
641
 * @property {$relationName}[] \${$relationAlias}
642
 * @method {$relationName}[] get{$relationAlias}(?array \$params = null)
643
PHP;
644
                            
645
                            $relationshipItems []= <<<PHP
646
        \$this->hasMany('{$propertyName}', {$relationClass}, '{$otherPropertyName}', ['alias' => '{$relationAlias}']);
647
PHP;
648
                            // check if we have many-to-many
649
                            foreach ($otherTableColumns as $manyTableColumn) {
650
                                assert($manyTableColumn instanceof ColumnInterface);
651
                                $manyColumnName = $manyTableColumn->getName();
652
                                $manyPropertyName = $this->getPropertyName($manyColumnName);
653
                                
654
                                // skip itself
655
                                if ($manyColumnName === $otherColumnName) {
656
                                    continue;
657
                                }
658
                                
659
                                foreach ($tables as $manyManyTable) {
660
                                    $manyManyTableName = $this->getTableName($manyManyTable);
661
                                    $manyManyTableInterface = $manyManyTableName . 'AbstractInterface';
662
                                    $manyManyTableClass = $manyManyTableName . '::class';
663
                                    $manyManyTableAlias = $manyManyTableName . 'List';
664
                                    
665
                                    // to prevent duplicates in this specific scenario when we find many-to-many relationships
666
                                    // that are not actually nodes, we will enforce the full many-to-many path alias
667
                                    if (!(str_starts_with($otherTable, $table . '_') || str_ends_with($otherTable, '_' . $table))) {
668
                                        $manyManyTableAlias = $relationName . $manyManyTableName . 'List';
669
                                    }
670
                                    
671
                                    $manyManyTableEager = strtolower($manyManyTableAlias);
672
                                    
673
                                    if (str_starts_with($manyColumnName, $manyManyTable . '_')) {
674
                                        
675
                                        $manyManyTableColumns = $this->describeColumns($manyManyTable);
676
                                        foreach ($manyManyTableColumns as $manyManyTableColumn) {
677
                                            $manyManyColumnName = $manyManyTableColumn->getName();
678
                                            if ($manyColumnName === $manyManyTable . '_' . $manyManyColumnName) {
679
                                                $manyManyPropertyName = $this->getPropertyName($manyManyColumnName);
680
                                                
681
                                                $useInterfaces[$manyManyTableInterface] = true;
682
                                                $interfaceInjectableItems []= <<<PHP
683
 * @property {$manyManyTableInterface}[] \${$manyManyTableEager}
684
 * @property {$manyManyTableInterface}[] \${$manyManyTableAlias}
685
 * @method {$manyManyTableInterface}[] get{$manyManyTableAlias}(?array \$params = null)
686
PHP;
687
                                                
688
                                                $useModels[$manyManyTableName] = true;
689
                                                $relationshipInjectableItems []= <<<PHP
690
 * @property {$manyManyTableName}[] \${$manyManyTableEager}
691
 * @property {$manyManyTableName}[] \${$manyManyTableAlias}
692
 * @method {$manyManyTableName}[] get{$manyManyTableAlias}(?array \$params = null)
693
PHP;
694
                                                
695
                                                $relationshipItems []= <<<PHP
696
        \$this->hasManyToMany(
697
            '{$propertyName}',
698
            {$relationClass},
699
            '{$otherPropertyName}',
700
            '{$manyPropertyName}',
701
            {$manyManyTableClass},
702
            '{$manyManyPropertyName}',
703
            ['alias' => '{$manyManyTableAlias}']
704
        );
705
PHP;
706
                                            }
707
                                        }
708
                                    }
709
                                }
710
                            }
711
                        }
712
                    }
713
                }
714
            }
715
        }
716
        
717
        // Belongs To
718
        foreach ($columns as $column) {
719
            assert($column instanceof ColumnInterface);
720
            $columnName = $column->getName();
721
            $propertyName = $this->getPropertyName($columnName);
722
            
723
            $relationName = $this->getTableName(substr($columnName, 0, strlen($columnName) - 3));
724
            $relationTableName = $relationName;
725
            
726
            if (str_ends_with($columnName, '_id')) {
727
                switch ($relationName) {
728
                    case 'Parent':
729
                    case 'Child':
730
                    case 'Left':
731
                    case 'Right':
732
                        $relationTableName = $this->getTableName($table);
733
                        if (str_contains($table, '_')) {
734
                            $length = strlen($relationTableName);
735
                            $midpoint = (int)floor($length / 2);
736
                            $firstPart = substr($relationTableName, 0, $midpoint);
737
                            $secondPart = substr($relationTableName, $midpoint + (($length % 2 === 0)? 0 : 1));
738
                            if ($firstPart === $secondPart) {
739
                                $relationTableName = $firstPart;
740
                            }
741
                        }
742
                        
743
                        break;
744
                }
745
            }
746
            
747
            if (str_ends_with($columnName, '_as') || str_ends_with($columnName, '_by')) {
748
                $relationName = $this->getTableName($columnName);
749
                $relationTableName = 'user';
750
            }
751
            
752
            if ($relationName) {
753
                
754
                $relationTableNameUncamelized = Helper::uncamelize($relationTableName);
755
                while (!empty($relationTableNameUncamelized) && !in_array($relationTableNameUncamelized, $tables, true)) {
756
                    $index = strpos($relationTableNameUncamelized, '_');
757
                    $relationTableNameUncamelized = $index? substr($relationTableNameUncamelized, $index + 1, strlen($relationTableNameUncamelized)) : null;
758
                }
759
                
760
                // can't find the table, skip
761
                if (empty($relationTableNameUncamelized)) {
762
                    continue;
763
                }
764
                
765
                $relationTableName = $this->getTableName($relationTableNameUncamelized);
766
                $relationTableInterface = $relationTableName . 'AbstractInterface';
767
                $relationClass = $relationTableName . '::class';
768
                $relationAlias = $relationName . 'Entity';
769
                $relationEager = strtolower($relationAlias);
770
                
771
                $useInterfaces[$relationTableInterface] = true;
772
                $interfaceInjectableItems []= <<<PHP
773
 * @property {$relationTableInterface} \${$relationEager}
774
 * @property {$relationTableInterface} \${$relationAlias}
775
 * @method {$relationTableInterface} get{$relationAlias}(?array \$params = null)
776
PHP;
777
                
778
                $useModels[$relationTableName] = true;
779
                $relationshipInjectableItems []= <<<PHP
780
 * @property {$relationTableName} \${$relationEager}
781
 * @property {$relationTableName} \${$relationAlias}
782
 * @method {$relationTableName} get{$relationAlias}(?array \$params = null)
783
PHP;
784
                
785
                $relationshipItems []= <<<PHP
786
        \$this->belongsTo('{$propertyName}', {$relationClass}, 'id', ['alias' => '{$relationAlias}']);
787
PHP;
788
            }
789
        }
790
        
791
        foreach (array_keys($useModels) as $useItem) {
792
            $relationshipUseItems []= 'use ' . $modelNamespace . $useItem . ';';
793
        }
794
        foreach (array_keys($useInterfaces) as $useInterface) {
795
            $interfaceUseItems []= 'use ' . $modelNamespace . $useInterface . ';';
796
        }
797
        
798
        // Avoid empty lines if not relationship were found
799
        if (empty($relationshipInjectableItems)) {
800
            $relationshipInjectableItems = [' * '];
801
        }
802
        if (empty($relationshipItems)) {
803
            $relationshipItems = ['// no default relationship found'];
804
        }
805
        
806
        return [
807
            'interfaceInjectableItems' => implode("\n" . ' *' . "\n", $interfaceInjectableItems),
808
            'injectableItems' => implode("\n" . ' *' . "\n", $relationshipInjectableItems),
809
            'useItems' => trim(implode("\n", $relationshipUseItems)),
810
            'interfaceUseItems' => trim(implode("\n", $interfaceUseItems)),
811
            'items' => trim(implode("\n" . "\n", $relationshipItems)),
812
        ];
813
    }
814
    
815
    public function getColumnMapMethod(array $columns): string
816
    {
817
        if ($this->isNoColumnMap()) {
818
            return '';
819
        }
820
        
821
        $columnMapItems = $this->getColumnMapItems($columns);
822
        $columnMapComment = $this->getColumnMapComment();
823
        return <<<PHP
824
    {$columnMapComment}
825
    public function columnMap(): array
826
    {
827
        return [
828
{$columnMapItems}
829
        ];
830
    }
831
PHP;
832
    }
833
    
834
    /**
835
     * Returns the documentation comment for the `getColumnMap` method.
836
     * 
837
     * @return string The documentation comment for the `getColumnMap` method.
838
     */
839
    public function getColumnMapComment(): string
840
    {
841
        if ($this->isNoComments()) {
842
            return '';
843
        }
844
        
845
        return <<<PHP
846
847
    /**
848
     * Returns an array that maps the column names of the database
849
     * table to the corresponding property names of the model.
850
     * 
851
     * @returns array The array mapping the column names to the property names
852
     */
853
PHP;
854
    }
855
    
856
    /**
857
     * Generates a string representation of column map items for a given array of columns.
858
     *
859
     * @param array $columns An array of columns.
860
     *
861
     * @return string The string representation of the column map items.
862
     */
863
    public function getColumnMapItems(array $columns): string
864
    {
865
        $columnMapItems = [];
866
        foreach ($columns as $column) {
867
            assert($column instanceof ColumnInterface);
868
            $columnName = $column->getName();
869
            $columnMap = $this->getPropertyName($columnName);
870
            $columnMapItems[] = <<<PHP
871
            '{$columnName}' => '{$columnMap}',
872
PHP;
873
        }
874
        return implode("\n", $columnMapItems);
875
    }
876
    
877
    /**
878
     * Generates property items for each column in the given array.
879
     *
880
     * @param array $columns An array of ColumnInterface objects.
881
     *
882
     * @return string The generated property items.
883
     */
884
    public function getPropertyItems(array $columns): string
885
    {
886
        $propertyItems = [];
887
        foreach ($columns as $column) {
888
            assert($column instanceof ColumnInterface);
889
            $definition = $this->getPropertyDefinitions($column);
890
            $propertyComment = $this->getPropertyComment($column, $definition);
891
            $propertyItems[] = <<<PHP
892
    {$propertyComment}
893
    {$definition['visibility']} {$definition['property']};
894
PHP;
895
        }
896
        
897
        return trim(implode("\n", $propertyItems));
898
    }
899
    
900
    /**
901
     * Generates the comment for a property with the given column name and property type.
902
     *
903
     * @param ColumnInterface $column The column object.
904
     * @param array $definitions The property definitions.
905
     *
906
     * @return string The generated property comment.
907
     */
908
    public function getPropertyComment(ColumnInterface $column, array $definitions): string
909
    {
910
        if ($this->isNoComments()) {
911
            return '';
912
        }
913
        $propertyType = $definitions['type'] ?: 'mixed';
914
        return <<<PHP
915
    
916
    /**
917
     * Column: {$definitions['columnName']}
918
     * Attributes: {$this->getColumnAttributes($column)}
919
     * @var {$propertyType}
920
     */
921
PHP;
922
    }
923
    
924
    /**
925
     * Generates a string representation of getters and setters for a given array of columns.
926
     *
927
     * @param array $columns An array of columns.
928
     * @param string $type (optional) The type of code to generate. Can be 'default', 'interface', or 'test'. Default is 'default'.
929
     * @param string $property (optional) The name of the property to use in setter methods. Default is 'model'.
930
     *
931
     * @return string The string representation of the getters and setters.
932
     */
933
    public function getGetSetMethods(array $columns, string $type = 'default', string $property = 'model'): string
934
    {
935
        $propertyItems = [];
936
        foreach ($columns as $column) {
937
            assert($column instanceof ColumnInterface);
938
            $definition = $this->getPropertyDefinitions($column);
939
            
940
            $getMethod = 'get' . ucfirst($definition['name']);
941
            $setMethod = 'set' . ucfirst($definition['name']);
942
            
943
            $testGetMethod = 'test' . ucfirst($getMethod);
944
            $testSetMethod = 'test' . ucfirst($setMethod);
945
            $defaultValue = $definition['defaultValue'] ?: 'null';
946
            
947
            $getMethodComments = $this->getSetMethodComment($column, $definition, true);
948
            $setMethodComments = $this->getSetMethodComment($column, $definition, false);
949
            
950
            if (!$this->isNoTypings()) {
951
                $propertyType = isset($definition['type'])? ': ' . $definition['type'] : '';
952
                $voidType = ': void';
953
            } else {
954
                $propertyType = '';
955
                $voidType = '';
956
            }
957
            
958
            // For Model
959
            if ($type === 'default') {
960
                $propertyItems[] = <<<PHP
961
    {$getMethodComments}
962
    public function {$getMethod}(){$propertyType}
963
    {
964
        return \$this->{$definition['name']};
965
    }
966
    {$setMethodComments}
967
    public function {$setMethod}({$definition['param']}){$voidType}
968
    {
969
        \$this->{$definition['name']} = \${$definition['name']};
970
    }
971
PHP;
972
            }
973
            
974
            // For Interface
975
            if ($type === 'interface') {
976
                $propertyItems[] = <<<PHP
977
    {$getMethodComments}
978
    public function {$getMethod}(){$propertyType};
979
    {$setMethodComments}
980
    public function {$setMethod}({$definition['param']}){$voidType};
981
PHP;
982
            }
983
            
984
            // For Tests
985
            if ($type === 'test') {
986
                $propertyItems[] = <<<PHP
987
988
    public function {$testGetMethod}(){$voidType}
989
    {
990
        \$this->assertEquals({$defaultValue}, \$this->{$property}->{$getMethod}());
991
    }
992
    
993
    public function {$testSetMethod}(){$voidType}
994
    {
995
        \$value = uniqid();
996
        \$this->{$property}->{$setMethod}(\$value);
997
        \$this->assertEquals(\$value, \$this->{$property}->{$getMethod}());
998
    }
999
PHP;
1000
            }
1001
        }
1002
        return trim(implode("\n", $propertyItems));
1003
    }
1004
    
1005
    /**
1006
     * Generates a comment for a getter or setter method for a specific column.
1007
     *
1008
     * @param ColumnInterface $column The column object.
1009
     * @param array $definitions The property definitions.
1010
     * @param bool $get Determines whether the comment is for a getter or setter method.
1011
     *
1012
     * @return string The generated comment.
1013
     */
1014
    public function getSetMethodComment(ColumnInterface $column, array $definitions, bool $get): string
1015
    {
1016
        if ($this->isNoComments()) {
1017
            return '';
1018
        }
1019
        
1020
        $propertyType = $definitions['type'] ?: 'mixed';
1021
        
1022
        if ($get) {
1023
            return <<<PHP
1024
1025
    /**
1026
     * Returns the value of field {$definitions['name']}
1027
     * Column: {$definitions['columnName']}
1028
     * Attributes: {$this->getColumnAttributes($column)}
1029
     * @return {$propertyType}
1030
     */
1031
PHP;
1032
        }
1033
        
1034
        else {
1035
            return <<<PHP
1036
1037
    /**
1038
     * Sets the value of field {$definitions['name']}
1039
     * Column: {$definitions['columnName']} 
1040
     * Attributes: {$this->getColumnAttributes($column)}
1041
     * @param {$propertyType} \${$definitions['name']}
1042
     * @return void
1043
     */
1044
PHP;
1045
        }
1046
    }
1047
    
1048
    public function getColumnAttributes(ColumnInterface $column): string
1049
    {
1050
        $attributes = [];
1051
        if ($column->isFirst()) {
1052
            $attributes []= 'First';
1053
        }
1054
        if ($column->isPrimary()) {
1055
            $attributes []= 'Primary';
1056
        }
1057
        if ($column->isNotNull()) {
1058
            $attributes []= 'NotNull';
1059
        }
1060
        if ($column->isNumeric()) {
1061
            $attributes []= 'Numeric';
1062
        }
1063
        if ($column->isUnsigned()) {
1064
            $attributes []= 'Unsigned';
1065
        }
1066
        if ($column->isAutoIncrement()) {
1067
            $attributes []= 'AutoIncrement';
1068
        }
1069
        if ($column->getSize()) {
1070
            $attributes []= 'Size(' . $column->getSize() . ')';
1071
        }
1072
        if ($column->getScale()) {
1073
            $attributes []= 'Scale(' . $column->getSize() . ')';
1074
        }
1075
        if ($column->getType()) {
1076
            $attributes []= 'Type(' . $column->getType() . ')';
1077
        }
1078
        return implode(' | ', $attributes);
1079
    }
1080
    
1081
    public function getPropertyDefinitions(ColumnInterface $column): array
1082
    {
1083
        // column
1084
        $columnName = $column->getName();
1085
        $columnType = $this->getColumnType($column);
1086
        $defaultValue = $this->getDefaultValue($column);
1087
        $optional = !$column->isNotNull() || $column->isAutoIncrement() || is_null($defaultValue);
1088
        
1089
        // property
1090
        $propertyVisibility = $this->isProtectedProperties()? 'protected' : 'public';
1091
        $propertyName = $this->getPropertyName($column->getName());
1092
        
1093
        // property type
1094
        $propertyType = $this->isNoTypings()? '' : 'mixed';
1095
        if ($this->isGranularTypings()) {
1096
            $rawValueType = $this->isAddRawValueType()? 'RawValue|' : '';
1097
            $nullType = $optional? '|null' : '';
1098
            $propertyType = $rawValueType . $columnType . $nullType;
1099
        }
1100
        
1101
        // property raw value
1102
        $propertyValue = isset($defaultValue)? ' = ' . $defaultValue : '';
1103
        if (empty($propertyValue) && $optional) {
1104
            $propertyValue = ' = null';
1105
        }
1106
        
1107
        $param = (empty($propertyType)? '' : $propertyType . ' ') . "\${$propertyName}";
1108
        $property = "{$param}{$propertyValue}";
1109
        
1110
        return [
1111
            'columnName' => $columnName,
1112
            'columnType' => $columnType,
1113
            'defaultValue' => $defaultValue,
1114
            'optional' => $optional,
1115
            'visibility' => $propertyVisibility,
1116
            'name' => $propertyName,
1117
            'type' => $propertyType,
1118
            'value' => $propertyValue,
1119
            'param' => $param,
1120
            'property' => $property,
1121
        ];
1122
    }
1123
    
1124
    /**
1125
     * Saves a file with the given text content.
1126
     *
1127
     * @param string $file The path of the file to be saved.
1128
     * @param string $text The content to be written to the file.
1129
     * @param bool $force Determines whether to overwrite an existing file. Default is false.
1130
     *
1131
     * @return bool Returns true if the file was saved successfully, false otherwise.
1132
     */
1133
    public function saveFile(string $file, string $text, bool $force = false): bool
1134
    {
1135
        if (!$force && file_exists($file)) {
1136
            return false;
1137
        }
1138
        
1139
        $directory = dirname($file);
1140
        
1141
        // Create the directory if it doesn't exist
1142
        if (!is_dir($directory) && !mkdir($directory, 0755, true) && !is_dir($directory)) {
1143
            return false; // Failed to create directory
1144
        }
1145
        
1146
        // Convert text to UTF-8
1147
        $utf8Text = mb_convert_encoding($text, 'UTF-8');
1148
        if ($utf8Text === false) {
1149
            return false; // Failed to convert to UTF-8
1150
        }
1151
        
1152
        // Optional: Add UTF-8 BOM
1153
//        $utf8Text = "\xEF\xBB\xBF" . $utf8Text;
1154
        
1155
        // Write the file
1156
        $fileHandle = fopen($file, 'w');
1157
        if ($fileHandle === false) {
1158
            return false; // Failed to open file
1159
        }
1160
        
1161
        $writeSuccess = fwrite($fileHandle, (string)$utf8Text) !== false;
1162
        $closeSuccess = fclose($fileHandle);
1163
        
1164
        return $writeSuccess && $closeSuccess;
1165
    }
1166
}
1167