Passed
Pull Request — master (#160)
by ARP
03:36
created

BeanDescriptor::checkForDuplicate()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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