Passed
Pull Request — master (#116)
by David
03:17
created

BeanDescriptor::getExposedProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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 Ramsey\Uuid\Uuid;
14
use TheCodingMachine\TDBM\AbstractTDBMObject;
15
use TheCodingMachine\TDBM\AlterableResultIterator;
16
use TheCodingMachine\TDBM\ResultIterator;
17
use TheCodingMachine\TDBM\SafeFunctions;
18
use TheCodingMachine\TDBM\TDBMException;
19
use TheCodingMachine\TDBM\TDBMSchemaAnalyzer;
20
use TheCodingMachine\TDBM\Utils\Annotation\AnnotationParser;
21
use Zend\Code\Generator\ClassGenerator;
22
use Zend\Code\Generator\DocBlock\Tag\ParamTag;
23
use Zend\Code\Generator\DocBlock\Tag\ReturnTag;
24
use Zend\Code\Generator\DocBlockGenerator;
25
use Zend\Code\Generator\FileGenerator;
26
use Zend\Code\Generator\MethodGenerator;
27
use Zend\Code\Generator\ParameterGenerator;
28
29
/**
30
 * This class represents a bean.
31
 */
32
class BeanDescriptor implements BeanDescriptorInterface
33
{
34
    /**
35
     * @var Table
36
     */
37
    private $table;
38
39
    /**
40
     * @var SchemaAnalyzer
41
     */
42
    private $schemaAnalyzer;
43
44
    /**
45
     * @var Schema
46
     */
47
    private $schema;
48
49
    /**
50
     * @var AbstractBeanPropertyDescriptor[]
51
     */
52
    private $beanPropertyDescriptors = [];
53
54
    /**
55
     * @var TDBMSchemaAnalyzer
56
     */
57
    private $tdbmSchemaAnalyzer;
58
59
    /**
60
     * @var NamingStrategyInterface
61
     */
62
    private $namingStrategy;
63
    /**
64
     * @var string
65
     */
66
    private $beanNamespace;
67
    /**
68
     * @var string
69
     */
70
    private $generatedBeanNamespace;
71
    /**
72
     * @var AnnotationParser
73
     */
74
    private $annotationParser;
75
76
    /**
77
     * @param Table $table
78
     * @param string $beanNamespace
79
     * @param string $generatedBeanNamespace
80
     * @param SchemaAnalyzer $schemaAnalyzer
81
     * @param Schema $schema
82
     * @param TDBMSchemaAnalyzer $tdbmSchemaAnalyzer
83
     * @param NamingStrategyInterface $namingStrategy
84
     * @param AnnotationParser $annotationParser
85
     */
86
    public function __construct(Table $table, string $beanNamespace, string $generatedBeanNamespace, SchemaAnalyzer $schemaAnalyzer, Schema $schema, TDBMSchemaAnalyzer $tdbmSchemaAnalyzer, NamingStrategyInterface $namingStrategy, AnnotationParser $annotationParser)
87
    {
88
        $this->table = $table;
89
        $this->beanNamespace = $beanNamespace;
90
        $this->generatedBeanNamespace = $generatedBeanNamespace;
91
        $this->schemaAnalyzer = $schemaAnalyzer;
92
        $this->schema = $schema;
93
        $this->tdbmSchemaAnalyzer = $tdbmSchemaAnalyzer;
94
        $this->namingStrategy = $namingStrategy;
95
        $this->annotationParser = $annotationParser;
96
        $this->initBeanPropertyDescriptors();
97
    }
98
99
    private function initBeanPropertyDescriptors(): void
100
    {
101
        $this->beanPropertyDescriptors = $this->getProperties($this->table);
102
    }
103
104
    /**
105
     * Returns the foreign-key the column is part of, if any. null otherwise.
106
     *
107
     * @param Table  $table
108
     * @param Column $column
109
     *
110
     * @return ForeignKeyConstraint|null
111
     */
112
    private function isPartOfForeignKey(Table $table, Column $column) : ?ForeignKeyConstraint
113
    {
114
        $localColumnName = $column->getName();
115
        foreach ($table->getForeignKeys() as $foreignKey) {
116
            foreach ($foreignKey->getUnquotedLocalColumns() as $columnName) {
117
                if ($columnName === $localColumnName) {
118
                    return $foreignKey;
119
                }
120
            }
121
        }
122
123
        return null;
124
    }
125
126
    /**
127
     * @return AbstractBeanPropertyDescriptor[]
128
     */
129
    public function getBeanPropertyDescriptors(): array
130
    {
131
        return $this->beanPropertyDescriptors;
132
    }
133
134
    /**
135
     * Returns the list of columns that are not nullable and not autogenerated for a given table and its parent.
136
     *
137
     * @return AbstractBeanPropertyDescriptor[]
138
     */
139
    public function getConstructorProperties(): array
140
    {
141
        $constructorProperties = array_filter($this->beanPropertyDescriptors, function (AbstractBeanPropertyDescriptor $property) {
142
            return $property->isCompulsory();
143
        });
144
145
        return $constructorProperties;
146
    }
147
148
    /**
149
     * Returns the list of columns that have default values for a given table.
150
     *
151
     * @return AbstractBeanPropertyDescriptor[]
152
     */
153
    public function getPropertiesWithDefault(): array
154
    {
155
        $properties = $this->getPropertiesForTable($this->table);
156
        $defaultProperties = array_filter($properties, function (AbstractBeanPropertyDescriptor $property) {
157
            return $property->hasDefault();
158
        });
159
160
        return $defaultProperties;
161
    }
162
163
    /**
164
     * Returns the list of properties exposed as getters and setters in this class.
165
     *
166
     * @return AbstractBeanPropertyDescriptor[]
167
     */
168
    public function getExposedProperties(): array
169
    {
170
        $exposedProperties = array_filter($this->beanPropertyDescriptors, function (AbstractBeanPropertyDescriptor $property) {
171
            return $property->getTable()->getName() == $this->table->getName();
172
        });
173
174
        return $exposedProperties;
175
    }
176
177
    /**
178
     * Returns the list of properties for this table (including parent tables).
179
     *
180
     * @param Table $table
181
     *
182
     * @return AbstractBeanPropertyDescriptor[]
183
     */
184
    private function getProperties(Table $table): array
185
    {
186
        // Security check: a table MUST have a primary key
187
        TDBMDaoGenerator::getPrimaryKeyColumnsOrFail($table);
188
189
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
190
        if ($parentRelationship) {
191
            $parentTable = $this->schema->getTable($parentRelationship->getForeignTableName());
192
            $properties = $this->getProperties($parentTable);
193
            // we merge properties by overriding property names.
194
            $localProperties = $this->getPropertiesForTable($table);
195
            foreach ($localProperties as $name => $property) {
196
                // We do not override properties if this is a primary key!
197
                if ($property->isPrimaryKey()) {
198
                    continue;
199
                }
200
                $properties[$name] = $property;
201
            }
202
        } else {
203
            $properties = $this->getPropertiesForTable($table);
204
        }
205
206
        return $properties;
207
    }
208
209
    /**
210
     * Returns the list of properties for this table (ignoring parent tables).
211
     *
212
     * @param Table $table
213
     *
214
     * @return AbstractBeanPropertyDescriptor[]
215
     */
216
    private function getPropertiesForTable(Table $table): array
217
    {
218
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
219
        if ($parentRelationship) {
220
            $ignoreColumns = $parentRelationship->getUnquotedLocalColumns();
221
        } else {
222
            $ignoreColumns = [];
223
        }
224
225
        $beanPropertyDescriptors = [];
226
        foreach ($table->getColumns() as $column) {
227
            if (array_search($column->getName(), $ignoreColumns) !== false) {
228
                continue;
229
            }
230
231
            $fk = $this->isPartOfForeignKey($table, $column);
232
            if ($fk !== null) {
233
                // Check that previously added descriptors are not added on same FK (can happen with multi key FK).
234
                foreach ($beanPropertyDescriptors as $beanDescriptor) {
235
                    if ($beanDescriptor instanceof ObjectBeanPropertyDescriptor && $beanDescriptor->getForeignKey() === $fk) {
236
                        continue 2;
237
                    }
238
                }
239
                // Check that this property is not an inheritance relationship
240
                $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
241
                if ($parentRelationship === $fk) {
242
                    continue;
243
                }
244
245
                $beanPropertyDescriptors[] = new ObjectBeanPropertyDescriptor($table, $fk, $this->namingStrategy, $this->beanNamespace);
246
            } else {
247
                $beanPropertyDescriptors[] = new ScalarBeanPropertyDescriptor($table, $column, $this->namingStrategy, $this->annotationParser);
248
            }
249
        }
250
251
        // Now, let's get the name of all properties and let's check there is no duplicate.
252
        /* @var $names AbstractBeanPropertyDescriptor[] */
253
        $names = [];
254
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
255
            $name = $beanDescriptor->getGetterName();
256
            if (isset($names[$name])) {
257
                $names[$name]->useAlternativeName();
258
                $beanDescriptor->useAlternativeName();
259
            } else {
260
                $names[$name] = $beanDescriptor;
261
            }
262
        }
263
264
        // Final check (throw exceptions if problem arises)
265
        $names = [];
266
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
267
            $name = $beanDescriptor->getGetterName();
268
            if (isset($names[$name])) {
269
                throw new TDBMException('Unsolvable name conflict while generating method name');
270
            } else {
271
                $names[$name] = $beanDescriptor;
272
            }
273
        }
274
275
        // Last step, let's rebuild the list with a map:
276
        $beanPropertyDescriptorsMap = [];
277
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
278
            $beanPropertyDescriptorsMap[$beanDescriptor->getVariableName()] = $beanDescriptor;
279
        }
