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

BeanDescriptor::getBaseBeanClassName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM\Utils;
5
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\Index;
8
use Doctrine\DBAL\Schema\Schema;
9
use Doctrine\DBAL\Schema\Table;
10
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
11
use JsonSerializable;
12
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
13
use 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
            if ($getter !== null) {
544
                $class->addMethodFromGenerator($getter);
545
            }
546
            if ($setter !== null) {
547
                $class->addMethodFromGenerator($setter);
548
            }
549
        }
550
551
        foreach ($this->getMethodDescriptors() as $methodDescriptor) {
552
            if ($methodDescriptor instanceof DirectForeignKeyMethodDescriptor) {
553
                [$method] = $methodDescriptor->getCode();
554
                $method = $this->codeGeneratorListener->onBaseBeanOneToManyGenerated($method, $methodDescriptor, $this, $this->configuration, $class);
555
                if ($method) {
556
                    $class->addMethodFromGenerator($method);
557
                }
558
            } elseif ($methodDescriptor instanceof PivotTableMethodsDescriptor) {
559
                [ $getter, $adder, $remover, $has, $setter ] = $methodDescriptor->getCode();
560
                $methods = $this->codeGeneratorListener->onBaseBeanManyToManyGenerated($getter, $adder, $remover, $has, $setter, $methodDescriptor, $this, $this->configuration, $class);
561
                foreach ($methods as $method) {
562
                    if ($method) {
563
                        $class->addMethodFromGenerator($method);
564
                    }
565
                }
566
            } else {
567
                throw new \RuntimeException('Unexpected instance'); // @codeCoverageIgnore
568
            }
569
        }
570
571
        $method = $this->generateJsonSerialize();
572
        $method = $this->codeGeneratorListener->onBaseBeanJsonSerializeGenerated($method, $this, $this->configuration, $class);
573
        if ($method !== null) {
574
            $class->addMethodFromGenerator($method);
575
        }
576
577
        $class->addMethodFromGenerator($this->generateGetUsedTablesCode());
578
        $onDeleteCode = $this->generateOnDeleteCode();
579
        if ($onDeleteCode) {
580
            $class->addMethodFromGenerator($onDeleteCode);
581
        }
582
        $cloneCode = $this->generateCloneCode();
583
        $cloneCode = $this->codeGeneratorListener->onBaseBeanCloneGenerated($cloneCode, $this, $this->configuration, $class);
584
        if ($cloneCode) {
585
            $class->addMethodFromGenerator($cloneCode);
586
        }
587
588
        return $file;
589
    }
590
591
    /**
592
     * Writes the representation of the PHP DAO file.
593
     *
594
     * @return FileGenerator
595
     */
596
    public function generateDaoPhpCode(): FileGenerator
