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

BeanDescriptor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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