ScaffoldTask::getRelationshipItems()   F
last analyzed

Complexity

Conditions 36
Paths > 20000

Size

Total Lines 234
Code Lines 153

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 1332

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 36
eloc 153
c 3
b 1
f 0
nc 28369
nop 3
dl 0
loc 234
ccs 0
cts 154
cp 0
crap 1332
rs 0

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