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

BeanDescriptor::generateDaoPhpCode()   F

Complexity

Conditions 17
Paths 12288

Size

Total Lines 363
Code Lines 261

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 261
dl 0
loc 363
rs 0.8399
c 0
b 0
f 0
cc 17
nc 12288
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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