Passed
Pull Request — master (#181)
by
unknown
05:45
created

getExtendedResultIteratorClassName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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