280
281
        return $beanPropertyDescriptorsMap;
282
    }
283
284
    private function generateBeanConstructor() : MethodGenerator
285
    {
286
        $constructorProperties = $this->getConstructorProperties();
287
288
        $constructor = new MethodGenerator('__construct', [], MethodGenerator::FLAG_PUBLIC);
289
        $constructor->setDocBlock('The constructor takes all compulsory arguments.');
290
291
        $assigns = [];
292
        $parentConstructorArguments = [];
293
294
        foreach ($constructorProperties as $property) {
295
            $parameter = new ParameterGenerator(ltrim($property->getVariableName(), '$'));
296
            if ($property->isTypeHintable()) {
297
                $parameter->setType($property->getPhpType());
298
            }
299
            $constructor->setParameter($parameter);
300
301
            $constructor->getDocBlock()->setTag($property->getParamAnnotation());
302
303
            if ($property->getTable()->getName() === $this->table->getName()) {
304
                $assigns[] = $property->getConstructorAssignCode()."\n";
305
            } else {
306
                $parentConstructorArguments[] = $property->getVariableName();
307
            }
308
        }
309
310
        $parentConstructorCode = sprintf("parent::__construct(%s);\n", implode(', ', $parentConstructorArguments));
311
312
        foreach ($this->getPropertiesWithDefault() as $property) {
313
            $assigns[] = $property->assignToDefaultCode()."\n";
314
        }
315
316
        $body = $parentConstructorCode . implode('', $assigns);
317
318
        $constructor->setBody($body);
319
320
        return $constructor;
321
    }
