Passed
Pull Request — master (#146)
by David
07:02
created

BeanDescriptor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 27
rs 9.8333
cc 1
nc 1
nop 12

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM\Utils;
5
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\Index;
8
use Doctrine\DBAL\Schema\Schema;
9
use Doctrine\DBAL\Schema\Table;
10
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
11
use JsonSerializable;
12
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
13
use PhpParser\Comment\Doc;
14
use Ramsey\Uuid\Uuid;
15
use TheCodingMachine\TDBM\AbstractTDBMObject;
16
use TheCodingMachine\TDBM\AlterableResultIterator;
17
use TheCodingMachine\TDBM\ConfigurationInterface;
18
use TheCodingMachine\TDBM\ResultIterator;
19
use TheCodingMachine\TDBM\SafeFunctions;
20
use TheCodingMachine\TDBM\Schema\ForeignKey;
21
use TheCodingMachine\TDBM\Schema\ForeignKeys;
22
use TheCodingMachine\TDBM\TDBMException;
23
use TheCodingMachine\TDBM\TDBMSchemaAnalyzer;
24
use TheCodingMachine\TDBM\TDBMService;
25
use TheCodingMachine\TDBM\Utils\Annotation\AbstractTraitAnnotation;
26
use TheCodingMachine\TDBM\Utils\Annotation\AddInterfaceOnDao;
27
use TheCodingMachine\TDBM\Utils\Annotation\AddTrait;
28
use TheCodingMachine\TDBM\Utils\Annotation\AddTraitOnDao;
29
use TheCodingMachine\TDBM\Utils\Annotation\AnnotationParser;
30
use TheCodingMachine\TDBM\Utils\Annotation\AddInterface;
31
use Zend\Code\Generator\AbstractMemberGenerator;
32
use Zend\Code\Generator\ClassGenerator;
33
use Zend\Code\Generator\DocBlock\Tag;
34
use Zend\Code\Generator\DocBlock\Tag\GenericTag;
35
use Zend\Code\Generator\DocBlock\Tag\ParamTag;
36
use Zend\Code\Generator\DocBlock\Tag\ReturnTag;
37
use Zend\Code\Generator\DocBlock\Tag\ThrowsTag;
38
use Zend\Code\Generator\DocBlock\Tag\VarTag;
39
use Zend\Code\Generator\DocBlockGenerator;
40
use Zend\Code\Generator\FileGenerator;
41
use Zend\Code\Generator\MethodGenerator;
42
use Zend\Code\Generator\ParameterGenerator;
43
use Zend\Code\Generator\PropertyGenerator;
44
use function implode;
45
use function var_export;
46
47
/**
48
 * This class represents a bean.
49
 */
50
class BeanDescriptor implements BeanDescriptorInterface
51
{
52
    /**
53
     * @var Table
54
     */
55
    private $table;
56
57
    /**
58
     * @var SchemaAnalyzer
59
     */
60
    private $schemaAnalyzer;
61
62
    /**
63
     * @var Schema
64
     */
65
    private $schema;
66
67
    /**
68
     * @var AbstractBeanPropertyDescriptor[]
69
     */
70
    private $beanPropertyDescriptors = [];
71
72
    /**
73
     * @var TDBMSchemaAnalyzer
74
     */
75
    private $tdbmSchemaAnalyzer;
76
77
    /**
78
     * @var NamingStrategyInterface
79
     */
80
    private $namingStrategy;
81
    /**
82
     * @var string
83
     */
84
    private $beanNamespace;
85
    /**
86
     * @var string
87
     */
88
    private $generatedBeanNamespace;
89
    /**
90
     * @var AnnotationParser
91
     */
92
    private $annotationParser;
93
    /**
94
     * @var string
95
     */
96
    private $daoNamespace;
97
    /**
98
     * @var string
99
     */
100
    private $generatedDaoNamespace;
101
    /**
102
     * @var CodeGeneratorListenerInterface
103
     */
104
    private $codeGeneratorListener;
105
    /**
106
     * @var ConfigurationInterface
107
     */
108
    private $configuration;
109
110
    public function __construct(
111
        Table $table,
112
        string $beanNamespace,
113
        string $generatedBeanNamespace,
114
        string $daoNamespace,
115
        string $generatedDaoNamespace,
116
        SchemaAnalyzer $schemaAnalyzer,
117
        Schema $schema,
118
        TDBMSchemaAnalyzer $tdbmSchemaAnalyzer,
119
        NamingStrategyInterface $namingStrategy,
120
        AnnotationParser $annotationParser,
121
        CodeGeneratorListenerInterface $codeGeneratorListener,
122
        ConfigurationInterface $configuration
123
    ) {
124
        $this->table = $table;
125
        $this->beanNamespace = $beanNamespace;
126
        $this->generatedBeanNamespace = $generatedBeanNamespace;
127
        $this->daoNamespace = $daoNamespace;
128
        $this->generatedDaoNamespace = $generatedDaoNamespace;
129
        $this->schemaAnalyzer = $schemaAnalyzer;
130
        $this->schema = $schema;
131
        $this->tdbmSchemaAnalyzer = $tdbmSchemaAnalyzer;
132
        $this->namingStrategy = $namingStrategy;
133
        $this->annotationParser = $annotationParser;
134
        $this->codeGeneratorListener = $codeGeneratorListener;
135
        $this->configuration = $configuration;
136
        $this->initBeanPropertyDescriptors();
137
    }
138
139
    private function initBeanPropertyDescriptors(): void
140
    {
141
        $this->beanPropertyDescriptors = $this->getProperties($this->table);
142
    }
143
144
    /**
145
     * Returns the foreign-key the column is part of, if any. null otherwise.
146
     *
147
     * @param Table  $table
148
     * @param Column $column
149
     *
150
     * @return ForeignKeyConstraint|null
151
     */
152
    private function isPartOfForeignKey(Table $table, Column $column) : ?ForeignKeyConstraint
153
    {
154
        $localColumnName = $column->getName();
155
        foreach ($table->getForeignKeys() as $foreignKey) {
156
            foreach ($foreignKey->getUnquotedLocalColumns() as $columnName) {
157
                if ($columnName === $localColumnName) {
158
                    return $foreignKey;
159
                }
160
            }
161
        }
162
163
        return null;
164
    }
165
166
    /**
167
     * @return AbstractBeanPropertyDescriptor[]
168
     */
169
    public function getBeanPropertyDescriptors(): array
170
    {
171
        return $this->beanPropertyDescriptors;
172
    }
173
174
    /**
175
     * Returns the list of columns that are not nullable and not autogenerated for a given table and its parent.
176
     *
177
     * @return AbstractBeanPropertyDescriptor[]
178
     */
179
    public function getConstructorProperties(): array
180
    {
181
        $constructorProperties = array_filter($this->beanPropertyDescriptors, function (AbstractBeanPropertyDescriptor $property) {
182
            return $property->isCompulsory();
183
        });
184
185
        return $constructorProperties;
186
    }
187
188
    /**
189
     * Returns the list of columns that have default values for a given table.
190
     *
191
     * @return AbstractBeanPropertyDescriptor[]
192
     */
193
    public function getPropertiesWithDefault(): array
194
    {
195
        $properties = $this->getPropertiesForTable($this->table);
196
        $defaultProperties = array_filter($properties, function (AbstractBeanPropertyDescriptor $property) {
197
            return $property->hasDefault();
198
        });
199
200
        return $defaultProperties;
201
    }
202
203
    /**
204
     * Returns the list of properties exposed as getters and setters in this class.
205
     *
206
     * @return AbstractBeanPropertyDescriptor[]
207
     */
208
    public function getExposedProperties(): array
209
    {
210
        $exposedProperties = array_filter($this->beanPropertyDescriptors, function (AbstractBeanPropertyDescriptor $property) {
211
            return $property->getTable()->getName() == $this->table->getName();
212
        });
213
214
        return $exposedProperties;
215
    }
216
217
    /**
218
     * Returns the list of properties for this table (including parent tables).
219
     *
220
     * @param Table $table
221
     *
222
     * @return AbstractBeanPropertyDescriptor[]
223
     */
224
    private function getProperties(Table $table): array
225
    {
226
        // Security check: a table MUST have a primary key
227
        TDBMDaoGenerator::getPrimaryKeyColumnsOrFail($table);
228
229
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
230
        if ($parentRelationship) {
231
            $parentTable = $this->schema->getTable($parentRelationship->getForeignTableName());
232
            $properties = $this->getProperties($parentTable);
233
            // we merge properties by overriding property names.
234
            $localProperties = $this->getPropertiesForTable($table);
235
            foreach ($localProperties as $name => $property) {
236
                // We do not override properties if this is a primary key!
237
                if ($property->isPrimaryKey()) {
238
                    continue;
239
                }
240
                $properties[$name] = $property;
241
            }
242
        } else {
243
            $properties = $this->getPropertiesForTable($table);
244
        }
245
246
        return $properties;
247
    }
248
249
    /**
250
     * Returns the list of properties for this table (ignoring parent tables).
251
     *
252
     * @param Table $table
253
     *
254
     * @return AbstractBeanPropertyDescriptor[]
255
     */
256
    private function getPropertiesForTable(Table $table): array
257
    {
258
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
259
        if ($parentRelationship) {
260
            $ignoreColumns = $parentRelationship->getUnquotedLocalColumns();
261
        } else {
262
            $ignoreColumns = [];
263
        }
264
265
        $beanPropertyDescriptors = [];
266
        foreach ($table->getColumns() as $column) {
267
            if (array_search($column->getName(), $ignoreColumns) !== false) {
268
                continue;
269
            }
270
271
            $fk = $this->isPartOfForeignKey($table, $column);
272
            if ($fk !== null) {
273
                // Check that previously added descriptors are not added on same FK (can happen with multi key FK).
274
                foreach ($beanPropertyDescriptors as $beanDescriptor) {
275
                    if ($beanDescriptor instanceof ObjectBeanPropertyDescriptor && $beanDescriptor->getForeignKey() === $fk) {
276
                        continue 2;
277
                    }
278
                }
279
                // Check that this property is not an inheritance relationship
280
                $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
281
                if ($parentRelationship === $fk) {
282
                    continue;
283
                }
284
285
                $beanPropertyDescriptors[] = new ObjectBeanPropertyDescriptor($table, $fk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser);
286
            } else {
287
                $beanPropertyDescriptors[] = new ScalarBeanPropertyDescriptor($table, $column, $this->namingStrategy, $this->annotationParser);
288
            }
289
        }
290
291
        // Now, let's get the name of all properties and let's check there is no duplicate.
292
        /* @var $names AbstractBeanPropertyDescriptor[] */
293
        $names = [];
294
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
295
            $name = $beanDescriptor->getGetterName();
296
            if (isset($names[$name])) {
297
                $names[$name]->useAlternativeName();
298
                $beanDescriptor->useAlternativeName();
299
            } else {
300
                $names[$name] = $beanDescriptor;
301
            }
302
        }
303
304
        // Final check (throw exceptions if problem arises)
305
        $names = [];
306
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
307
            $name = $beanDescriptor->getGetterName();
308
            if (isset($names[$name])) {
309
                throw new TDBMException('Unsolvable name conflict while generating method name');
310
            } else {
311
                $names[$name] = $beanDescriptor;
312
            }
313
        }
314
315
        // Last step, let's rebuild the list with a map:
316
        $beanPropertyDescriptorsMap = [];
317
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
318
            $beanPropertyDescriptorsMap[$beanDescriptor->getVariableName()] = $beanDescriptor;
319
        }
