Completed
Push — 5.1 ( 686ab3 )
by David
25s queued 22s
created

BeanDescriptor::checkForDuplicate()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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