322
323
    /**
324
     * Returns the descriptors of one-to-many relationships (the foreign keys pointing on this beans)
325
     *
326
     * @return DirectForeignKeyMethodDescriptor[]
327
     */
328
    private function getDirectForeignKeysDescriptors(): array
329
    {
330
        $fks = $this->tdbmSchemaAnalyzer->getIncomingForeignKeys($this->table->getName());
331
332
        $descriptors = [];
333
334
        foreach ($fks as $fk) {
335
            $descriptors[] = new DirectForeignKeyMethodDescriptor($fk, $this->table, $this->namingStrategy);
336
        }
337
338
        return $descriptors;
339
    }
340
341
    /**
342
     * @return PivotTableMethodsDescriptor[]
343
     */
344
    private function getPivotTableDescriptors(): array
345
    {
346
        $descs = [];
347
        foreach ($this->schemaAnalyzer->detectJunctionTables(true) as $table) {
348
            // There are exactly 2 FKs since this is a pivot table.
349
            $fks = array_values($table->getForeignKeys());
350
351
            if ($fks[0]->getForeignTableName() === $this->table->getName()) {
352
                list($localFk, $remoteFk) = $fks;
353
            } elseif ($fks[1]->getForeignTableName() === $this->table->getName()) {
354
                list($remoteFk, $localFk) = $fks;
355
            } else {
356
                continue;
357
            }
358
359
            $descs[] = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $this->namingStrategy, $this->beanNamespace);
360
        }
