Passed
Pull Request — 5.1 (#190)
by
unknown
08:39
created

BeanDescriptor::getProperties()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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