320
321
        return $beanPropertyDescriptorsMap;
322
    }
323
324
    private function generateBeanConstructor() : MethodGenerator
325
    {
326
        $constructorProperties = $this->getConstructorProperties();
327
328
        $constructor = new MethodGenerator('__construct', [], MethodGenerator::FLAG_PUBLIC);
329
        $constructor->setDocBlock('The constructor takes all compulsory arguments.');
330
331
        $assigns = [];
332
        $parentConstructorArguments = [];
333
334
        foreach ($constructorProperties as $property) {
335
            $parameter = new ParameterGenerator(ltrim($property->getVariableName(), '$'));
336
            if ($property->isTypeHintable()) {
337
                $parameter->setType($property->getPhpType());
338
            }
339
            $constructor->setParameter($parameter);
340
341
            $constructor->getDocBlock()->setTag($property->getParamAnnotation());
342
343
            if ($property->getTable()->getName() === $this->table->getName()) {
344
                $assigns[] = $property->getConstructorAssignCode()."\n";
345
            } else {
346
                $parentConstructorArguments[] = $property->getVariableName();
347
            }
348
        }
349
350
        $parentConstructorCode = sprintf("parent::__construct(%s);\n", implode(', ', $parentConstructorArguments));
351
352
        foreach ($this->getPropertiesWithDefault() as $property) {
353
            $assigns[] = $property->assignToDefaultCode()."\n";
354
        }
355
356
        $body = $parentConstructorCode . implode('', $assigns);
357
358
        $constructor->setBody($body);
359
360
        return $constructor;
361
    }
362
363
    /**
364
     * Returns the descriptors of one-to-many relationships (the foreign keys pointing on this beans)
365
     *
366
     * @return DirectForeignKeyMethodDescriptor[]
367
     */
368
    private function getDirectForeignKeysDescriptors(): array
369
    {
370
        $fks = $this->tdbmSchemaAnalyzer->getIncomingForeignKeys($this->table->getName());
371
372
        $descriptors = [];
373
374
        foreach ($fks as $fk) {
375
            $descriptors[] = new DirectForeignKeyMethodDescriptor($fk, $this->table, $this->namingStrategy, $this->annotationParser, $this->beanNamespace);
376
        }
377
378
        return $descriptors;
379
    }
380
381
    /**
382
     * @return PivotTableMethodsDescriptor[]
383
     */
384
    private function getPivotTableDescriptors(): array
385
    {
386
        $descs = [];
387
        foreach ($this->schemaAnalyzer->detectJunctionTables(true) as $table) {
388
            // There are exactly 2 FKs since this is a pivot table.
389
            $fks = array_values($table->getForeignKeys());
390
391
            //todo auto pivot case?
392
            if ($fks[0]->getForeignTableName() === $this->table->getName()) {
393
                list($localFk, $remoteFk) = $fks;
394
                $descs[] = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser);
395
            }
396
            if ($fks[1]->getForeignTableName() === $this->table->getName()) {
397
                list($remoteFk, $localFk) = $fks;
398
                $descs[] = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser);
399
            }
400
401
402
        }
403
404
        return $descs;
405
    }
406
407
    /**
408
     * Returns the list of method descriptors (and applies the alternative name if needed).
409
     *
410
     * @return MethodDescriptorInterface[]
411
     */
412
    public function getMethodDescriptors(): array
413
    {
414
        $directForeignKeyDescriptors = $this->getDirectForeignKeysDescriptors();
415
        $pivotTableDescriptors = $this->getPivotTableDescriptors();
416
417
        $descriptors = array_merge($directForeignKeyDescriptors, $pivotTableDescriptors);
418
419
        // Descriptors by method names
420
        $descriptorsByMethodName = [];
421
422
        foreach ($descriptors as $descriptor) {
423
            $descriptorsByMethodName[$descriptor->getName()][] = $descriptor;
424
        }
425
426
        foreach ($descriptorsByMethodName as $descriptorsForMethodName) {
427
            if (count($descriptorsForMethodName) > 1) {
428
                foreach ($descriptorsForMethodName as $descriptor) {
429
                    $descriptor->useAlternativeName();
430
                }
431
            }
432
        }
433
434
        return $descriptors;
435
    }
436
437
    public function generateJsonSerialize(): MethodGenerator
438
    {
439
        $tableName = $this->table->getName();
440
        $parentFk = $this->schemaAnalyzer->getParentRelationship($tableName);
441
442
        $method = new MethodGenerator('jsonSerialize');
443
        $method->setDocBlock('Serializes the object for JSON encoding.');
444
        $method->getDocBlock()->setTag(new ParamTag('$stopRecursion', ['bool'], 'Parameter used internally by TDBM to stop embedded objects from embedding other objects.'));
445
        $method->getDocBlock()->setTag(new ReturnTag(['array']));
446
        $method->setParameter(new ParameterGenerator('stopRecursion', 'bool', false));
447
448
        if ($parentFk !== null) {
449
            $body = '$array = parent::jsonSerialize($stopRecursion);';
450
        } else {
451
            $body = '$array = [];';
452
        }
453
454
        foreach ($this->getExposedProperties() as $beanPropertyDescriptor) {
455
            $propertyCode = $beanPropertyDescriptor->getJsonSerializeCode();
456
            if (!empty($propertyCode)) {
457
                $body .= PHP_EOL . $propertyCode;
458
            }
459
        }
460
461
        // Many2many relationships
462
        foreach ($this->getMethodDescriptors() as $methodDescriptor) {
463
            $methodCode = $methodDescriptor->getJsonSerializeCode();
464
            if (!empty($methodCode)) {
465
                $body .= PHP_EOL . $methodCode;
466
            }
467
        }
468
469
        $body .= PHP_EOL . 'return $array;';
470
471
        $method->setBody($body);
472
473
        return $method;
474
    }
