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