361
362
        return $descs;
363
    }
364
365
    /**
366
     * Returns the list of method descriptors (and applies the alternative name if needed).
367
     *
368
     * @return MethodDescriptorInterface[]
369
     */
370
    public function getMethodDescriptors(): array
371
    {
372
        $directForeignKeyDescriptors = $this->getDirectForeignKeysDescriptors();
373
        $pivotTableDescriptors = $this->getPivotTableDescriptors();
374
375
        $descriptors = array_merge($directForeignKeyDescriptors, $pivotTableDescriptors);
376
377
        // Descriptors by method names
378
        $descriptorsByMethodName = [];
379
380
        foreach ($descriptors as $descriptor) {
381
            $descriptorsByMethodName[$descriptor->getName()][] = $descriptor;
382
        }
383
384
        foreach ($descriptorsByMethodName as $descriptorsForMethodName) {
385
            if (count($descriptorsForMethodName) > 1) {
386
                foreach ($descriptorsForMethodName as $descriptor) {
387
                    $descriptor->useAlternativeName();
388
                }
389
            }
390
        }
391
392
        return $descriptors;
393
    }
394
395
    public function generateJsonSerialize(): MethodGenerator
396
    {
397
        $tableName = $this->table->getName();
398
        $parentFk = $this->schemaAnalyzer->getParentRelationship($tableName);
399
        if ($parentFk !== null) {
400
            $initializer = '$array = parent::jsonSerialize($stopRecursion);';
401
        } else {
402
            $initializer = '$array = [];';
403
        }
404
405
        $method = new MethodGenerator('jsonSerialize');
406
        $method->setDocBlock('Serializes the object for JSON encoding.');
407
        $method->getDocBlock()->setTag(new ParamTag('$stopRecursion', ['bool'], 'Parameter used internally by TDBM to stop embedded objects from embedding other objects.'));
408
        $method->getDocBlock()->setTag(new ReturnTag(['array']));
409
        $method->setParameter(new ParameterGenerator('stopRecursion', 'bool', false));
410
411
        $str = '%s
412
%s
413
%s
414
return $array;
415
';
416
417
        $propertiesCode = '';
418
        foreach ($this->getExposedProperties() as $beanPropertyDescriptor) {
419
            $propertiesCode .= $beanPropertyDescriptor->getJsonSerializeCode();
420
        }
421
422
        // Many2many relationships
423
        $methodsCode = '';
424
        foreach ($this->getMethodDescriptors() as $methodDescriptor) {
425
            $methodsCode .= $methodDescriptor->getJsonSerializeCode();
426
        }
427
428
        $method->setBody(sprintf($str, $initializer, $propertiesCode, $methodsCode));
429
430
        return $method;
431
    }
432
433
    /**
434
     * Returns as an array the class we need to extend from and the list of use statements.
435
     *
436
     * @param ForeignKeyConstraint|null $parentFk
437
     * @return string[]
438
     */