475
476
    /**
477
     * Returns as an array the class we need to extend from and the list of use statements.
478
     *
479
     * @param ForeignKeyConstraint|null $parentFk
480
     * @return string[]
481
     */
482
    private function generateExtendsAndUseStatements(ForeignKeyConstraint $parentFk = null): array
483
    {
484
        $classes = [];
485
        if ($parentFk !== null) {
486
            $extends = $this->namingStrategy->getBeanClassName($parentFk->getForeignTableName());
487
            $classes[] = $extends;
488
        }
489
490
        foreach ($this->getBeanPropertyDescriptors() as $beanPropertyDescriptor) {
491
            $className = $beanPropertyDescriptor->getClassName();
492
            if (null !== $className) {
493
                $classes[] = $className;
494
            }
495
        }
496
497
        foreach ($this->getMethodDescriptors() as $descriptor) {
498
            $classes = array_merge($classes, $descriptor->getUsedClasses());
499
        }
500
501
        $classes = array_unique($classes);
502
503
        return $classes;
504
    }
505
506
    /**
507
     * Returns the representation of the PHP bean file with all getters and setters.
508
     *
509
     * @return ?FileGenerator
510
     */
511
    public function generatePhpCode(): ?FileGenerator
512
    {
513
        $file = new FileGenerator();
514
        $class = new ClassGenerator();
515
        $class->setAbstract(true);
516
        $file->setClass($class);
517
        $file->setNamespace($this->generatedBeanNamespace);
518
519
        $tableName = $this->table->getName();
520
        $baseClassName = $this->namingStrategy->getBaseBeanClassName($tableName);
521
        $className = $this->namingStrategy->getBeanClassName($tableName);
522
        $parentFk = $this->schemaAnalyzer->getParentRelationship($this->table->getName());
523
524
        $classes = $this->generateExtendsAndUseStatements($parentFk);
525
526
        foreach ($classes as $useClass) {
527
            $file->setUse($this->beanNamespace.'\\'.$useClass);
528
        }
529
530
        /*$uses = array_map(function ($className) {
531
            return 'use '.$this->beanNamespace.'\\'.$className.";\n";
532
        }, $classes);
533
        $use = implode('', $uses);*/
534
535
        $extends = $this->getExtendedBeanClassName();
536
        if ($extends === null) {
537
            $class->setExtendedClass(AbstractTDBMObject::class);
538
            $file->setUse(AbstractTDBMObject::class);
539
        } else {
540
            $class->setExtendedClass($extends);
541
        }
542
543
        $file->setUse(ResultIterator::class);
544
        $file->setUse(AlterableResultIterator::class);
545
        $file->setUse(Uuid::class);
546
        $file->setUse(JsonSerializable::class);
547
        $file->setUse(ForeignKeys::class);
548
549
        $class->setName($baseClassName);
550
551
        $file->setDocBlock(new DocBlockGenerator(
552
            'This file has been automatically generated by TDBM.',
553
            <<<EOF
554
DO NOT edit this file, as it might be overwritten.
555
If you need to perform changes, edit the $className class instead!
556
EOF
557
        ));
558
559
        $class->setDocBlock(new DocBlockGenerator("The $baseClassName class maps the '$tableName' table in database."));
560
561
        /** @var AddInterface[] $addInterfaceAnnotations */
562
        $addInterfaceAnnotations = $this->annotationParser->getTableAnnotations($this->table)->findAnnotations(AddInterface::class);
563
564
        $interfaces = [ JsonSerializable::class ];
565
        foreach ($addInterfaceAnnotations as $annotation) {
566
            $interfaces[] = $annotation->getName();
567
        }
568
569
        $class->setImplementedInterfaces($interfaces);
570
571
        $this->registerTraits($class, AddTrait::class);
572
573
        $method = $this->generateBeanConstructor();
574
        $method = $this->codeGeneratorListener->onBaseBeanConstructorGenerated($method, $this, $this->configuration, $class);
575
        if ($method) {
576
            $class->addMethodFromGenerator($this->generateBeanConstructor());
577
        }
578
579
        $fks = [];
580
        foreach ($this->getExposedProperties() as $property) {
581
            if ($property instanceof ObjectBeanPropertyDescriptor) {
582
                $fks[] = $property->getForeignKey();
583
            }
584
            [$getter, $setter] = $property->getGetterSetterCode();
585
            [$getter, $setter] = $this->codeGeneratorListener->onBaseBeanPropertyGenerated($getter, $setter, $property, $this, $this->configuration, $class);
586
            if ($getter !== null) {
587
                $class->addMethodFromGenerator($getter);
588
            }
589
            if ($setter !== null) {
590
                $class->addMethodFromGenerator($setter);
591
            }
592
        }
593
594
        $pivotTableMethodsDescriptors = [];
595
        foreach ($this->getMethodDescriptors() as $methodDescriptor) {
596
            if ($methodDescriptor instanceof DirectForeignKeyMethodDescriptor) {
597
                [$method] = $methodDescriptor->getCode();
598
                $method = $this->codeGeneratorListener->onBaseBeanOneToManyGenerated($method, $methodDescriptor, $this, $this->configuration, $class);
599
                if ($method) {
600
                    $class->addMethodFromGenerator($method);
601
                }
602
            } elseif ($methodDescriptor instanceof PivotTableMethodsDescriptor) {
603
                $pivotTableMethodsDescriptors[] = $methodDescriptor;
604
                [ $getter, $adder, $remover, $has, $setter ] = $methodDescriptor->getCode();
605
                $methods = $this->codeGeneratorListener->onBaseBeanManyToManyGenerated($getter, $adder, $remover, $has, $setter, $methodDescriptor, $this, $this->configuration, $class);
606
                foreach ($methods as $method) {
607
                    if ($method) {
608
                        $class->addMethodFromGenerator($method);
609
                    }
610
                }
611
            } else {
612
                throw new \RuntimeException('Unexpected instance'); // @codeCoverageIgnore
613
            }
614
        }
615
616
        $manyToManyRelationshipCode = $this->generateGetManyToManyRelationshipDescriptorCode($pivotTableMethodsDescriptors);
617
        if ($manyToManyRelationshipCode !== null) {
618
            $class->addMethodFromGenerator($manyToManyRelationshipCode);
619
        }
620
        $manyToManyRelationshipKeysCode = $this->generateGetManyToManyRelationshipDescriptorKeysCode($pivotTableMethodsDescriptors);
621
        if ($manyToManyRelationshipKeysCode !== null) {
622
            $class->addMethodFromGenerator($manyToManyRelationshipKeysCode);
623
        }
624
625
        $foreignKeysProperty = new PropertyGenerator('foreignKeys');
626
        $foreignKeysProperty->setStatic(true);
627
        $foreignKeysProperty->setVisibility(AbstractMemberGenerator::VISIBILITY_PRIVATE);
628
        $foreignKeysProperty->setDocBlock(new DocBlockGenerator(null, null, [new VarTag(null, ['\\'.ForeignKeys::class])]));
629
        $class->addPropertyFromGenerator($foreignKeysProperty);
630
631
        $method = $this->generateGetForeignKeys($fks);
632
        $class->addMethodFromGenerator($method);
633
634
        $method = $this->generateJsonSerialize();
635
        $method = $this->codeGeneratorListener->onBaseBeanJsonSerializeGenerated($method, $this, $this->configuration, $class);
636
        if ($method !== null) {
637
            $class->addMethodFromGenerator($method);
638
        }
639
640
        $class->addMethodFromGenerator($this->generateGetUsedTablesCode());
641
        $onDeleteCode = $this->generateOnDeleteCode();
642
        if ($onDeleteCode) {
643
            $class->addMethodFromGenerator($onDeleteCode);
644
        }
645
        $cloneCode = $this->generateCloneCode();
646
        $cloneCode = $this->codeGeneratorListener->onBaseBeanCloneGenerated($cloneCode, $this, $this->configuration, $class);
647
        if ($cloneCode) {
648
            $class->addMethodFromGenerator($cloneCode);
649
        }
650
651
        $file = $this->codeGeneratorListener->onBaseBeanGenerated($file, $this, $this->configuration);
652
653
        return $file;
654
    }
