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

generateGetManyToManyRelationshipDescriptorKeysCode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 23
rs 9.7998
cc 3
nc 3
nop 1
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM\Utils;
5
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\Index;
8
use Doctrine\DBAL\Schema\Schema;
9
use Doctrine\DBAL\Schema\Table;
10
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
11
use JsonSerializable;
12
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
13
use PhpParser\Comment\Doc;
14
use Ramsey\Uuid\Uuid;
15
use TheCodingMachine\TDBM\AbstractTDBMObject;
16
use TheCodingMachine\TDBM\AlterableResultIterator;
17
use TheCodingMachine\TDBM\ConfigurationInterface;
18
use TheCodingMachine\TDBM\ResultIterator;
19
use TheCodingMachine\TDBM\SafeFunctions;
20
use TheCodingMachine\TDBM\Schema\ForeignKey;
21
use TheCodingMachine\TDBM\Schema\ForeignKeys;
22
use TheCodingMachine\TDBM\TDBMException;
23
use TheCodingMachine\TDBM\TDBMSchemaAnalyzer;
24
use TheCodingMachine\TDBM\TDBMService;
25
use TheCodingMachine\TDBM\Utils\Annotation\AbstractTraitAnnotation;
26
use TheCodingMachine\TDBM\Utils\Annotation\AddInterfaceOnDao;
27
use TheCodingMachine\TDBM\Utils\Annotation\AddTrait;
28
use TheCodingMachine\TDBM\Utils\Annotation\AddTraitOnDao;
29
use TheCodingMachine\TDBM\Utils\Annotation\AnnotationParser;
30
use TheCodingMachine\TDBM\Utils\Annotation\AddInterface;
31
use Zend\Code\Generator\AbstractMemberGenerator;
32
use Zend\Code\Generator\ClassGenerator;
33
use Zend\Code\Generator\DocBlock\Tag;
34
use Zend\Code\Generator\DocBlock\Tag\GenericTag;
35
use Zend\Code\Generator\DocBlock\Tag\ParamTag;
36
use Zend\Code\Generator\DocBlock\Tag\ReturnTag;
37
use Zend\Code\Generator\DocBlock\Tag\ThrowsTag;
38
use Zend\Code\Generator\DocBlock\Tag\VarTag;
39
use Zend\Code\Generator\DocBlockGenerator;
40
use Zend\Code\Generator\FileGenerator;
41
use Zend\Code\Generator\MethodGenerator;
42
use Zend\Code\Generator\ParameterGenerator;
43
use Zend\Code\Generator\PropertyGenerator;
44
use function implode;
45
use function var_export;
46
47
/**
48
 * This class represents a bean.
49
 */
50
class BeanDescriptor implements BeanDescriptorInterface
51
{
52
    /**
53
     * @var Table
54
     */
55
    private $table;
56
57
    /**
58
     * @var SchemaAnalyzer
59
     */
60
    private $schemaAnalyzer;
61
62
    /**
63
     * @var Schema
64
     */
65
    private $schema;
66
67
    /**
68
     * @var AbstractBeanPropertyDescriptor[]
69
     */
70
    private $beanPropertyDescriptors = [];
71
72
    /**
73
     * @var TDBMSchemaAnalyzer
74
     */
75
    private $tdbmSchemaAnalyzer;
76
77
    /**
78
     * @var NamingStrategyInterface
79
     */
80
    private $namingStrategy;
81
    /**
82
     * @var string
83
     */
84
    private $beanNamespace;
85
    /**
86
     * @var string
87
     */
88
    private $generatedBeanNamespace;
89
    /**
90
     * @var AnnotationParser
91
     */
92
    private $annotationParser;
93
    /**
94
     * @var string
95
     */
96
    private $daoNamespace;
97
    /**
98
     * @var string
99
     */
100
    private $generatedDaoNamespace;
101
    /**
102
     * @var CodeGeneratorListenerInterface
103
     */
104
    private $codeGeneratorListener;
105
    /**
106
     * @var ConfigurationInterface
107
     */
108
    private $configuration;
109
110
    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