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

BeanDescriptor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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