Passed
Pull Request — master (#127)
by
unknown
06:05
created

BeanDescriptor::generateGetForeignKeys()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 43
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

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