439
    private function generateExtendsAndUseStatements(ForeignKeyConstraint $parentFk = null): array
440
    {
441
        $classes = [];
442
        if ($parentFk !== null) {
443
            $extends = $this->namingStrategy->getBeanClassName($parentFk->getForeignTableName());
444
            $classes[] = $extends;
445
        }
446
447
        foreach ($this->getBeanPropertyDescriptors() as $beanPropertyDescriptor) {
448
            $className = $beanPropertyDescriptor->getClassName();
449
            if (null !== $className) {
450
                $classes[] = $className;
451
            }
452
        }
453
454
        foreach ($this->getMethodDescriptors() as $descriptor) {
455
            $classes = array_merge($classes, $descriptor->getUsedClasses());
456
        }
457
458
        $classes = array_unique($classes);
459
460
        return $classes;
461
    }
462
463
    /**
464
     * Writes the PHP bean file with all getters and setters from the table passed in parameter.
465
     *
466
     * @return FileGenerator
467
     */
468
    public function generatePhpCode(): FileGenerator
469
    {
470
471
        $file = new FileGenerator();
472
        $class = new ClassGenerator();
473
        $file->setClass($class);
474
        $file->setNamespace($this->generatedBeanNamespace);
475
476
        $tableName = $this->table->getName();
477
        $baseClassName = $this->namingStrategy->getBaseBeanClassName($tableName);
478
        $className = $this->namingStrategy->getBeanClassName($tableName);
479
        $parentFk = $this->schemaAnalyzer->getParentRelationship($this->table->getName());
480
481
        $classes = $this->generateExtendsAndUseStatements($parentFk);
482
483
        foreach ($classes as $useClass) {
484
            $file->setUse($this->beanNamespace.'\\'.$useClass);
485
        }
486
487
        /*$uses = array_map(function ($className) {
488
            return 'use '.$this->beanNamespace.'\\'.$className.";\n";
489
        }, $classes);
490
        $use = implode('', $uses);*/
491
492
        $extends = $this->getExtendedBeanClassName();
493
        if ($extends === null) {
494
            $class->setExtendedClass(AbstractTDBMObject::class);
495
            $file->setUse(AbstractTDBMObject::class);
496
        } else {
497
            $class->setExtendedClass($extends);
498
        }
499
500
        $file->setUse(ResultIterator::class);
501
        $file->setUse(AlterableResultIterator::class);
502
        $file->setUse(Uuid::class);
503
        $file->setUse(JsonSerializable::class);
504
505
        $class->setName($baseClassName);
506
        $class->setAbstract(true);
507
508
        // TODO: CHECK AVAILABILITY OF declare(strict_types=1);
509
510
        $file->setDocBlock(new DocBlockGenerator('This file has been automatically generated by TDBM.', <<<EOF
511
DO NOT edit this file, as it might be overwritten.
512
If you need to perform changes, edit the $className class instead!
513
EOF
514
        ));
515
516
        $class->setDocBlock(new DocBlockGenerator("The $baseClassName class maps the '$tableName' table in database."));
517
        $class->setImplementedInterfaces([ JsonSerializable::class ]);
518
519
520
        $class->addMethodFromGenerator($this->generateBeanConstructor());
521
522
        foreach ($this->getExposedProperties() as $property) {
523
            foreach ($property->getGetterSetterCode() as $generator) {
524
                $class->addMethodFromGenerator($generator);
525
            }
526
        }
527
528
        foreach ($this->getMethodDescriptors() as $methodDescriptor) {
529
            foreach ($methodDescriptor->getCode() as $generator) {
530
                $class->addMethodFromGenerator($generator);
531
            }
532
        }
533
534
        $class->addMethodFromGenerator($this->generateJsonSerialize());
535
        $class->addMethodFromGenerator($this->generateGetUsedTablesCode());
536
        $onDeleteCode = $this->generateOnDeleteCode();
537
        if ($onDeleteCode) {
538
            $class->addMethodFromGenerator($onDeleteCode);
539
        }
540
        $cloneCode = $this->generateCloneCode();
541
        if ($cloneCode) {
0 ignored issues
show
introduced by
$cloneCode is of type Zend\Code\Generator\MethodGenerator, thus it always evaluated to true.
Loading history...
542
            $class->addMethodFromGenerator($cloneCode);
543
        }
544
545
        return $file;
546
    }
