Passed
Pull Request — master (#251)
by
unknown
03:11
created

BeanDescriptor::psr2VarExport()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 13
rs 9.9666
c 0
b 0
f 0
cc 4
nc 4
nop 2
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;
0 ignored issues
show
Bug introduced by
The type PhpParser\Comment\Doc was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

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