Passed
Pull Request — master (#243)
by
unknown
04:32
created

BeanDescriptor::generateExtendsAndUseStatements()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1331
                if ($typeName === /** @scrutinizer ignore-deprecated */ Type::DATETIME_IMMUTABLE) {

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1332
                    $filterArrayCode .= sprintf(
1333
                        "            %s => \$this->tdbmService->getConnection()->convertToDatabaseValue(%s, %s),\n",
1334
                        var_export($element->getColumnName(), true),
1335
                        $element->getSafeVariableName(),
1336
                        var_export($typeName, true)
1337
                    );
1338
                } else {
1339
                    $filterArrayCode .= '            '.var_export($element->getColumnName(), true).' => '.$element->getSafeVariableName().",\n";
1340
                }
1341
            } elseif ($element instanceof ObjectBeanPropertyDescriptor) {
1342
                $foreignKey = $element->getForeignKey();
1343
                $columns = SafeFunctions::arrayCombine($foreignKey->getUnquotedLocalColumns(), $foreignKey->getUnquotedForeignColumns());
1344
                $foreignTable = $this->schema->getTable($foreignKey->getForeignTableName());
1345
                foreach ($columns as $localColumn => $foreignColumn) {
1346
                    // 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.
1347
                    $targetedElement = new ScalarBeanPropertyDescriptor($foreignTable, $foreignTable->getColumn($foreignColumn), $this->namingStrategy, $this->annotationParser);
1348
                    if ($first || ($element->isCompulsory() && $index->isUnique())) {
1349
                        // First parameter for index is not nullable
1350
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => '.$element->getSafeVariableName().'->'.$targetedElement->getGetterName()."(),\n";
1351
                    } else {
1352
                        // Other parameters for index is not nullable
1353
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => ('.$element->getSafeVariableName().' !== null) ? '.$element->getSafeVariableName().'->'.$targetedElement->getGetterName()."() : null,\n";
1354
                    }
1355
                }
1356
            }
1357
            $commentArguments[] = substr($element->getSafeVariableName(), 1);
1358
            if ($first) {
1359
                $first = false;
1360
            }
1361
        }
1362
1363
        $methodName = $this->namingStrategy->getFindByIndexMethodName($index, $elements);
1364
1365
        $method = new MethodGenerator($methodName);
1366
1367
        if ($index->isUnique()) {
1368
            $parameters[] = new ParameterGenerator('additionalTablesFetch', 'array', []);
1369
            $params[] = new ParamTag('additionalTablesFetch', [ 'string[]' ], 'A list of additional tables to fetch (for performance improvement)');
1370
            $params[] = new ReturnTag([ '\\'.$beanNamespace.'\\'.$beanClassName, 'null' ]);
1371
            $method->setReturnType('?\\'.$beanNamespace.'\\'.$beanClassName);
1372
1373
            $docBlock = new DocBlockGenerator("Get a $beanClassName filtered by ".implode(', ', $commentArguments). '.', null, $params);
1374
            $docBlock->setWordWrap(false);
1375
1376
            $body = "\$filter = [
1377
".$filterArrayCode."        ];
1378
return \$this->findOne(\$filter, [], \$additionalTablesFetch);
1379
";
1380
        } else {
1381
            $parameters[] = (new ParameterGenerator('orderBy'))->setDefaultValue(null);
1382
            $params[] = new ParamTag('orderBy', [ 'mixed' ], 'The order string');
1383
            $parameters[] = new ParameterGenerator('additionalTablesFetch', 'array', []);
1384
            $params[] = new ParamTag('additionalTablesFetch', [ 'string[]' ], 'A list of additional tables to fetch (for performance improvement)');
1385
            $parameters[] = (new ParameterGenerator('mode', '?int'))->setDefaultValue(null);
1386
            $params[] = new ParamTag('mode', [ 'int', 'null' ], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.');
1387
            $method->setReturnType($this->resultIteratorNamespace . '\\' . $this->getResultIteratorClassName());
1388
1389
            $docBlock = new DocBlockGenerator("Get a list of $beanClassName filtered by ".implode(', ', $commentArguments).".", null, $params);
1390
            $docBlock->setWordWrap(false);
1391
1392
            $body = "\$filter = [
1393
".$filterArrayCode."        ];
1394
return \$this->find(\$filter, [], \$orderBy, \$additionalTablesFetch, \$mode);
1395
";
1396
        }
1397
1398
        $method->setParameters($parameters);
1399
        $method->setDocBlock($docBlock);
1400
        $method->setBody($body);
1401
1402
        return $method;
1403
    }
1404
1405
    /**
1406
     * Generates the code for the getUsedTable protected method.
1407
     *
1408
     * @return MethodGenerator
1409
     */