547
548
    /**
549
     * @param string $beanNamespace
550
     * @param string $beanClassName
551
     *
552
     * @return mixed[] first element: list of used beans, second item: PHP code as a string
553
     */
554
    public function generateFindByDaoCode(string $beanNamespace, string $beanClassName): array
555
    {
556
        $code = '';
557
        $usedBeans = [];
558
        foreach ($this->removeDuplicateIndexes($this->table->getIndexes()) as $index) {
559
            if (!$index->isPrimary()) {
560
                list($usedBeansForIndex, $codeForIndex) = $this->generateFindByDaoCodeForIndex($index, $beanNamespace, $beanClassName);
561
                $code .= $codeForIndex;
562
                $usedBeans = array_merge($usedBeans, $usedBeansForIndex);
563
            }
564
        }
565
566
        return [$usedBeans, $code];
567
    }
568
569
    /**
570
     * Remove identical indexes (indexes on same columns)
571
     *
572
     * @param Index[] $indexes
573
     * @return Index[]
574
     */
575
    private function removeDuplicateIndexes(array $indexes): array
576
    {
577
        $indexesByKey = [];
578
        foreach ($indexes as $index) {
579
            $indexesByKey[implode('__`__', $index->getUnquotedColumns())] = $index;
580
        }
581
582
        return array_values($indexesByKey);
583
    }
584
585
    /**
586
     * @param Index  $index
587
     * @param string $beanNamespace
588
     * @param string $beanClassName
589
     *
590
     * @return mixed[] first element: list of used beans, second item: PHP code as a string
591
     */
592
    private function generateFindByDaoCodeForIndex(Index $index, string $beanNamespace, string $beanClassName): array