655
656
    private function registerTraits(ClassGenerator $class, string $annotationClass): void
657
    {
658
        /** @var AbstractTraitAnnotation[] $addTraitAnnotations */
659
        $addTraitAnnotations = $this->annotationParser->getTableAnnotations($this->table)->findAnnotations($annotationClass);
660
661
        foreach ($addTraitAnnotations as $annotation) {
662
            $class->addTrait($annotation->getName());
663
        }
664
665
        foreach ($addTraitAnnotations as $annotation) {
666
            foreach ($annotation->getInsteadOf() as $method => $replacedTrait) {
667
                $class->addTraitOverride($method, $replacedTrait);
668
            }
669
            foreach ($annotation->getAs() as $method => $replacedMethod) {
670
                $class->addTraitAlias($method, $replacedMethod);
671
            }
672
        }
673
    }
674
675
    /**
676
     * Writes the representation of the PHP DAO file.
677
     *
678
     * @return ?FileGenerator
679
     */
680
    public function generateDaoPhpCode(): ?FileGenerator
681
    {
682
        $file = new FileGenerator();
683
        $class = new ClassGenerator();
684
        $class->setAbstract(true);
685
        $file->setClass($class);
686
        $file->setNamespace($this->generatedDaoNamespace);
687
688
        $tableName = $this->table->getName();
689
690
        $primaryKeyColumns = TDBMDaoGenerator::getPrimaryKeyColumnsOrFail($this->table);
691
692
        list($defaultSort, $defaultSortDirection) = $this->getDefaultSortColumnFromAnnotation($this->table);
693
694
        $className = $this->namingStrategy->getDaoClassName($tableName);
695
        $baseClassName = $this->namingStrategy->getBaseDaoClassName($tableName);
696
        $beanClassWithoutNameSpace = $this->namingStrategy->getBeanClassName($tableName);
697
        $beanClassName = $this->beanNamespace.'\\'.$beanClassWithoutNameSpace;
698
699
        $findByDaoCodeMethods = $this->generateFindByDaoCode($this->beanNamespace, $beanClassWithoutNameSpace, $class);
700
701
        $usedBeans[] = $beanClassName;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$usedBeans was never initialized. Although not strictly required by PHP, it is generally a good practice to add $usedBeans = array(); before regardless.
Loading history...
702
        // Let's suppress duplicates in used beans (if any)
703
        $usedBeans = array_flip(array_flip($usedBeans));
704
        foreach ($usedBeans as $usedBean) {
705
            $class->addUse($usedBean);
706
        }
707
708
        $file->setDocBlock(new DocBlockGenerator(
709
            <<<EOF
710
This file has been automatically generated by TDBM.
711
DO NOT edit this file, as it might be overwritten.
712
If you need to perform changes, edit the $className class instead!
713
EOF
714
        ));
715
716
        $file->setNamespace($this->generatedDaoNamespace);
717
718
        $class->addUse(TDBMService::class);
719
        $class->addUse(ResultIterator::class);
720
        $class->addUse(TDBMException::class);
721
722
        $class->setName($baseClassName);
723
724
        $class->setDocBlock(new DocBlockGenerator("The $baseClassName class will maintain the persistence of $beanClassWithoutNameSpace class into the $tableName table."));
725
726
        /** @var AddInterfaceOnDao[] $addInterfaceOnDaoAnnotations */
727
        $addInterfaceOnDaoAnnotations = $this->annotationParser->getTableAnnotations($this->table)->findAnnotations(AddInterfaceOnDao::class);
728
729
        $interfaces = [];
730
        foreach ($addInterfaceOnDaoAnnotations as $annotation) {
731
            $interfaces[] = $annotation->getName();
732
        }
733
734
        $class->setImplementedInterfaces($interfaces);
735
736
        $this->registerTraits($class, AddTraitOnDao::class);
737
738
        $tdbmServiceProperty = new PropertyGenerator('tdbmService');
739
        $tdbmServiceProperty->setDocBlock(new DocBlockGenerator(null, null, [new VarTag(null, ['\\'.TDBMService::class])]));
740
        $class->addPropertyFromGenerator($tdbmServiceProperty);
741
742
        $defaultSortProperty = new PropertyGenerator('defaultSort', $defaultSort);
743
        $defaultSortProperty->setDocBlock(new DocBlockGenerator('The default sort column.', null, [new VarTag(null, ['string', 'null'])]));
744
        $class->addPropertyFromGenerator($defaultSortProperty);
745
746
        $defaultSortPropertyDirection = new PropertyGenerator('defaultDirection', $defaultSort && $defaultSortDirection ? $defaultSortDirection : 'asc');
747
        $defaultSortPropertyDirection->setDocBlock(new DocBlockGenerator('The default sort direction.', null, [new VarTag(null, ['string'])]));
748
        $class->addPropertyFromGenerator($defaultSortPropertyDirection);
749
750
        $constructorMethod = new MethodGenerator(
751
            '__construct',
752
            [ new ParameterGenerator('tdbmService', TDBMService::class) ],
753
            MethodGenerator::FLAG_PUBLIC,
754
            '$this->tdbmService = $tdbmService;',
755
            'Sets the TDBM service used by this DAO.'
756
        );
757
        $constructorMethod = $this->codeGeneratorListener->onBaseDaoConstructorGenerated($constructorMethod, $this, $this->configuration, $class);
758
        if ($constructorMethod !== null) {
759
            $class->addMethodFromGenerator($constructorMethod);
760
        }
761
762
        $saveMethod = new MethodGenerator(
763
            'save',
764
            [ new ParameterGenerator('obj', $beanClassName) ],
765
            MethodGenerator::FLAG_PUBLIC,
766
            '$this->tdbmService->save($obj);',
767
            new DocBlockGenerator(
768
                "Persist the $beanClassWithoutNameSpace instance.",
769
                null,
770
                [
771
                    new ParamTag('obj', [$beanClassWithoutNameSpace], 'The bean to save.')
772
                ]
773
            )
774
        );
775
        $saveMethod->setReturnType('void');
776
777
        $saveMethod = $this->codeGeneratorListener->onBaseDaoSaveGenerated($saveMethod, $this, $this->configuration, $class);
778
        if ($saveMethod !== null) {
779
            $class->addMethodFromGenerator($saveMethod);
780
        }
781
782
        $findAllBody = <<<EOF
783
if (\$this->defaultSort) {
784
    \$orderBy = '$tableName.'.\$this->defaultSort.' '.\$this->defaultDirection;
785
} else {
786
    \$orderBy = null;
787
}
788
return \$this->tdbmService->findObjects('$tableName', null, [], \$orderBy);
789
EOF;
790
791
        $findAllMethod = new MethodGenerator(
792
            'findAll',
793
            [],
794
            MethodGenerator::FLAG_PUBLIC,
795
            $findAllBody,
796
            (new DocBlockGenerator(
797
                "Get all $beanClassWithoutNameSpace records.",
798
                null,
799
                [
800
                    new ReturnTag([ '\\'.$beanClassName.'[]', '\\'.ResultIterator::class ])
801
                ]
802
            ))->setWordWrap(false)
803
        );
804
        $findAllMethod->setReturnType('\\'.ResultIterator::class);
805
        $findAllMethod = $this->codeGeneratorListener->onBaseDaoFindAllGenerated($findAllMethod, $this, $this->configuration, $class);
806
        if ($findAllMethod !== null) {
807
            $class->addMethodFromGenerator($findAllMethod);
808
        }
809
810
        if (count($primaryKeyColumns) > 0) {
811
            $lazyLoadingParameterName = 'lazyLoading';
812
            $parameters = [];
813
            $parametersTag = [];
814
            $primaryKeyFilter = [];
815
816
            foreach ($primaryKeyColumns as $primaryKeyColumn) {
817
                if ($primaryKeyColumn === $lazyLoadingParameterName) {
818
                    throw new TDBMException('Primary Column name `' . $lazyLoadingParameterName . '` is not allowed.');
819
                }
820
                $phpType = TDBMDaoGenerator::dbalTypeToPhpType($this->table->getColumn($primaryKeyColumn)->getType());
821
                $parameters[] = new ParameterGenerator($primaryKeyColumn, $phpType);
822
                $parametersTag[] = new ParamTag($primaryKeyColumn, [$phpType]);
823
                $primaryKeyFilter[] = "'$primaryKeyColumn' => \$$primaryKeyColumn";
824
            }
825
            $parameters[] = new ParameterGenerator($lazyLoadingParameterName, 'bool', false);
826
            $parametersTag[] = new ParamTag($lazyLoadingParameterName, ['bool'], 'If set to true, the object will not be loaded right away. Instead, it will be loaded when you first try to access a method of the object.');
827
            $parametersTag[] = new ReturnTag(['\\'.$beanClassName]);
828
            $parametersTag[] = new ThrowsTag('\\'.TDBMException::class);
829
830
            $getByIdMethod = new MethodGenerator(
831
                'getById',
832
                $parameters,
833
                MethodGenerator::FLAG_PUBLIC,
834
                "return \$this->tdbmService->findObjectByPk('$tableName', [" . implode(', ', $primaryKeyFilter) . "], [], \$$lazyLoadingParameterName);",
835
                (new DocBlockGenerator(
836
                    "Get $beanClassWithoutNameSpace specified by its ID (its primary key).",
837
                    'If the primary key does not exist, an exception is thrown.',
838
                    $parametersTag
839
                ))->setWordWrap(false)
840
            );
841
            $getByIdMethod->setReturnType($beanClassName);
842
            $getByIdMethod = $this->codeGeneratorListener->onBaseDaoGetByIdGenerated($getByIdMethod, $this, $this->configuration, $class);
843
            if ($getByIdMethod) {
844
                $class->addMethodFromGenerator($getByIdMethod);
845
            }
846
        }
847
848
        $deleteMethodBody = <<<EOF
849
if (\$cascade === true) {
850
    \$this->tdbmService->deleteCascade(\$obj);
851
} else {
852
    \$this->tdbmService->delete(\$obj);
853
}
854
EOF;
855
856
857
        $deleteMethod = new MethodGenerator(
858
            'delete',
859
            [
860
                new ParameterGenerator('obj', $beanClassName),
861
                new ParameterGenerator('cascade', 'bool', false)
862
            ],
863
            MethodGenerator::FLAG_PUBLIC,
864
            $deleteMethodBody,
865
            (new DocBlockGenerator(
866
                "Get all $beanClassWithoutNameSpace records.",
867
                null,
868
                [
869
                    new ParamTag('obj', ['\\'.$beanClassName], 'The object to delete'),
870
                    new ParamTag('cascade', ['bool'], 'If true, it will delete all objects linked to $obj'),
871
                ]
872
            ))->setWordWrap(false)
873
        );
874
        $deleteMethod->setReturnType('void');
875
        $deleteMethod = $this->codeGeneratorListener->onBaseDaoDeleteGenerated($deleteMethod, $this, $this->configuration, $class);
876
        if ($deleteMethod !== null) {
877
            $class->addMethodFromGenerator($deleteMethod);
878
        }
879
880
        $findMethodBody = <<<EOF
881
if (\$this->defaultSort && \$orderBy == null) {
882
    \$orderBy = '$tableName.'.\$this->defaultSort.' '.\$this->defaultDirection;
883
}
884
return \$this->tdbmService->findObjects('$tableName', \$filter, \$parameters, \$orderBy, \$additionalTablesFetch, \$mode);
885
EOF;
886
887
888
        $findMethod = new MethodGenerator(
889
            'find',
890
            [
891
                (new ParameterGenerator('filter'))->setDefaultValue(null),
892
                new ParameterGenerator('parameters', 'array', []),
893
                (new ParameterGenerator('orderBy'))->setDefaultValue(null),
894
                new ParameterGenerator('additionalTablesFetch', 'array', []),
895
                (new ParameterGenerator('mode', '?int'))->setDefaultValue(null),
896
            ],
897
            MethodGenerator::FLAG_PROTECTED,
898
            $findMethodBody,
899
            (new DocBlockGenerator(
900
                "Get all $beanClassWithoutNameSpace records.",
901
                null,
902
                [
903
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
904
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
905
                    new ParamTag('orderBy', ['mixed'], 'The order string'),
906
                    new ParamTag('additionalTablesFetch', ['string[]'], 'A list of additional tables to fetch (for performance improvement)'),
907
                    new ParamTag('mode', ['int', 'null'], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.'),
908
                    new ReturnTag(['\\' . $beanClassName . '[]', '\\'.ResultIterator::class])
909
                ]
910
            ))->setWordWrap(false)
911
        );
912
        $findMethod->setReturnType('\\'.ResultIterator::class);
913
        $findMethod = $this->codeGeneratorListener->onBaseDaoFindGenerated($findMethod, $this, $this->configuration, $class);
914
        if ($findMethod !== null) {
915
            $class->addMethodFromGenerator($findMethod);
916
        }
917
918
        $findFromSqlMethodBody = <<<EOF
919
if (\$this->defaultSort && \$orderBy == null) {
920
    \$orderBy = '$tableName.'.\$this->defaultSort.' '.\$this->defaultDirection;
921
}
922
return \$this->tdbmService->findObjectsFromSql('$tableName', \$from, \$filter, \$parameters, \$orderBy, \$mode);
923
EOF;
924
925
        $findFromSqlMethod = new MethodGenerator(
926
            'findFromSql',
927
            [
928
                new ParameterGenerator('from', 'string'),
929
                (new ParameterGenerator('filter'))->setDefaultValue(null),
930
                new ParameterGenerator('parameters', 'array', []),
931
                (new ParameterGenerator('orderBy'))->setDefaultValue(null),
932
                new ParameterGenerator('additionalTablesFetch', 'array', []),
933
                (new ParameterGenerator('mode', '?int'))->setDefaultValue(null),
934
            ],
935
            MethodGenerator::FLAG_PROTECTED,
936
            $findFromSqlMethodBody,
937
            (new DocBlockGenerator(
938
                "Get a list of $beanClassWithoutNameSpace specified by its filters.",
939
                "Unlike the `find` method that guesses the FROM part of the statement, here you can pass the \$from part.
940
941
You should not put an alias on the main table name. So your \$from variable should look like:
942
943
   \"$tableName JOIN ... ON ...\"",
944
                [
945
                    new ParamTag('from', ['string'], 'The sql from statement'),
946
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
947
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
948
                    new ParamTag('orderBy', ['mixed'], 'The order string'),
949
                    new ParamTag('additionalTablesFetch', ['string[]'], 'A list of additional tables to fetch (for performance improvement)'),
950
                    new ParamTag('mode', ['int', 'null'], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.'),
951
                    new ReturnTag(['\\'.$beanClassName . '[]', '\\'.ResultIterator::class])
952
                ]
953
            ))->setWordWrap(false)
954
        );
955
        $findFromSqlMethod->setReturnType('\\'.ResultIterator::class);
956
        $findFromSqlMethod = $this->codeGeneratorListener->onBaseDaoFindFromSqlGenerated($findFromSqlMethod, $this, $this->configuration, $class);
957
        if ($findFromSqlMethod !== null) {
958
            $class->addMethodFromGenerator($findFromSqlMethod);
959
        }
960
961
        $findFromRawSqlMethodBody = <<<EOF
962
return \$this->tdbmService->findObjectsFromRawSql('$tableName', \$sql, \$parameters, \$mode, null, \$countSql);
963
EOF;
964
965
        $findFromRawSqlMethod = new MethodGenerator(
966
            'findFromRawSql',
967
            [
968
                new ParameterGenerator('sql', 'string'),
969
                new ParameterGenerator('parameters', 'array', []),
970
                (new ParameterGenerator('countSql', '?string'))->setDefaultValue(null),
971
                (new ParameterGenerator('mode', '?int'))->setDefaultValue(null),
972
            ],
973
            MethodGenerator::FLAG_PROTECTED,
974
            $findFromRawSqlMethodBody,
975
            (new DocBlockGenerator(
976
                "Get a list of $beanClassWithoutNameSpace from a SQL query.",
977
                "Unlike the `find` and `findFromSql` methods, here you can pass the whole \$sql query.
978
979
You should not put an alias on the main table name, and select its columns using `*`. So the SELECT part of you \$sql should look like:
980
981
   \"SELECT $tableName .* FROM ...\"",
982
                [
983
                    new ParamTag('sql', ['string'], 'The sql query'),
984
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the query'),
985
                    new ParamTag('countSql', ['string', 'null'], 'The sql query that provides total count of rows (automatically computed if not provided)'),
986
                    new ParamTag('mode', ['int', 'null'], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.'),
987
                    new ReturnTag(['\\'.$beanClassName . '[]', '\\'.ResultIterator::class])
988
                ]
989
            ))->setWordWrap(false)
990
        );
991
        $findFromRawSqlMethod->setReturnType('\\'.ResultIterator::class);
992
        $findFromRawSqlMethod = $this->codeGeneratorListener->onBaseDaoFindFromRawSqlGenerated($findFromRawSqlMethod, $this, $this->configuration, $class);
993
        if ($findFromRawSqlMethod !== null) {
994
            $class->addMethodFromGenerator($findFromRawSqlMethod);
995
        }
996
997
        $findOneMethodBody = <<<EOF
998
return \$this->tdbmService->findObject('$tableName', \$filter, \$parameters, \$additionalTablesFetch);
999
EOF;
1000
1001
1002
        $findOneMethod = new MethodGenerator(
1003
            'findOne',
1004
            [
1005
                (new ParameterGenerator('filter'))->setDefaultValue(null),
1006
                new ParameterGenerator('parameters', 'array', []),
1007
                new ParameterGenerator('additionalTablesFetch', 'array', []),
1008
            ],
1009
            MethodGenerator::FLAG_PROTECTED,
1010
            $findOneMethodBody,
1011
            (new DocBlockGenerator(
1012
                "Get a single $beanClassWithoutNameSpace specified by its filters.",
1013
                null,
1014
                [
1015
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
1016
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
1017
                    new ParamTag('additionalTablesFetch', ['string[]'], 'A list of additional tables to fetch (for performance improvement)'),
1018
                    new ReturnTag(['\\'.$beanClassName, 'null'])
1019
                ]
1020
            ))->setWordWrap(false)
1021
        );
1022
        $findOneMethod->setReturnType("?$beanClassName");
1023
        $findOneMethod = $this->codeGeneratorListener->onBaseDaoFindOneGenerated($findOneMethod, $this, $this->configuration, $class);
1024
        if ($findOneMethod !== null) {
1025
            $class->addMethodFromGenerator($findOneMethod);
1026
        }
1027
1028
        $findOneFromSqlMethodBody = <<<EOF
1029
return \$this->tdbmService->findObjectFromSql('$tableName', \$from, \$filter, \$parameters);
1030
EOF;
1031
1032
        $findOneFromSqlMethod = new MethodGenerator(
1033
            'findOneFromSql',
1034
            [
1035
                new ParameterGenerator('from', 'string'),
1036
                (new ParameterGenerator('filter'))->setDefaultValue(null),
1037
                new ParameterGenerator('parameters', 'array', []),
1038
            ],
1039
            MethodGenerator::FLAG_PROTECTED,
1040
            $findOneFromSqlMethodBody,
1041
            new DocBlockGenerator(
1042
                "Get a single $beanClassWithoutNameSpace specified by its filters.",
1043
                "Unlike the `findOne` method that guesses the FROM part of the statement, here you can pass the \$from part.
1044
1045
You should not put an alias on the main table name. So your \$from variable should look like:
1046
1047
    \"$tableName JOIN ... ON ...\"",
1048
                [
1049
                    new ParamTag('from', ['string'], 'The sql from statement'),
1050
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
1051
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
1052
                    new ReturnTag(['\\'.$beanClassName, 'null'])
1053
                ]
1054
            )
1055
        );
1056
        $findOneFromSqlMethod->setReturnType("?$beanClassName");
1057
        $findOneFromSqlMethod = $this->codeGeneratorListener->onBaseDaoFindOneFromSqlGenerated($findOneFromSqlMethod, $this, $this->configuration, $class);
1058
        if ($findOneFromSqlMethod !== null) {
1059
            $class->addMethodFromGenerator($findOneFromSqlMethod);
1060
        }
1061
1062
1063
        $setDefaultSortMethod = new MethodGenerator(
1064
            'setDefaultSort',
1065
            [
1066
                new ParameterGenerator('defaultSort', 'string'),
1067
            ],
1068
            MethodGenerator::FLAG_PUBLIC,
1069
            '$this->defaultSort = $defaultSort;',
1070
            new DocBlockGenerator(
1071
                "Sets the default column for default sorting.",
1072
                null,
1073
                [
1074
                    new ParamTag('defaultSort', ['string']),
1075
                ]
1076
            )
1077
        );
1078
        $setDefaultSortMethod->setReturnType('void');
1079
        $setDefaultSortMethod = $this->codeGeneratorListener->onBaseDaoSetDefaultSortGenerated($setDefaultSortMethod, $this, $this->configuration, $class);
1080
        if ($setDefaultSortMethod !== null) {
1081
            $class->addMethodFromGenerator($setDefaultSortMethod);
1082
        }
1083
1084
        foreach ($findByDaoCodeMethods as $method) {
1085
            $class->addMethodFromGenerator($method);
1086
        }
1087
1088
        $file = $this->codeGeneratorListener->onBaseDaoGenerated($file, $this, $this->configuration);
1089
1090
        return $file;
1091
    }
1092
1093
    /**
1094
     * Tries to find a @defaultSort annotation in one of the columns.
1095
     *
1096
     * @param Table $table
1097
     *
1098
     * @return mixed[] First item: column name, Second item: column order (asc/desc)
1099
     */
1100
    private function getDefaultSortColumnFromAnnotation(Table $table): array
1101
    {
1102
        $defaultSort = null;
1103
        $defaultSortDirection = null;
1104
        foreach ($table->getColumns() as $column) {
1105
            $comments = $column->getComment();
1106
            $matches = [];
1107
            if ($comments !== null && preg_match('/@defaultSort(\((desc|asc)\))*/', $comments, $matches) != 0) {
1108
                $defaultSort = $column->getName();
1109
                if (count($matches) === 3) {
1110
                    $defaultSortDirection = $matches[2];
1111
                } else {
1112
                    $defaultSortDirection = 'ASC';
1113
                }
1114
            }
1115
        }
1116
1117
        return [$defaultSort, $defaultSortDirection];
1118
    }
1119
1120
    /**
1121
     * @param string $beanNamespace
1122
     * @param string $beanClassName
1123
     *
1124
     * @return MethodGenerator[]
1125
     */
1126
    private function generateFindByDaoCode(string $beanNamespace, string $beanClassName, ClassGenerator $class): array
1127
    {
1128
        $methods = [];
1129
        foreach ($this->removeDuplicateIndexes($this->table->getIndexes()) as $index) {
1130
            if (!$index->isPrimary()) {
1131
                $method = $this->generateFindByDaoCodeForIndex($index, $beanNamespace, $beanClassName);
1132
1133
                if ($method !== null) {
1134
                    $method = $this->codeGeneratorListener->onBaseDaoFindByIndexGenerated($method, $index, $this, $this->configuration, $class);
1135
                    if ($method !== null) {
1136
                        $methods[] = $method;
1137
                    }
1138
                }
1139
            }
1140
        }
1141
        usort($methods, static function (MethodGenerator $methodA, MethodGenerator $methodB) {
1142
            return $methodA->getName() <=> $methodB->getName();
1143
        });
1144
1145
        return $methods;
1146
    }
1147
1148
    /**
1149
     * Remove identical indexes (indexes on same columns)
1150
     *
1151
     * @param Index[] $indexes
1152
     * @return Index[]
1153
     */
1154
    private function removeDuplicateIndexes(array $indexes): array
1155
    {
1156
        $indexesByKey = [];
1157
        foreach ($indexes as $index) {
1158
            $key = implode('__`__', $index->getUnquotedColumns());
1159
            // Unique Index have precedence over non unique one
1160
            if (!isset($indexesByKey[$key]) || $index->isUnique()) {
1161
                $indexesByKey[$key] = $index;
1162
            }
1163
        }
1164
1165
        return array_values($indexesByKey);
1166
    }
1167
1168
    /**
1169
     * @param Index  $index
1170
     * @param string $beanNamespace
1171
     * @param string $beanClassName
1172
     *
1173
     * @return MethodGenerator|null
1174
     */
1175
    private function generateFindByDaoCodeForIndex(Index $index, string $beanNamespace, string $beanClassName): ?MethodGenerator
1176
    {
1177
        $columns = $index->getColumns();
1178
        $usedBeans = [];
1179
1180
        /**
1181
         * The list of elements building this index (expressed as columns or foreign keys)
1182
         * @var AbstractBeanPropertyDescriptor[]
1183
         */
1184
        $elements = [];
1185
1186
        foreach ($columns as $column) {
1187
            $fk = $this->isPartOfForeignKey($this->table, $this->table->getColumn($column));
1188
            if ($fk !== null) {
1189
                if (!in_array($fk, $elements)) {
1190
                    $elements[] = new ObjectBeanPropertyDescriptor($this->table, $fk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser);
1191
                }
1192
            } else {
1193
                $elements[] = new ScalarBeanPropertyDescriptor($this->table, $this->table->getColumn($column), $this->namingStrategy, $this->annotationParser);
1194
            }
1195
        }
1196
1197
        // If the index is actually only a foreign key, let's bypass it entirely.
1198
        if (count($elements) === 1 && $elements[0] instanceof ObjectBeanPropertyDescriptor) {
1199
            return null;
1200
        }
1201
1202
        $parameters = [];
1203
        //$functionParameters = [];
1204
        $first = true;
1205
        /** @var AbstractBeanPropertyDescriptor $element */
1206
        foreach ($elements as $element) {
1207
            $parameter = new ParameterGenerator(ltrim($element->getVariableName(), '$'));
1208
            if (!$first && !($element->isCompulsory() && $index->isUnique())) {
1209
                $parameterType = '?';
1210
            //$functionParameter = '?';
1211
            } else {
1212
                $parameterType = '';
1213
                //$functionParameter = '';
1214
            }
1215
            $parameterType .= $element->getPhpType();
1216
            $parameter->setType($parameterType);
1217
            if (!$first && !($element->isCompulsory() && $index->isUnique())) {
1218
                $parameter->setDefaultValue(null);
1219
            }
1220
            //$functionParameter .= $element->getPhpType();
1221
            $elementClassName = $element->getClassName();
1222
            if ($elementClassName) {
1223
                $usedBeans[] = $beanNamespace.'\\'.$elementClassName;
1224
            }
1225
            //$functionParameter .= ' '.$element->getVariableName();
1226
            if ($first) {
1227
                $first = false;
1228
            } /*else {
1229
                $functionParameter .= ' = null';
1230
            }*/
1231
            //$functionParameters[] = $functionParameter;
1232
            $parameters[] = $parameter;
1233
        }
1234
1235
        //$functionParametersString = implode(', ', $functionParameters);
1236
1237
        $count = 0;
1238
1239
        $params = [];
1240
        $filterArrayCode = '';
1241
        $commentArguments = [];
1242
        $first = true;
1243
        foreach ($elements as $element) {
1244
            $params[] = $element->getParamAnnotation();
1245
            if ($element instanceof ScalarBeanPropertyDescriptor) {
1246
                $filterArrayCode .= '            '.var_export($element->getColumnName(), true).' => '.$element->getVariableName().",\n";
1247
            } elseif ($element instanceof ObjectBeanPropertyDescriptor) {
1248
                $foreignKey = $element->getForeignKey();
1249
                $columns = SafeFunctions::arrayCombine($foreignKey->getLocalColumns(), $foreignKey->getForeignColumns());
1250
                ++$count;
1251
                $foreignTable = $this->schema->getTable($foreignKey->getForeignTableName());
1252
                foreach ($columns as $localColumn => $foreignColumn) {
1253
                    // TODO: a foreign key could point to another foreign key. In this case, there is no getter for the pointed column. We don't support this case.
1254
                    $targetedElement = new ScalarBeanPropertyDescriptor($foreignTable, $foreignTable->getColumn($foreignColumn), $this->namingStrategy, $this->annotationParser);
1255
                    if ($first || $element->isCompulsory() && $index->isUnique()) {
1256
                        // First parameter for index is not nullable
1257
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => '.$element->getVariableName().'->'.$targetedElement->getGetterName()."(),\n";
1258
                    } else {
1259
                        // Other parameters for index is not nullable
1260
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => ('.$element->getVariableName().' !== null) ? '.$element->getVariableName().'->'.$targetedElement->getGetterName()."() : null,\n";
1261
                    }
1262
                }
1263
            }
1264
            $commentArguments[] = substr($element->getVariableName(), 1);
1265
            if ($first) {
1266
                $first = false;
1267
            }
1268
        }
1269
1270
        //$paramsString = implode("\n", $params);
1271
1272
1273
        $methodName = $this->namingStrategy->getFindByIndexMethodName($index, $elements);
1274
1275
        $method = new MethodGenerator($methodName);
1276
1277
        if ($index->isUnique()) {
1278
            $parameters[] = new ParameterGenerator('additionalTablesFetch', 'array', []);
1279
            $params[] = new ParamTag('additionalTablesFetch', [ 'string[]' ], 'A list of additional tables to fetch (for performance improvement)');
1280
            $params[] = new ReturnTag([ '\\'.$beanNamespace.'\\'.$beanClassName, 'null' ]);
1281
            $method->setReturnType('?\\'.$beanNamespace.'\\'.$beanClassName);
1282
1283
            $docBlock = new DocBlockGenerator("Get a $beanClassName filtered by ".implode(', ', $commentArguments). '.', null, $params);
1284
            $docBlock->setWordWrap(false);
1285
1286
            $body = "\$filter = [
1287
".$filterArrayCode."        ];
1288
return \$this->findOne(\$filter, [], \$additionalTablesFetch);
1289
";
1290
        } else {
1291
            $parameters[] = (new ParameterGenerator('orderBy'))->setDefaultValue(null);
1292
            $params[] = new ParamTag('orderBy', [ 'mixed' ], 'The order string');
1293
            $parameters[] = new ParameterGenerator('additionalTablesFetch', 'array', []);
1294
            $params[] = new ParamTag('additionalTablesFetch', [ 'string[]' ], 'A list of additional tables to fetch (for performance improvement)');
1295
            $parameters[] = (new ParameterGenerator('mode', '?int'))->setDefaultValue(null);
1296
            $params[] = new ParamTag('mode', [ 'int', 'null' ], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.');
1297
            $params[] = new ReturnTag([ '\\'.$beanNamespace.'\\'.$beanClassName.'[]', '\\'.ResultIterator::class ]);
1298
            $method->setReturnType('\\'.ResultIterator::class);
1299
1300
            $docBlock = new DocBlockGenerator("Get a list of $beanClassName filtered by ".implode(', ', $commentArguments).".", null, $params);
1301
            $docBlock->setWordWrap(false);
1302
1303
            $body = "\$filter = [
1304
".$filterArrayCode."        ];
1305
return \$this->find(\$filter, [], \$orderBy, \$additionalTablesFetch, \$mode);
1306
";
1307
        }
1308
1309
        $method->setParameters($parameters);
1310
        $method->setDocBlock($docBlock);
1311
        $method->setBody($body);
1312
1313
        return $method;
1314
    }
1315
1316
    /**
1317
     * Generates the code for the getUsedTable protected method.
1318
     *
1319
     * @return MethodGenerator
1320
     */
1321
    private function generateGetUsedTablesCode(): MethodGenerator
1322
    {
1323
        $hasParentRelationship = $this->schemaAnalyzer->getParentRelationship($this->table->getName()) !== null;
1324
        if ($hasParentRelationship) {
1325
            $code = sprintf('$tables = parent::getUsedTables();
1326
$tables[] = %s;
1327
1328
return $tables;', var_export($this->table->getName(), true));
1329
        } else {
1330
            $code = sprintf('        return [ %s ];', var_export($this->table->getName(), true));
1331
        }
1332
1333
        $method = new MethodGenerator('getUsedTables');
1334
        $method->setDocBlock('Returns an array of used tables by this bean (from parent to child relationship).');
1335
        $method->getDocBlock()->setTag(new ReturnTag(['string[]']));
1336
        $method->setReturnType('array');
1337
        $method->setBody($code);
1338
1339
        return $method;
1340
    }
1341
1342
    private function generateOnDeleteCode(): ?MethodGenerator
1343
    {
1344
        $code = '';
1345
        $relationships = $this->getPropertiesForTable($this->table);
1346
        foreach ($relationships as $relationship) {
1347
            if ($relationship instanceof ObjectBeanPropertyDescriptor) {
1348
                $tdbmFk = ForeignKey::createFromFk($relationship->getForeignKey());
1349
                $code .= '$this->setRef('.var_export($tdbmFk->getCacheKey(), true).', null, '.var_export($this->table->getName(), true).");\n";
1350
            }
1351
        }
1352
1353
        if (!$code) {
1354
            return null;
1355
        }
1356
1357
        $method = new MethodGenerator('onDelete');
1358
        $method->setDocBlock('Method called when the bean is removed from database.');
1359
        $method->setReturnType('void');
1360
        $method->setBody('parent::onDelete();
1361
'.$code);
1362
1363
        return $method;
1364
    }
1365
1366
    /**
1367
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1368
     * @return MethodGenerator
1369
     */
1370
    private function generateGetManyToManyRelationshipDescriptorCode(array $pivotTableMethodsDescriptors): ?MethodGenerator
1371
    {
1372
        if (empty($pivotTableMethodsDescriptors)) {
1373
            return null;
1374
        }
1375
1376
        $method = new MethodGenerator('_getManyToManyRelationshipDescriptor');
1377
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PUBLIC);
1378
        $method->setDocBlock('Get the paths used for many to many relationships methods.');
1379
        $method->getDocBlock()->setTag(new GenericTag('internal'));
1380
        $method->setReturnType(ManyToManyRelationshipPathDescriptor::class);
1381
1382
        $parameter = new ParameterGenerator('pathKey');
1383
        $parameter->setType('string');
1384
        $method->setParameter($parameter);
1385
1386
        $code = 'switch ($pathKey) {'."\n";
1387
        foreach ($pivotTableMethodsDescriptors as $pivotTableMethodsDescriptor) {
1388
            $code .= '    case '.var_export($pivotTableMethodsDescriptor->getManyToManyRelationshipKey(), true).":\n";
1389
            $code .= '        return '.$pivotTableMethodsDescriptor->getManyToManyRelationshipInstantiationCode().";\n";
1390
        }
1391
        $code .= "    default:\n";
1392
        $code .= "        return parent::_getManyToManyRelationshipDescriptor(\$pathKey);\n";
1393
        $code .= "}\n";
1394
1395
        $method->setBody($code);
1396
1397
        return $method;
1398
    }
1399
1400
    /**
1401
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1402
     * @return MethodGenerator
1403
     */
1404
    private function generateGetManyToManyRelationshipDescriptorKeysCode(array $pivotTableMethodsDescriptors): ?MethodGenerator
1405
    {
1406
        if (empty($pivotTableMethodsDescriptors)) {
1407
            return null;
1408
        }
1409
1410
        $method = new MethodGenerator('_getManyToManyRelationshipDescriptorKeys');
1411
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PUBLIC);
1412
        $method->setReturnType('array');
1413
        $method->setDocBlock('Returns the list of keys supported for many to many relationships');
1414
        $method->getDocBlock()->setTag(new GenericTag('internal'));
1415
        $method->getDocBlock()->setTag(new ReturnTag('string[]'));
1416
1417
        $keys = [];
1418
        foreach ($pivotTableMethodsDescriptors as $pivotTableMethodsDescriptor) {
1419
            $keys[] = var_export($pivotTableMethodsDescriptor->getManyToManyRelationshipKey(), true);
1420
        }
1421
1422
        $code = 'return array_merge(parent::_getManyToManyRelationshipDescriptorKeys(), ['.implode(', ', $keys).']);';
1423
1424
        $method->setBody($code);
1425
1426
        return $method;
1427
    }
1428
1429
    private function generateCloneCode(): MethodGenerator
1430
    {
1431
        $precode = '';
1432
        $postcode = '';
1433
1434
        foreach ($this->beanPropertyDescriptors as $beanPropertyDescriptor) {
1435
            $postcode .= $beanPropertyDescriptor->getCloneRule();
1436
        }
1437
1438
        //cloning many to many relationships
1439
        foreach ($this->getPivotTableDescriptors() as $beanMethodDescriptor) {
1440
            $precode .= $beanMethodDescriptor->getCloneRule()."\n";
1441
        }
1442
1443
        $method = new MethodGenerator('__clone');
1444
        $method->setBody($precode."parent::__clone();\n".$postcode);
1445
1446
        return $method;
1447
    }
1448
1449
    /**
1450
     * Returns the bean class name (without the namespace).
1451
     *
1452
     * @return string
1453
     */
1454
    public function getBeanClassName() : string
1455
    {
1456
        return $this->namingStrategy->getBeanClassName($this->table->getName());
1457
    }
1458
1459
    /**
1460
     * Returns the base bean class name (without the namespace).
1461
     *
1462
     * @return string
1463
     */
1464
    public function getBaseBeanClassName() : string
1465
    {
1466
        return $this->namingStrategy->getBaseBeanClassName($this->table->getName());
1467
    }
1468
1469
    /**
1470
     * Returns the DAO class name (without the namespace).
1471
     *
1472
     * @return string
1473
     */
1474
    public function getDaoClassName() : string
1475
    {
1476
        return $this->namingStrategy->getDaoClassName($this->table->getName());
1477
    }
1478
1479
    /**
1480
     * Returns the base DAO class name (without the namespace).
1481
     *
1482
     * @return string
1483
     */
1484
    public function getBaseDaoClassName() : string
1485
    {
1486
        return $this->namingStrategy->getBaseDaoClassName($this->table->getName());
1487
    }
1488
1489
    /**
1490
     * Returns the table used to build this bean.
1491
     *
1492
     * @return Table
1493
     */
1494
    public function getTable(): Table
1495
    {
1496
        return $this->table;
1497
    }
1498
1499
    /**
1500
     * Returns the extended bean class name (without the namespace), or null if the bean is not extended.
1501
     *
1502
     * @return string
1503
     */
1504
    public function getExtendedBeanClassName(): ?string
1505
    {
1506
        $parentFk = $this->schemaAnalyzer->getParentRelationship($this->table->getName());
1507
        if ($parentFk !== null) {
1508
            return $this->namingStrategy->getBeanClassName($parentFk->getForeignTableName());
1509
        } else {
1510
            return null;
1511
        }
1512
    }
1513
1514
    /**
1515
     * @return string
1516
     */
1517
    public function getBeanNamespace(): string
1518
    {
1519
        return $this->beanNamespace;
1520
    }
1521
1522
    /**
1523
     * @return string
1524
     */
1525
    public function getGeneratedBeanNamespace(): string
1526
    {
1527
        return $this->generatedBeanNamespace;
1528
    }
1529
1530
    /**
1531
     * @param ForeignKeyConstraint[] $fks
1532
     */
1533
    private function generateGetForeignKeys(array $fks): MethodGenerator
1534
    {
1535
        $fkArray = [];
1536
1537
        foreach ($fks as $fk) {
1538
            $tdbmFk = ForeignKey::createFromFk($fk);
1539
            $fkArray[$tdbmFk->getCacheKey()] = [
1540
                ForeignKey::FOREIGN_TABLE => $fk->getForeignTableName(),
1541
                ForeignKey::LOCAL_COLUMNS => $fk->getUnquotedLocalColumns(),
1542
                ForeignKey::FOREIGN_COLUMNS => $fk->getUnquotedForeignColumns(),
1543
            ];
1544
        }
1545
1546
        ksort($fkArray);
1547
        foreach ($fkArray as $tableFks) {
1548
            ksort($tableFks);
1549
        }
1550
1551
        $code = <<<EOF
1552
if (\$tableName === %s) {
1553
    if (self::\$foreignKeys === null) {
1554
        self::\$foreignKeys = new ForeignKeys(%s);
1555
    }
1556
    return self::\$foreignKeys;
1557
}
1558
return parent::getForeignKeys(\$tableName);
1559
EOF;
1560
        $code = sprintf($code, var_export($this->getTable()->getName(), true), $this->psr2VarExport($fkArray, '    '));
1561
1562
        $method = new MethodGenerator('getForeignKeys');
1563
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
1564
        $method->setStatic(true);
1565
        $method->setDocBlock('Internal method used to retrieve the list of foreign keys attached to this bean.');
1566
        $method->setReturnType(ForeignKeys::class);
1567
1568
        $parameter = new ParameterGenerator('tableName');
1569
        $parameter->setType('string');
1570
        $method->setParameter($parameter);
1571
1572
1573
        $method->setBody($code);
1574
1575
        return $method;
1576
    }
1577
1578
    /**
1579
     * @param mixed $var
1580
     * @param string $indent
1581
     * @return string
1582
     */
1583
    private function psr2VarExport($var, string $indent=''): string
1584
    {
1585
        if (is_array($var)) {
1586
            $indexed = array_keys($var) === range(0, count($var) - 1);
1587
            $r = [];
1588
            foreach ($var as $key => $value) {
1589
                $r[] = "$indent    "
1590
                    . ($indexed ? '' : $this->psr2VarExport($key) . ' => ')
1591
                    . $this->psr2VarExport($value, "$indent    ");
1592
            }
1593
            return "[\n" . implode(",\n", $r) . "\n" . $indent . ']';
1594
        }
1595
        return var_export($var, true);
1596
    }
1597
}
1598