597
    {
598
        $file = new FileGenerator();
599
        $class = new ClassGenerator();
600
        $file->setClass($class);
601
        $file->setNamespace($this->generatedDaoNamespace);
602
603
        $tableName = $this->table->getName();
604
605
        $primaryKeyColumns = TDBMDaoGenerator::getPrimaryKeyColumnsOrFail($this->table);
606
607
        list($defaultSort, $defaultSortDirection) = $this->getDefaultSortColumnFromAnnotation($this->table);
608
609
        $className = $this->namingStrategy->getDaoClassName($tableName);
610
        $baseClassName = $this->namingStrategy->getBaseDaoClassName($tableName);
611
        $beanClassWithoutNameSpace = $this->namingStrategy->getBeanClassName($tableName);
612
        $beanClassName = $this->beanNamespace.'\\'.$beanClassWithoutNameSpace;
613
614
        $findByDaoCodeMethods = $this->generateFindByDaoCode($this->beanNamespace, $beanClassWithoutNameSpace, $class);
615
616
        $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...
617
        // Let's suppress duplicates in used beans (if any)
618
        $usedBeans = array_flip(array_flip($usedBeans));
619
        foreach ($usedBeans as $usedBean) {
620
            $class->addUse($usedBean);
621
        }
622
623
        $file->setDocBlock(new DocBlockGenerator(<<<EOF
624
This file has been automatically generated by TDBM.
625
DO NOT edit this file, as it might be overwritten.
626
If you need to perform changes, edit the $className class instead!
627
EOF
628
        ));
629
630
        $file->setNamespace($this->generatedDaoNamespace);
631
632
        $class->addUse(TDBMService::class);
633
        $class->addUse(ResultIterator::class);
634
        $class->addUse(TDBMException::class);
635
636
        $class->setName($baseClassName);
637
638
        $class->setDocBlock(new DocBlockGenerator("The $baseClassName class will maintain the persistence of $beanClassWithoutNameSpace class into the $tableName table."));
639
640
        $tdbmServiceProperty = new PropertyGenerator('tdbmService');
641
        $tdbmServiceProperty->setDocBlock(new DocBlockGenerator(null, null, [new VarTag(null, ['\\'.TDBMService::class])]));
642
        $class->addPropertyFromGenerator($tdbmServiceProperty);
643
644
        $defaultSortProperty = new PropertyGenerator('defaultSort', $defaultSort);
645
        $defaultSortProperty->setDocBlock(new DocBlockGenerator('The default sort column.', null, [new VarTag(null, ['string', 'null'])]));
646
        $class->addPropertyFromGenerator($defaultSortProperty);
647
648
        $defaultSortPropertyDirection = new PropertyGenerator('defaultDirection', $defaultSort && $defaultSortDirection ? $defaultSortDirection : 'asc');
649
        $defaultSortPropertyDirection->setDocBlock(new DocBlockGenerator('The default sort direction.', null, [new VarTag(null, ['string'])]));
650
        $class->addPropertyFromGenerator($defaultSortPropertyDirection);
651
652
        $constructorMethod = new MethodGenerator('__construct',
653
            [ new ParameterGenerator('tdbmService', TDBMService::class) ],
654
            MethodGenerator::FLAG_PUBLIC,
655
            '$this->tdbmService = $tdbmService;',
656
            'Sets the TDBM service used by this DAO.');
657
        $constructorMethod = $this->codeGeneratorListener->onBaseDaoConstructorGenerated($constructorMethod, $this, $this->configuration, $class);
658
        if ($constructorMethod !== null) {
659
            $class->addMethodFromGenerator($constructorMethod);
660
        }
661
662
        $saveMethod = new MethodGenerator(
663
            'save',
664
            [ new ParameterGenerator('obj', $beanClassName) ],
665
            MethodGenerator::FLAG_PUBLIC,
666
            '$this->tdbmService->save($obj);',
667
            new DocBlockGenerator("Persist the $beanClassWithoutNameSpace instance.",
668
                null,
669
                [
670
                    new ParamTag('obj', [$beanClassWithoutNameSpace], 'The bean to save.')
671
                ]));
672
        $saveMethod->setReturnType('void');
673
674
        $saveMethod = $this->codeGeneratorListener->onBaseDaoSaveGenerated($saveMethod, $this, $this->configuration, $class);
675
        if ($saveMethod !== null) {
676
            $class->addMethodFromGenerator($saveMethod);
677
        }
678
679
        $findAllBody = <<<EOF
680
if (\$this->defaultSort) {
681
    \$orderBy = '$tableName.'.\$this->defaultSort.' '.\$this->defaultDirection;
682
} else {
683
    \$orderBy = null;
684
}
685
return \$this->tdbmService->findObjects('$tableName', null, [], \$orderBy);
686
EOF;
687
688
        $findAllMethod = new MethodGenerator(
689
            'findAll',
690
            [],
691
            MethodGenerator::FLAG_PUBLIC,
692
            $findAllBody,
693
            (new DocBlockGenerator("Get all $beanClassWithoutNameSpace records.",
694
                null,
695
                [
696
                    new ReturnTag([ '\\'.$beanClassName.'[]', '\\'.ResultIterator::class ])
697
                ]))->setWordWrap(false)
698
        );
699
        $findAllMethod->setReturnType('iterable');
700
        $findAllMethod = $this->codeGeneratorListener->onBaseDaoFindAllGenerated($findAllMethod, $this, $this->configuration, $class);
701
        if ($findAllMethod !== null) {
702
            $class->addMethodFromGenerator($findAllMethod);
703
        }
704
705
        if (count($primaryKeyColumns) === 1) {
706
            $primaryKeyColumn = $primaryKeyColumns[0];
707
            $primaryKeyPhpType = TDBMDaoGenerator::dbalTypeToPhpType($this->table->getColumn($primaryKeyColumn)->getType());
708
709
            $getByIdMethod = new MethodGenerator(
710
                'getById',
711
                [
712
                    new ParameterGenerator('id', $primaryKeyPhpType),
713
                    new ParameterGenerator('lazyLoading', 'bool', false)
714
                ],
715
                MethodGenerator::FLAG_PUBLIC,
716
                "return \$this->tdbmService->findObjectByPk('$tableName', ['$primaryKeyColumn' => \$id], [], \$lazyLoading);",
717
                (new DocBlockGenerator("Get $beanClassWithoutNameSpace specified by its ID (its primary key).",
718
                    'If the primary key does not exist, an exception is thrown.',
719
                    [
720
                        new ParamTag('id', [$primaryKeyPhpType]),
721
                        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.'),
722
                        new ReturnTag(['\\'.$beanClassName . '[]', '\\'.ResultIterator::class]),
723
                        new ThrowsTag('\\'.TDBMException::class)
724
                    ]))->setWordWrap(false)
725
            );
726
            $getByIdMethod = $this->codeGeneratorListener->onBaseDaoGetByIdGenerated($getByIdMethod, $this, $this->configuration, $class);
727
            if ($getByIdMethod) {
728
                $class->addMethodFromGenerator($getByIdMethod);
729
            }
730
        }
731
732
        $deleteMethodBody = <<<EOF
733
if (\$cascade === true) {
734
    \$this->tdbmService->deleteCascade(\$obj);
735
} else {
736
    \$this->tdbmService->delete(\$obj);
737
}
738
EOF;
739
740
741
        $deleteMethod = new MethodGenerator(
742
            'delete',
743
            [
744
                new ParameterGenerator('obj', $beanClassName),
745
                new ParameterGenerator('cascade', 'bool', false)
746
            ],
747
            MethodGenerator::FLAG_PUBLIC,
748
            $deleteMethodBody,
749
            (new DocBlockGenerator("Get all $beanClassWithoutNameSpace records.",
750
                null,
751
                [
752
                    new ParamTag('obj', ['\\'.$beanClassName], 'The object to delete'),
753
                    new ParamTag('cascade', ['bool'], 'If true, it will delete all objects linked to $obj'),
754
                ]))->setWordWrap(false)
755
        );
756
        $deleteMethod->setReturnType('void');
757
        $deleteMethod = $this->codeGeneratorListener->onBaseDaoDeleteGenerated($deleteMethod, $this, $this->configuration, $class);
758
        if ($deleteMethod !== null) {
759
            $class->addMethodFromGenerator($deleteMethod);
760
        }
761
762
        $findMethodBody = <<<EOF
763
if (\$this->defaultSort && \$orderBy == null) {
764
    \$orderBy = '$tableName.'.\$this->defaultSort.' '.\$this->defaultDirection;
765
}
766
return \$this->tdbmService->findObjects('$tableName', \$filter, \$parameters, \$orderBy, \$additionalTablesFetch, \$mode);
767
EOF;
768
769
770
        $findMethod = new MethodGenerator(
771
            'find',
772
            [
773
                (new ParameterGenerator('filter'))->setDefaultValue(null),
774
                new ParameterGenerator('parameters', 'array', []),
775
                (new ParameterGenerator('orderBy'))->setDefaultValue(null),
776
                new ParameterGenerator('additionalTablesFetch', 'array', []),
777
                (new ParameterGenerator('mode', '?int'))->setDefaultValue(null),
778
            ],
779
            MethodGenerator::FLAG_PROTECTED,
780
            $findMethodBody,
781
            (new DocBlockGenerator("Get all $beanClassWithoutNameSpace records.",
782
                null,
783
                [
784
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
785
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
786
                    new ParamTag('orderBy', ['mixed'], 'The order string'),
787
                    new ParamTag('additionalTablesFetch', ['string[]'], 'A list of additional tables to fetch (for performance improvement)'),
788
                    new ParamTag('mode', ['int', 'null'], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.'),
789
                    new ReturnTag(['\\' . $beanClassName . '[]', '\\'.ResultIterator::class])
790
                ]))->setWordWrap(false)
791
        );
792
        $findMethod->setReturnType('iterable');
793
        $findMethod = $this->codeGeneratorListener->onBaseDaoFindGenerated($findMethod, $this, $this->configuration, $class);
794
        if ($findMethod !== null) {
795
            $class->addMethodFromGenerator($findMethod);
796
        }
797
798
        $findFromSqlMethodBody = <<<EOF
799
if (\$this->defaultSort && \$orderBy == null) {
800
    \$orderBy = '$tableName.'.\$this->defaultSort.' '.\$this->defaultDirection;
801
}
802
return \$this->tdbmService->findObjectsFromSql('$tableName', \$from, \$filter, \$parameters, \$orderBy, \$mode);
803
EOF;
804
805
        $findFromSqlMethod = new MethodGenerator(
806
            'findFromSql',
807
            [
808
                new ParameterGenerator('from', 'string'),
809
                (new ParameterGenerator('filter'))->setDefaultValue(null),
810
                new ParameterGenerator('parameters', 'array', []),
811
                (new ParameterGenerator('orderBy'))->setDefaultValue(null),
812
                new ParameterGenerator('additionalTablesFetch', 'array', []),
813
                (new ParameterGenerator('mode', '?int'))->setDefaultValue(null),
814
            ],
815
            MethodGenerator::FLAG_PROTECTED,
816
            $findFromSqlMethodBody,
817
            (new DocBlockGenerator("Get a list of $beanClassWithoutNameSpace specified by its filters.",
818
                "Unlike the `find` method that guesses the FROM part of the statement, here you can pass the \$from part.
819
820
You should not put an alias on the main table name. So your \$from variable should look like:
821
822
   \"$tableName JOIN ... ON ...\"",
823
                [
824
                    new ParamTag('from', ['string'], 'The sql from statement'),
825
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
826
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
827
                    new ParamTag('orderBy', ['mixed'], 'The order string'),
828
                    new ParamTag('additionalTablesFetch', ['string[]'], 'A list of additional tables to fetch (for performance improvement)'),
829
                    new ParamTag('mode', ['int', 'null'], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.'),
830
                    new ReturnTag(['\\'.$beanClassName . '[]', '\\'.ResultIterator::class])
831
                ]))->setWordWrap(false)
832
        );
833
        $findFromSqlMethod->setReturnType('iterable');
834
        $findFromSqlMethod = $this->codeGeneratorListener->onBaseDaoFindFromSqlGenerated($findFromSqlMethod, $this, $this->configuration, $class);
835
        if ($findFromSqlMethod !== null) {
836
            $class->addMethodFromGenerator($findFromSqlMethod);
837
        }
838
839
        $findFromRawSqlMethodBody = <<<EOF
840
return \$this->tdbmService->findObjectsFromRawSql('$tableName', \$sql, \$parameters, \$mode, null, \$countSql);
841
EOF;
842
843
        $findFromRawSqlMethod = new MethodGenerator(
844
            'findFromRawSql',
845
            [
846
                new ParameterGenerator('sql', 'string'),
847
                new ParameterGenerator('parameters', 'array', []),
848
                (new ParameterGenerator('countSql', '?string'))->setDefaultValue(null),
849
                (new ParameterGenerator('mode', '?int'))->setDefaultValue(null),
850
            ],
851
            MethodGenerator::FLAG_PROTECTED,
852
            $findFromRawSqlMethodBody,
853
            (new DocBlockGenerator("Get a list of $beanClassWithoutNameSpace from a SQL query.",
854
                "Unlike the `find` and `findFromSql` methods, here you can pass the whole \$sql query.
855
856
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:
857
858
   \"SELECT $tableName .* FROM ...\"",
859
                [
860
                    new ParamTag('sql', ['string'], 'The sql query'),
861
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the query'),
862
                    new ParamTag('countSql', ['string', 'null'], 'The sql query that provides total count of rows (automatically computed if not provided)'),
863
                    new ParamTag('mode', ['int', 'null'], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.'),
864
                    new ReturnTag(['\\'.$beanClassName . '[]', '\\'.ResultIterator::class])
865
                ]))->setWordWrap(false)
866
        );
867
        $findFromRawSqlMethod->setReturnType('iterable');
868
        $findFromRawSqlMethod = $this->codeGeneratorListener->onBaseDaoFindFromRawSqlGenerated($findFromRawSqlMethod, $this, $this->configuration, $class);
869
        if ($findFromRawSqlMethod !== null) {
870
            $class->addMethodFromGenerator($findFromRawSqlMethod);
871
        }
872
873
        $findOneMethodBody = <<<EOF
874
return \$this->tdbmService->findObject('$tableName', \$filter, \$parameters, \$additionalTablesFetch);
875
EOF;
876
877
878
        $findOneMethod = new MethodGenerator(
879
            'findOne',
880
            [
881
                (new ParameterGenerator('filter'))->setDefaultValue(null),
882
                new ParameterGenerator('parameters', 'array', []),
883
                new ParameterGenerator('additionalTablesFetch', 'array', []),
884
            ],
885
            MethodGenerator::FLAG_PROTECTED,
886
            $findOneMethodBody,
887
            (new DocBlockGenerator("Get a single $beanClassWithoutNameSpace specified by its filters.",
888
                null,
889
                [
890
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
891
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
892
                    new ParamTag('additionalTablesFetch', ['string[]'], 'A list of additional tables to fetch (for performance improvement)'),
893
                    new ReturnTag(['\\'.$beanClassName, 'null'])
894
                ]))->setWordWrap(false)
895
        );
896
        $findOneMethod->setReturnType("?$beanClassName");
897
        $findOneMethod = $this->codeGeneratorListener->onBaseDaoFindOneGenerated($findOneMethod, $this, $this->configuration, $class);
898
        if ($findOneMethod !== null) {
899
            $class->addMethodFromGenerator($findOneMethod);
900
        }
901
902
        $findOneFromSqlMethodBody = <<<EOF
903
return \$this->tdbmService->findObjectFromSql('$tableName', \$from, \$filter, \$parameters);
904
EOF;
905
906
        $findOneFromSqlMethod = new MethodGenerator(
907
            'findOneFromSql',
908
            [
909
                new ParameterGenerator('from', 'string'),
910
                (new ParameterGenerator('filter'))->setDefaultValue(null),
911
                new ParameterGenerator('parameters', 'array', []),
912
            ],
913
            MethodGenerator::FLAG_PROTECTED,
914
            $findOneFromSqlMethodBody,
915
            new DocBlockGenerator("Get a single $beanClassWithoutNameSpace specified by its filters.",
916
                "Unlike the `findOne` method that guesses the FROM part of the statement, here you can pass the \$from part.
917
918
You should not put an alias on the main table name. So your \$from variable should look like:
919
920
    \"$tableName JOIN ... ON ...\"",
921
                [
922
                    new ParamTag('from', ['string'], 'The sql from statement'),
923
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
924
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
925
                    new ReturnTag(['\\'.$beanClassName, 'null'])
926
                ])
927
        );
928
        $findOneFromSqlMethod->setReturnType("?$beanClassName");
929
        $findOneFromSqlMethod = $this->codeGeneratorListener->onBaseDaoFindOneFromSqlGenerated($findOneFromSqlMethod, $this, $this->configuration, $class);
930
        if ($findOneFromSqlMethod !== null) {
931
            $class->addMethodFromGenerator($findOneFromSqlMethod);
932
        }
933
934
935
        $setDefaultSortMethod = new MethodGenerator(
936
            'setDefaultSort',
937
            [
938
                new ParameterGenerator('defaultSort', 'string'),
939
            ],
940
            MethodGenerator::FLAG_PUBLIC,
941
            '$this->defaultSort = $defaultSort;',
942
            new DocBlockGenerator("Sets the default column for default sorting.",
943
            null,
944
                [
945
                    new ParamTag('defaultSort', ['string']),
946
                ])
947
        );
948
        $setDefaultSortMethod->setReturnType('void');
949
        $setDefaultSortMethod = $this->codeGeneratorListener->onBaseDaoSetDefaultSortGenerated($setDefaultSortMethod, $this, $this->configuration, $class);
950
        if ($setDefaultSortMethod !== null) {
951
            $class->addMethodFromGenerator($setDefaultSortMethod);
952
        }
953
954
        foreach ($findByDaoCodeMethods as $method) {
955
            $class->addMethodFromGenerator($method);
956
        }
957
958
        return $file;
959
    }
960
961
    /**
962
     * Tries to find a @defaultSort annotation in one of the columns.
963
     *
964
     * @param Table $table
965
     *
966
     * @return mixed[] First item: column name, Second item: column order (asc/desc)
967
     */
968
    private function getDefaultSortColumnFromAnnotation(Table $table): array
969
    {
970
        $defaultSort = null;
971
        $defaultSortDirection = null;
972
        foreach ($table->getColumns() as $column) {
973
            $comments = $column->getComment();
974
            $matches = [];
975
            if ($comments !== null && preg_match('/@defaultSort(\((desc|asc)\))*/', $comments, $matches) != 0) {
976
                $defaultSort = $column->getName();
977
                if (count($matches) === 3) {
978
                    $defaultSortDirection = $matches[2];
979
                } else {
980
                    $defaultSortDirection = 'ASC';
981
                }
982
            }
983
        }
984
985
        return [$defaultSort, $defaultSortDirection];
986
    }
987
988
    /**
989
     * @param string $beanNamespace
990
     * @param string $beanClassName
991
     *
992
     * @return MethodGenerator[]
993
     */
994
    private function generateFindByDaoCode(string $beanNamespace, string $beanClassName, ClassGenerator $class): array
995
    {
996
        $methods = [];
997
        foreach ($this->removeDuplicateIndexes($this->table->getIndexes()) as $index) {
998
            if (!$index->isPrimary()) {
999
                $method = $this->generateFindByDaoCodeForIndex($index, $beanNamespace, $beanClassName);
1000
1001
                if ($method !== null) {
1002
                    $method = $this->codeGeneratorListener->onBaseDaoFindByIndexGenerated($method, $index, $this, $this->configuration, $class);
1003
                    if ($method !== null) {
1004
                        $methods[] = $method;
1005
                    }
1006
                }
1007
            }
1008
        }
1009
1010
        return $methods;
1011
    }
1012
1013
    /**
1014
     * Remove identical indexes (indexes on same columns)
1015
     *
1016
     * @param Index[] $indexes
1017
     * @return Index[]
1018
     */
1019
    private function removeDuplicateIndexes(array $indexes): array
1020
    {
1021
        $indexesByKey = [];
1022
        foreach ($indexes as $index) {
1023
            $indexesByKey[implode('__`__', $index->getUnquotedColumns())] = $index;
1024
        }
1025
1026
        return array_values($indexesByKey);
1027
    }
1028
1029
    /**
1030
     * @param Index  $index
1031
     * @param string $beanNamespace
1032
     * @param string $beanClassName
1033
     *
1034
     * @return MethodGenerator|null
1035
     */
1036
    private function generateFindByDaoCodeForIndex(Index $index, string $beanNamespace, string $beanClassName): ?MethodGenerator
1037
    {
1038
        $columns = $index->getColumns();
1039
        $usedBeans = [];
1040
1041
        /**
1042
         * The list of elements building this index (expressed as columns or foreign keys)
1043
         * @var AbstractBeanPropertyDescriptor[]
1044
         */
1045
        $elements = [];
1046
1047
        foreach ($columns as $column) {
1048
            $fk = $this->isPartOfForeignKey($this->table, $this->table->getColumn($column));
1049
            if ($fk !== null) {
1050
                if (!in_array($fk, $elements)) {
1051
                    $elements[] = new ObjectBeanPropertyDescriptor($this->table, $fk, $this->namingStrategy, $this->beanNamespace);
1052
                }
1053
            } else {
1054
                $elements[] = new ScalarBeanPropertyDescriptor($this->table, $this->table->getColumn($column), $this->namingStrategy, $this->annotationParser);
1055
            }
1056
        }
1057
1058
        // If the index is actually only a foreign key, let's bypass it entirely.
1059
        if (count($elements) === 1 && $elements[0] instanceof ObjectBeanPropertyDescriptor) {
1060
            return null;
1061
        }
1062
1063
        $parameters = [];
1064
        //$functionParameters = [];
1065
        $first = true;
1066
        foreach ($elements as $element) {
1067
            $parameter = new ParameterGenerator(ltrim($element->getVariableName(), '$'));
1068
            if (!$first) {
1069
                $parameterType = '?';
1070
                //$functionParameter = '?';
1071
            } else {
1072
                $parameterType = '';
1073
                //$functionParameter = '';
1074
            }
1075
            $parameterType .= $element->getPhpType();
1076
            $parameter->setType($parameterType);
1077
            if (!$first) {
1078
                $parameter->setDefaultValue(null);
1079
            }
1080
            //$functionParameter .= $element->getPhpType();
1081
            $elementClassName = $element->getClassName();
1082
            if ($elementClassName) {
1083
                $usedBeans[] = $beanNamespace.'\\'.$elementClassName;
1084
            }
1085
            //$functionParameter .= ' '.$element->getVariableName();
1086
            if ($first) {
1087
                $first = false;
1088
            } /*else {
1089
                $functionParameter .= ' = null';
1090
            }*/
1091
            //$functionParameters[] = $functionParameter;
1092
            $parameters[] = $parameter;
1093
        }
1094
1095
        //$functionParametersString = implode(', ', $functionParameters);
1096
1097
        $count = 0;
1098
1099
        $params = [];
1100
        $filterArrayCode = '';
1101
        $commentArguments = [];
1102
        $first = true;
1103
        foreach ($elements as $element) {
1104
            $params[] = $element->getParamAnnotation();
1105
            if ($element instanceof ScalarBeanPropertyDescriptor) {
1106
                $filterArrayCode .= '            '.var_export($element->getColumnName(), true).' => '.$element->getVariableName().",\n";
1107
            } elseif ($element instanceof ObjectBeanPropertyDescriptor) {
1108
                $foreignKey = $element->getForeignKey();
1109
                $columns = SafeFunctions::arrayCombine($foreignKey->getLocalColumns(), $foreignKey->getForeignColumns());
1110
                ++$count;
1111
                $foreignTable = $this->schema->getTable($foreignKey->getForeignTableName());
1112
                foreach ($columns as $localColumn => $foreignColumn) {
1113
                    // 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.
1114
                    $targetedElement = new ScalarBeanPropertyDescriptor($foreignTable, $foreignTable->getColumn($foreignColumn), $this->namingStrategy, $this->annotationParser);
1115
                    if ($first) {
1116
                        // First parameter for index is not nullable
1117
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => '.$element->getVariableName().'->'.$targetedElement->getGetterName()."(),\n";
1118
                    } else {
1119
                        // Other parameters for index is not nullable
1120
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => ('.$element->getVariableName().' !== null) ? '.$element->getVariableName().'->'.$targetedElement->getGetterName()."() : null,\n";
1121
                    }
1122
                }
1123
            }
1124
            $commentArguments[] = substr($element->getVariableName(), 1);
1125
            if ($first) {
1126
                $first = false;
1127
            }
1128
        }
1129
1130
        //$paramsString = implode("\n", $params);
1131
1132
1133
        $methodName = $this->namingStrategy->getFindByIndexMethodName($index, $elements);
1134
1135
        $method = new MethodGenerator($methodName);
1136
1137
        if ($index->isUnique()) {
1138
            $parameters[] = new ParameterGenerator('additionalTablesFetch', 'array', []);
1139
            $params[] = new ParamTag('additionalTablesFetch', [ 'string[]' ], 'A list of additional tables to fetch (for performance improvement)');
1140
            $params[] = new ReturnTag([ '\\'.$beanNamespace.'\\'.$beanClassName, 'null' ]);
1141
            $method->setReturnType('?\\'.$beanNamespace.'\\'.$beanClassName);
1142
1143
            $docBlock = new DocBlockGenerator("Get a $beanClassName filtered by ".implode(', ', $commentArguments). '.', null, $params);
1144
            $docBlock->setWordWrap(false);
1145
1146
            $body = "\$filter = [
1147
".$filterArrayCode."        ];
1148
return \$this->findOne(\$filter, [], \$additionalTablesFetch);
1149
";
1150
        } else {
1151
            $parameters[] = (new ParameterGenerator('orderBy'))->setDefaultValue(null);
1152
            $params[] = new ParamTag('orderBy', [ 'mixed' ], 'The order string');
1153
            $parameters[] = new ParameterGenerator('additionalTablesFetch', 'array', []);
1154
            $params[] = new ParamTag('additionalTablesFetch', [ 'string[]' ], 'A list of additional tables to fetch (for performance improvement)');
1155
            $parameters[] = (new ParameterGenerator('mode', '?int'))->setDefaultValue(null);
1156
            $params[] = new ParamTag('mode', [ 'int', 'null' ], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.');
1157
            $params[] = new ReturnTag([ '\\'.$beanNamespace.'\\'.$beanClassName.'[]', '\\'.ResultIterator::class ]);
1158
            $method->setReturnType('iterable');
1159
1160
            $docBlock = new DocBlockGenerator("Get a list of $beanClassName filtered by ".implode(', ', $commentArguments).".", null, $params);
1161
            $docBlock->setWordWrap(false);
1162
1163
            $body = "\$filter = [
1164
".$filterArrayCode."        ];
1165
return \$this->find(\$filter, [], \$orderBy, \$additionalTablesFetch, \$mode);
1166
";
1167
        }
1168
1169
        $method->setParameters($parameters);
1170
        $method->setDocBlock($docBlock);
1171
        $method->setBody($body);
1172
1173
        return $method;
1174
    }
1175
1176
    /**
1177
     * Generates the code for the getUsedTable protected method.
1178
     *
1179
     * @return MethodGenerator
1180
     */
1181
    private function generateGetUsedTablesCode(): MethodGenerator
1182
    {
1183
        $hasParentRelationship = $this->schemaAnalyzer->getParentRelationship($this->table->getName()) !== null;
1184
        if ($hasParentRelationship) {
1185
            $code = sprintf('$tables = parent::getUsedTables();
1186
$tables[] = %s;
1187
1188
return $tables;', var_export($this->table->getName(), true));
1189
        } else {
1190
            $code = sprintf('        return [ %s ];', var_export($this->table->getName(), true));
1191
        }
1192
1193
        $method = new MethodGenerator('getUsedTables');
1194
        $method->setDocBlock('Returns an array of used tables by this bean (from parent to child relationship).');
1195
        $method->getDocBlock()->setTag(new ReturnTag(['string[]']));
1196
        $method->setReturnType('array');
1197
        $method->setBody($code);
1198
1199
        return $method;
1200
    }
1201
1202
    private function generateOnDeleteCode(): ?MethodGenerator
1203
    {
1204
        $code = '';
1205
        $relationships = $this->getPropertiesForTable($this->table);
1206
        foreach ($relationships as $relationship) {
1207
            if ($relationship instanceof ObjectBeanPropertyDescriptor) {
1208
                $code .= sprintf('$this->setRef('.var_export($relationship->getForeignKey()->getName(), true).', null, '.var_export($this->table->getName(), true).");\n");
1209
            }
1210
        }
1211
1212
        if (!$code) {
1213
            return null;
1214
        }
1215
1216
        $method = new MethodGenerator('onDelete');
1217
        $method->setDocBlock('Method called when the bean is removed from database.');
1218
        $method->setReturnType('void');
1219
        $method->setBody('parent::onDelete();
1220
'.$code);
1221
1222
        return $method;
1223
    }
1224
1225
    private function generateCloneCode(): MethodGenerator
1226
    {
1227
        $code = '';
1228
1229
        foreach ($this->beanPropertyDescriptors as $beanPropertyDescriptor) {
1230
            $code .= $beanPropertyDescriptor->getCloneRule();
1231
        }
1232
1233
        $method = new MethodGenerator('__clone');
1234
        $method->setBody('parent::__clone();
1235
'.$code);
1236
1237
        return $method;
1238
    }
1239
1240
    /**
1241
     * Returns the bean class name (without the namespace).
1242
     *
1243
     * @return string
1244
     */
1245
    public function getBeanClassName() : string
1246
    {
1247
        return $this->namingStrategy->getBeanClassName($this->table->getName());
1248
    }
1249
1250
    /**
1251
     * Returns the base bean class name (without the namespace).
1252
     *
1253
     * @return string
1254
     */
1255
    public function getBaseBeanClassName() : string
1256
    {
1257
        return $this->namingStrategy->getBaseBeanClassName($this->table->getName());
1258
    }
1259
1260
    /**
1261
     * Returns the DAO class name (without the namespace).
1262
     *
1263
     * @return string
1264
     */
1265
    public function getDaoClassName() : string
1266
    {
1267
        return $this->namingStrategy->getDaoClassName($this->table->getName());
1268
    }
1269
1270
    /**
1271
     * Returns the base DAO class name (without the namespace).
1272
     *
1273
     * @return string
1274
     */
1275
    public function getBaseDaoClassName() : string
1276
    {
1277
        return $this->namingStrategy->getBaseDaoClassName($this->table->getName());
1278
    }
1279
1280
    /**
1281
     * Returns the table used to build this bean.
1282
     *
1283
     * @return Table
1284
     */
1285
    public function getTable(): Table
1286
    {
1287
        return $this->table;
1288
    }
1289
1290
    /**
1291
     * Returns the extended bean class name (without the namespace), or null if the bean is not extended.
1292
     *
1293
     * @return string
1294
     */
1295
    public function getExtendedBeanClassName(): ?string
1296
    {
1297
        $parentFk = $this->schemaAnalyzer->getParentRelationship($this->table->getName());
1298
        if ($parentFk !== null) {
1299
            return $this->namingStrategy->getBeanClassName($parentFk->getForeignTableName());
1300
        } else {
1301
            return null;
1302
        }
1303
    }
1304
1305
    /**
1306
     * @return string
1307
     */
1308
    public function getBeanNamespace(): string
1309
    {
1310
        return $this->beanNamespace;
1311
    }
1312
1313
    /**
1314
     * @return string
1315
     */
1316
    public function getGeneratedBeanNamespace(): string
1317
    {
1318
        return $this->generatedBeanNamespace;
1319
    }
1320
}
1321