Passed
Pull Request — master (#176)
by
unknown
04:40
created

BeanDescriptor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 32
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
c 0
b 0
f 0
dl 0
loc 32
rs 9.7666
cc 1
nc 1
nop 15

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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