1410
    private function generateGetUsedTablesCode(): MethodGenerator
1411
    {
1412
        $hasParentRelationship = $this->schemaAnalyzer->getParentRelationship($this->table->getName()) !== null;
1413
        if ($hasParentRelationship) {
1414
            $code = sprintf('$tables = parent::getUsedTables();
1415
$tables[] = %s;
1416
1417
return $tables;', var_export($this->table->getName(), true));
1418
        } else {
1419
            $code = sprintf('        return [ %s ];', var_export($this->table->getName(), true));
1420
        }
1421
1422
        $method = new MethodGenerator('getUsedTables');
1423
        $method->setDocBlock(new DocBlockGenerator(
1424
            'Returns an array of used tables by this bean (from parent to child relationship).',
1425
            null,
1426
            [new ReturnTag(['string[]'])]
1427
        ));
1428
        $method->setReturnType('array');
1429
        $method->setBody($code);
1430
1431
        return $method;
1432
    }
1433
1434
    private function generateOnDeleteCode(): ?MethodGenerator
1435
    {
1436
        $code = '';
1437
        $relationships = $this->getPropertiesForTable($this->table);
1438
        foreach ($relationships as $relationship) {
1439
            if ($relationship instanceof ObjectBeanPropertyDescriptor) {
1440
                $tdbmFk = ForeignKey::createFromFk($relationship->getForeignKey());
1441
                $code .= sprintf(
1442
                    "\$this->setRef(%s, null, %s);\n",
1443
                    var_export($tdbmFk->getCacheKey(), true),
1444
                    var_export($this->table->getName(), true)
1445
                );
1446
            }
1447
        }
1448
1449
        if (!$code) {
1450
            return null;
1451
        }
1452
1453
        $method = new MethodGenerator('onDelete');
1454
        $method->setDocBlock(new DocBlockGenerator('Method called when the bean is removed from database.'));
1455
        $method->setReturnType('void');
1456
        $method->setBody('parent::onDelete();
1457
'.$code);
1458
1459
        return $method;
1460
    }
1461
1462
    /**
1463
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1464
     * @return MethodGenerator
1465
     */
1466
    private function generateGetManyToManyRelationshipDescriptorCode(array $pivotTableMethodsDescriptors): ?MethodGenerator
1467
    {
1468
        if (empty($pivotTableMethodsDescriptors)) {
1469
            return null;
1470
        }
1471
1472
        $method = new MethodGenerator('_getManyToManyRelationshipDescriptor');
1473
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PUBLIC);
1474
        $method->setDocBlock(new DocBlockGenerator(
1475
            'Get the paths used for many to many relationships methods.',
1476
            null,
1477
            [new GenericTag('internal')]
1478
        ));
1479
        $method->setReturnType(ManyToManyRelationshipPathDescriptor::class);
1480
1481
        $parameter = new ParameterGenerator('pathKey');
1482
        $parameter->setType('string');
1483
        $method->setParameter($parameter);
1484
1485
        $code = 'switch ($pathKey) {'."\n";
1486
        foreach ($pivotTableMethodsDescriptors as $pivotTableMethodsDescriptor) {
1487
            $code .= '    case '.var_export($pivotTableMethodsDescriptor->getManyToManyRelationshipKey(), true).":\n";
1488
            $code .= '        return '.$pivotTableMethodsDescriptor->getManyToManyRelationshipInstantiationCode().";\n";
1489
        }
1490
        $code .= "    default:\n";
1491
        $code .= "        return parent::_getManyToManyRelationshipDescriptor(\$pathKey);\n";
1492
        $code .= "}\n";
1493
1494
        $method->setBody($code);
1495
1496
        return $method;
1497
    }
1498
1499
    /**
1500
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1501
     * @return MethodGenerator
1502
     */
1503
    private function generateGetManyToManyRelationshipDescriptorKeysCode(array $pivotTableMethodsDescriptors): ?MethodGenerator
1504
    {
1505
        if (empty($pivotTableMethodsDescriptors)) {
1506
            return null;
1507
        }
1508
1509
        $method = new MethodGenerator('_getManyToManyRelationshipDescriptorKeys');
1510
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PUBLIC);
1511
        $method->setReturnType('array');
1512
        $method->setDocBlock(new DocBlockGenerator(
1513
            'Returns the list of keys supported for many to many relationships',
1514
            null,
1515
            [new GenericTag('internal'), new ReturnTag('string[]')]
1516
        ));
1517
1518
        $keys = [];
1519
        foreach ($pivotTableMethodsDescriptors as $pivotTableMethodsDescriptor) {
1520
            $keys[] = var_export($pivotTableMethodsDescriptor->getManyToManyRelationshipKey(), true);
1521
        }
1522
1523
        $code = 'return array_merge(parent::_getManyToManyRelationshipDescriptorKeys(), ['.implode(', ', $keys).']);';
1524
1525
        $method->setBody($code);
1526
1527
        return $method;
1528
    }