593
    {
594
        $columns = $index->getColumns();
595
        $usedBeans = [];
596
597
        /**
598
         * The list of elements building this index (expressed as columns or foreign keys)
599
         * @var AbstractBeanPropertyDescriptor[]
600
         */
601
        $elements = [];
602
603
        foreach ($columns as $column) {
604
            $fk = $this->isPartOfForeignKey($this->table, $this->table->getColumn($column));
605
            if ($fk !== null) {
606
                if (!in_array($fk, $elements)) {
607
                    $elements[] = new ObjectBeanPropertyDescriptor($this->table, $fk, $this->namingStrategy, $this->beanNamespace);
608
                }
609
            } else {
610
                $elements[] = new ScalarBeanPropertyDescriptor($this->table, $this->table->getColumn($column), $this->namingStrategy, $this->annotationParser);
611
            }
612
        }
613
614
        // If the index is actually only a foreign key, let's bypass it entirely.
615
        if (count($elements) === 1 && $elements[0] instanceof ObjectBeanPropertyDescriptor) {
616
            return [[], ''];
617
        }
618
619
        $functionParameters = [];
620
        $first = true;
621
        foreach ($elements as $element) {
622
            if (!$first) {
623
                $functionParameter = '?';
624
            } else {
625
                $functionParameter = '';
626
            }
627
            $functionParameter .= $element->getPhpType();
628
            $elementClassName = $element->getClassName();
629
            if ($elementClassName) {
630
                $usedBeans[] = $beanNamespace.'\\'.$elementClassName;
631
            }
632
            $functionParameter .= ' '.$element->getVariableName();
633
            if ($first) {
634
                $first = false;
635
            } else {
636
                $functionParameter .= ' = null';
637
            }
638
            $functionParameters[] = $functionParameter;
639
        }
640
641
        $functionParametersString = implode(', ', $functionParameters);
642
643
        $count = 0;
644
645
        $params = [];
646
        $filterArrayCode = '';
647
        $commentArguments = [];
648
        $first = true;
649
        foreach ($elements as $element) {
650
            $params[] = $element->getParamAnnotation();
651
            if ($element instanceof ScalarBeanPropertyDescriptor) {
652
                $filterArrayCode .= '            '.var_export($element->getColumnName(), true).' => '.$element->getVariableName().",\n";
653
            } elseif ($element instanceof ObjectBeanPropertyDescriptor) {
654
                $foreignKey = $element->getForeignKey();
655
                $columns = SafeFunctions::arrayCombine($foreignKey->getLocalColumns(), $foreignKey->getForeignColumns());
656
                ++$count;
657
                $foreignTable = $this->schema->getTable($foreignKey->getForeignTableName());
658
                foreach ($columns as $localColumn => $foreignColumn) {
659
                    // 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.
660
                    $targetedElement = new ScalarBeanPropertyDescriptor($foreignTable, $foreignTable->getColumn($foreignColumn), $this->namingStrategy, $this->annotationParser);
661
                    if ($first) {
662
                        // First parameter for index is not nullable
663
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => '.$element->getVariableName().'->'.$targetedElement->getGetterName()."(),\n";
664
                    } else {
665
                        // Other parameters for index is not nullable
666
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => ('.$element->getVariableName().' !== null) ? '.$element->getVariableName().'->'.$targetedElement->getGetterName()."() : null,\n";
667
                    }
668
                }
669
            }
670
            $commentArguments[] = substr($element->getVariableName(), 1);
671
            if ($first) {
672
                $first = false;
673
            }
674
        }
675
676
        $paramsString = '';
677
        foreach ($params as $param) {
678
            $paramsString .= $param->generate()."\n";
679
        }
680
        //$paramsString = implode("\n", $params);
681
682
        $methodName = $this->namingStrategy->getFindByIndexMethodName($index, $elements);
683
684
        if ($index->isUnique()) {
685
            $returnType = $beanClassName;
686
687
            $code = "
688
    /**
689
     * Get a $beanClassName filtered by ".implode(', ', $commentArguments).".
690
     *
691
$paramsString
692
     * @param string[] \$additionalTablesFetch A list of additional tables to fetch (for performance improvement)
693
     * @return $returnType|null
694
     */
695
    public function $methodName($functionParametersString, array \$additionalTablesFetch = array()) : ?$returnType
696
    {
697
        \$filter = [
698
".$filterArrayCode."        ];
699
        return \$this->findOne(\$filter, [], \$additionalTablesFetch);
700
    }
701
";
702
        } else {
703
            $returnType = "{$beanClassName}[]|ResultIterator";
704
705
            $code = "
706
    /**
707
     * Get a list of $beanClassName filtered by ".implode(', ', $commentArguments).".
708
     *
709
$paramsString
710
     * @param mixed \$orderBy The order string
711
     * @param string[] \$additionalTablesFetch A list of additional tables to fetch (for performance improvement)
712
     * @param int \$mode Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.
713
     * @return $returnType
714
     */
715
    public function $methodName($functionParametersString, \$orderBy = null, array \$additionalTablesFetch = array(), ?int \$mode = null) : iterable
716
    {
717
        \$filter = [
718
".$filterArrayCode."        ];
719
        return \$this->find(\$filter, [], \$orderBy, \$additionalTablesFetch, \$mode);
720
    }
721
";
722
        }
723
724
        return [$usedBeans, $code];
725
    }
726
727
    /**
728
     * Generates the code for the getUsedTable protected method.
729
     *
730
     * @return MethodGenerator
731
     */
732
    private function generateGetUsedTablesCode(): MethodGenerator
