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

BeanDescriptor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 15
rs 9.8333
c 0
b 0
f 0
cc 1
nc 1
nop 12

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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