1529
1530
    /**
1531
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1532
     * @return MethodGenerator
1533
     */
1534
    private function generateCloneCode(array $pivotTableMethodsDescriptors): MethodGenerator
1535
    {
1536
        $precode = '';
1537
        $postcode = '';
1538
1539
        foreach ($this->beanPropertyDescriptors as $beanPropertyDescriptor) {
1540
            $postcode .= $beanPropertyDescriptor->getCloneRule();
1541
        }
1542
1543
        //cloning many to many relationships
1544
        foreach ($pivotTableMethodsDescriptors as $beanMethodDescriptor) {
1545
            $precode .= $beanMethodDescriptor->getCloneRule()."\n";
1546
        }
1547
1548
        $method = new MethodGenerator('__clone');
1549
        $method->setBody($precode."parent::__clone();\n".$postcode);
1550
1551
        return $method;
1552
    }
1553
1554
    /**
1555
     * Returns the bean class name (without the namespace).
1556
     *
1557
     * @return string
1558
     */
1559
    public function getBeanClassName() : string
1560
    {
1561
        return $this->namingStrategy->getBeanClassName($this->table->getName());
1562
    }
1563
1564
    /**
1565
     * Returns the base bean class name (without the namespace).
1566
     *
1567
     * @return string
1568
     */
1569
    public function getBaseBeanClassName() : string
1570
    {
1571
        return $this->namingStrategy->getBaseBeanClassName($this->table->getName());
1572
    }
1573
1574
    /**
1575
     * Returns the DAO class name (without the namespace).
1576
     *
1577
     * @return string
1578
     */
1579
    public function getDaoClassName() : string
1580
    {
1581
        return $this->namingStrategy->getDaoClassName($this->table->getName());
1582
    }
1583
1584
    /**
1585
     * Returns the base DAO class name (without the namespace).
1586
     *
1587
     * @return string
1588
     */
1589
    public function getBaseDaoClassName() : string
1590
    {
1591
        return $this->namingStrategy->getBaseDaoClassName($this->table->getName());
1592
    }
1593
1594
    /**
1595
     * Returns the ResultIterator class name (without the namespace).
1596
     *
1597
     * @return string
1598
     */
1599
    public function getResultIteratorClassName() : string
1600
    {
1601
        return $this->namingStrategy->getResultIteratorClassName($this->table->getName());
1602
    }
1603
1604
    /**
1605
     * Returns the base ResultIterator class name (without the namespace).
1606
     *
1607
     * @return string
1608
     */
1609
    public function getBaseResultIteratorClassName() : string
1610
    {
1611
        return $this->namingStrategy->getBaseResultIteratorClassName($this->table->getName());
1612
    }
1613
1614
    /**
1615
     * Returns the table used to build this bean.
1616
     *
1617
     * @return Table
1618
     */
1619
    public function getTable(): Table
1620
    {
1621
        return $this->table;
1622
    }
1623
1624
    /**
1625
     * Returns the extended bean class name (without the namespace), or null if the bean is not extended.
1626
     *
1627
     * @return string
1628
     */
1629
    public function getExtendedBeanClassName(): ?string
1630
    {
1631
        $parentFk = $this->schemaAnalyzer->getParentRelationship($this->table->getName());
1632
        if ($parentFk !== null) {
1633
            return $this->namingStrategy->getBeanClassName($parentFk->getForeignTableName());
1634
        } else {
1635
            return null;
1636
        }
1637
    }
1638
1639
    /**
1640
     * @return string
1641
     */
1642
    public function getBeanNamespace(): string
1643
    {
1644
        return $this->beanNamespace;
1645
    }
1646
1647
    /**
1648
     * @return string
1649
     */
1650
    public function getGeneratedBeanNamespace(): string
1651
    {
1652
        return $this->generatedBeanNamespace;
1653
    }
1654
1655
    /**
1656
     * @param ForeignKeyConstraint[] $fks
1657
     */
1658
    private function generateGetForeignKeys(array $fks): MethodGenerator
