Passed
Pull Request — 5.1 (#190)
by
unknown
09:45 queued 23s
created

BeanDescriptor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

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