733
    {
734
        $hasParentRelationship = $this->schemaAnalyzer->getParentRelationship($this->table->getName()) !== null;
735
        if ($hasParentRelationship) {
736
            $code = sprintf('$tables = parent::getUsedTables();
737
$tables[] = %s;
738
739
return $tables;', var_export($this->table->getName(), true));
740
        } else {
741
            $code = sprintf('        return [ %s ];', var_export($this->table->getName(), true));
742
        }
743
744
        $method = new MethodGenerator('getUsedTables');
745
        $method->setDocBlock('Returns an array of used tables by this bean (from parent to child relationship).');
746
        $method->getDocBlock()->setTag(new ReturnTag(['string[]']));
747
        $method->setReturnType('array');
748
        $method->setBody($code);
749
750
        return $method;
751
    }
752
753
    private function generateOnDeleteCode(): ?MethodGenerator
754
    {
755
        $code = '';
756
        $relationships = $this->getPropertiesForTable($this->table);
757
        foreach ($relationships as $relationship) {
758
            if ($relationship instanceof ObjectBeanPropertyDescriptor) {
759
                $code .= sprintf('$this->setRef('.var_export($relationship->getForeignKey()->getName(), true).', null, '.var_export($this->table->getName(), true).");\n");
760
            }
761
        }
762
763
        if (!$code) {
764
            return null;
765
        }
766
767
        $method = new MethodGenerator('onDelete');
768
        $method->setDocBlock('Method called when the bean is removed from database.');
769
        $method->setReturnType('void');
770
        $method->setBody('parent::onDelete();
771
'.$code);
772
773
        return $method;
774
    }
775
776
    private function generateCloneCode(): ?MethodGenerator
777
    {
778
        $code = '';
779
780
        foreach ($this->beanPropertyDescriptors as $beanPropertyDescriptor) {
781
            $code .= $beanPropertyDescriptor->getCloneRule();
782
        }
783
784
        $method = new MethodGenerator('__clone');
785
        $method->setBody('parent::__clone();
786
'.$code);
787
788
        return $method;
789
    }
790
791
    /**
792
     * Returns the bean class name (without the namespace).
793
     *
794
     * @return string
795
     */
796
    public function getBeanClassName() : string
797
    {
798
        return $this->namingStrategy->getBeanClassName($this->table->getName());
799
    }
800
801
    /**
802
     * Returns the base bean class name (without the namespace).
803
     *
804
     * @return string
805
     */
806
    public function getBaseBeanClassName() : string
807
    {
808
        return $this->namingStrategy->getBaseBeanClassName($this->table->getName());
809
    }
810
811
    /**
812
     * Returns the DAO class name (without the namespace).
813
     *
814
     * @return string
815
     */
816
    public function getDaoClassName() : string
817
    {
818
        return $this->namingStrategy->getDaoClassName($this->table->getName());
819
    }
820
821
    /**
822
     * Returns the base DAO class name (without the namespace).
823
     *
824
     * @return string
825
     */
826
    public function getBaseDaoClassName() : string
827
    {
828
        return $this->namingStrategy->getBaseDaoClassName($this->table->getName());
829
    }
830
831
    /**
832
     * Returns the table used to build this bean.
833
     *
834
     * @return Table
835
     */
836
    public function getTable(): Table
837
    {
838
        return $this->table;
839
    }
840
841
    /**
842
     * Returns the extended bean class name (without the namespace), or null if the bean is not extended.
843
     *
844
     * @return string
845
     */
846
    public function getExtendedBeanClassName(): ?string
847
    {
848
        $parentFk = $this->schemaAnalyzer->getParentRelationship($this->table->getName());
849
        if ($parentFk !== null) {
850
            return $this->namingStrategy->getBeanClassName($parentFk->getForeignTableName());
851
        } else {
852
            return null;
853
        }
854
    }
855
856
    /**
857
     * @return string
858
     */
859
    public function getBeanNamespace(): string
860
    {
861
        return $this->beanNamespace;
862
    }
863
864
    /**
865
     * @return string
866
     */
867
    public function getGeneratedBeanNamespace(): string
868
    {
869
        return $this->generatedBeanNamespace;
870
    }
871
}
872