1659
    {
1660
        $fkArray = [];
1661
1662
        foreach ($fks as $fk) {
1663
            $tdbmFk = ForeignKey::createFromFk($fk);
1664
1665
            // Override column name in case of inheritance
1666
            $foreignTableName = $fk->getForeignTableName();
1667
            $foreignColumns = $fk->getUnquotedForeignColumns();
1668
            foreach ($foreignColumns as $key => $foreignColumn) {
1669
                $descriptor = $this->findScalarPropertyDescriptorInTable($foreignTableName, $foreignColumn);
1670
                if ($descriptor instanceof InheritanceReferencePropertyDescriptor) {
1671
                    $foreignColumns[$key] = $this->foreignColumnNameInInheritance($descriptor, $foreignColumn);
1672
                }
1673
            }
1674
1675
            $fkArray[$tdbmFk->getCacheKey()] = [
1676
                ForeignKey::FOREIGN_TABLE => $fk->getForeignTableName(),
1677
                ForeignKey::LOCAL_COLUMNS => $fk->getUnquotedLocalColumns(),
1678
                ForeignKey::FOREIGN_COLUMNS => $foreignColumns,
1679
            ];
1680
        }
1681
1682
        ksort($fkArray);
1683
        foreach ($fkArray as $tableFks) {
1684
            ksort($tableFks);
1685
        }
1686
1687
        $code = <<<EOF
1688
if (\$tableName === %s) {
1689
    if (self::\$foreignKeys === null) {
1690
        self::\$foreignKeys = new ForeignKeys(%s);
1691
    }
1692
    return self::\$foreignKeys;
1693
}
1694
return parent::getForeignKeys(\$tableName);
1695
EOF;
1696
        $code = sprintf($code, var_export($this->getTable()->getName(), true), $this->psr2VarExport($fkArray, '        '));
1697
1698
        $method = new MethodGenerator('getForeignKeys');
1699
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
1700
        $method->setStatic(true);
1701
        $method->setDocBlock(new DocBlockGenerator('Internal method used to retrieve the list of foreign keys attached to this bean.'));
1702
        $method->setReturnType(ForeignKeys::class);
1703
1704
        $parameter = new ParameterGenerator('tableName');
1705
        $parameter->setType('string');
1706
        $method->setParameter($parameter);
1707
1708
1709
        $method->setBody($code);
1710
1711
        return $method;
1712
    }
1713
1714
    private function findScalarPropertyDescriptorInTable(string $tableName, string $columnName): ?ScalarBeanPropertyDescriptor
1715
    {
1716
        $beanDescriptor = $this->registry->getBeanForTableName($tableName);
1717
        foreach ($beanDescriptor->getBeanPropertyDescriptors() as $descriptor) {
1718
            if ($descriptor instanceof ScalarBeanPropertyDescriptor && $descriptor->getColumnName() === $columnName) {
1719
                return $descriptor;
1720
            }
1721
        }
1722
        return null;
1723
    }
1724
1725
    /**
1726
     * Extract the foreign column name from a InheritanceReferencePropertyDescriptor
1727
     */
1728
    private function foreignColumnNameInInheritance(InheritanceReferencePropertyDescriptor $descriptor, string $column): string
1729
    {
1730
        $nonReferenceDescriptor = $descriptor->getNonScalarReferencedPropertyDescriptor();
1731
        if ($nonReferenceDescriptor instanceof ScalarBeanPropertyDescriptor) {
1732
            return $nonReferenceDescriptor->getColumnName();
1733
        }
1734
        if ($nonReferenceDescriptor instanceof ObjectBeanPropertyDescriptor) {
0 ignored issues
show
introduced by
$nonReferenceDescriptor is always a sub-type of TheCodingMachine\TDBM\Ut...tBeanPropertyDescriptor.
Loading history...
1735
            $foreignKey = $nonReferenceDescriptor->getForeignKey();
1736
            $localColumns = $foreignKey->getLocalColumns();
1737
            $foreignColumns = $foreignKey->getForeignColumns();
1738
            foreach ($localColumns as $key => $localColumn) {
1739
                if ($localColumn === $column) {
1740
                    return $foreignColumns[$key];
1741
                }
1742
            }
1743
        }
1744
        return $column;
1745
    }
1746
1747
    /**
1748
     * @param mixed $var
1749
     * @param string $indent
1750
     * @return string
1751
     */
1752
    private function psr2VarExport($var, string $indent=''): string
1753
    {
1754
        if (is_array($var)) {
1755
            $indexed = array_keys($var) === range(0, count($var) - 1);
1756
            $r = [];
1757
            foreach ($var as $key => $value) {
1758
                $r[] = "$indent    "
1759
                    . ($indexed ? '' : $this->psr2VarExport($key) . ' => ')
1760
                    . $this->psr2VarExport($value, "$indent    ");
1761
            }
1762
            return "[\n" . implode(",\n", $r) . "\n" . $indent . ']';
1763
        }
1764
        return var_export($var, true);
1765
    }
1766
}
1767