Passed
Pull Request — master (#176)
by
unknown
02:38
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 ResultIterator 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(
1161
            "The $baseClassName class will iterate over results of $beanClassWithoutNameSpace class.",
1162
            null,
1163
            [new Tag\MethodTag('getIterator', ['\\' . $beanClassName . '[]'])]
1164
        ))->setWordWrap(false));
1165
1166
        $file = $this->codeGeneratorListener->onBaseResultIteratorGenerated($file, $this, $this->configuration);
1167
1168
        return $file;
1169
    }
1170
1171
    /**
1172
     * Tries to find a @defaultSort annotation in one of the columns.
1173
     *
1174
     * @param Table $table
1175
     *
1176
     * @return mixed[] First item: column name, Second item: column order (asc/desc)
1177
     */
1178
    private function getDefaultSortColumnFromAnnotation(Table $table): array
1179
    {
1180
        $defaultSort = null;
1181
        $defaultSortDirection = null;
1182
        foreach ($table->getColumns() as $column) {
1183
            $comments = $column->getComment();
1184
            $matches = [];
1185
            if ($comments !== null && preg_match('/@defaultSort(\((desc|asc)\))*/', $comments, $matches) != 0) {
1186
                $defaultSort = $column->getName();
1187
                if (count($matches) === 3) {
1188
                    $defaultSortDirection = $matches[2];
1189
                } else {
1190
                    $defaultSortDirection = 'ASC';
1191
                }
1192
            }
1193
        }
1194
1195
        return [$defaultSort, $defaultSortDirection];
1196
    }
1197
1198
    /**
1199
     * @param string $beanNamespace
1200
     * @param string $beanClassName
1201
     *
1202
     * @return MethodGenerator[]
1203
     */
1204
    private function generateFindByDaoCode(string $beanNamespace, string $beanClassName, ClassGenerator $class): array
1205
    {
1206
        $methods = [];
1207
        foreach ($this->removeDuplicateIndexes($this->table->getIndexes()) as $index) {
1208
            if (!$index->isPrimary()) {
1209
                $method = $this->generateFindByDaoCodeForIndex($index, $beanNamespace, $beanClassName);
1210
1211
                if ($method !== null) {
1212
                    $method = $this->codeGeneratorListener->onBaseDaoFindByIndexGenerated($method, $index, $this, $this->configuration, $class);
1213
                    if ($method !== null) {
1214
                        $methods[] = $method;
1215
                    }
1216
                }
1217
            }
1218
        }
1219
        usort($methods, static function (MethodGenerator $methodA, MethodGenerator $methodB) {
1220
            return $methodA->getName() <=> $methodB->getName();
1221
        });
1222
1223
        return $methods;
1224
    }
1225
1226
    /**
1227
     * Remove identical indexes (indexes on same columns)
1228
     *
1229
     * @param Index[] $indexes
1230
     * @return Index[]
1231
     */
1232
    private function removeDuplicateIndexes(array $indexes): array
1233
    {
1234
        $indexesByKey = [];
1235
        foreach ($indexes as $index) {
1236
            $key = implode('__`__', $index->getUnquotedColumns());
1237
            // Unique Index have precedence over non unique one
1238
            if (!isset($indexesByKey[$key]) || $index->isUnique()) {
1239
                $indexesByKey[$key] = $index;
1240
            }
1241
        }
1242
1243
        return array_values($indexesByKey);
1244
    }
1245
1246
    /**
1247
     * @param Index  $index
1248
     * @param string $beanNamespace
1249
     * @param string $beanClassName
1250
     *
1251
     * @return MethodGenerator|null
1252
     */
1253
    private function generateFindByDaoCodeForIndex(Index $index, string $beanNamespace, string $beanClassName): ?MethodGenerator
1254
    {
1255
        $columns = $index->getColumns();
1256
        $usedBeans = [];
1257
1258
        /**
1259
         * The list of elements building this index (expressed as columns or foreign keys)
1260
         * @var AbstractBeanPropertyDescriptor[]
1261
         */
1262
        $elements = [];
1263
1264
        foreach ($columns as $column) {
1265
            $fk = $this->isPartOfForeignKey($this->table, $this->table->getColumn($column));
1266
            if ($fk !== null) {
1267
                if (!isset($elements[$fk->getName()])) {
1268
                    $elements[$fk->getName()] = new ObjectBeanPropertyDescriptor($this->table, $fk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser, $this->registry->getBeanForTableName($fk->getForeignTableName()));
1269
                }
1270
            } else {
1271
                $elements[] = new ScalarBeanPropertyDescriptor($this->table, $this->table->getColumn($column), $this->namingStrategy, $this->annotationParser);
1272
            }
1273
        }
1274
        $elements = array_values($elements);
1275
1276
        // If the index is actually only a foreign key, let's bypass it entirely.
1277
        if (count($elements) === 1 && $elements[0] instanceof ObjectBeanPropertyDescriptor) {
1278
            return null;
1279
        }
1280
1281
        $parameters = [];
1282
        //$functionParameters = [];
1283
        $first = true;
1284
        /** @var AbstractBeanPropertyDescriptor $element */
1285
        foreach ($elements as $element) {
1286
            $parameter = new ParameterGenerator(ltrim($element->getVariableName(), '$'));
1287
            if (!$first && !($element->isCompulsory() && $index->isUnique())) {
1288
                $parameterType = '?';
1289
            //$functionParameter = '?';
1290
            } else {
1291
                $parameterType = '';
1292
                //$functionParameter = '';
1293
            }
1294
            $parameterType .= $element->getPhpType();
1295
            $parameter->setType($parameterType);
1296
            if (!$first && !($element->isCompulsory() && $index->isUnique())) {
1297
                $parameter->setDefaultValue(null);
1298
            }
1299
            //$functionParameter .= $element->getPhpType();
1300
            $elementClassName = $element->getClassName();
1301
            if ($elementClassName) {
1302
                $usedBeans[] = $beanNamespace.'\\'.$elementClassName;
1303
            }
1304
            //$functionParameter .= ' '.$element->getVariableName();
1305
            if ($first) {
1306
                $first = false;
1307
            } /*else {
1308
                $functionParameter .= ' = null';
1309
            }*/
1310
            //$functionParameters[] = $functionParameter;
1311
            $parameters[] = $parameter;
1312
        }
1313
1314
        //$functionParametersString = implode(', ', $functionParameters);
1315
1316
        $count = 0;
1317
1318
        $params = [];
1319
        $filterArrayCode = '';
1320
        $commentArguments = [];
1321
        $first = true;
1322
        foreach ($elements as $element) {
1323
            $params[] = $element->getParamAnnotation();
1324
            if ($element instanceof ScalarBeanPropertyDescriptor) {
1325
                $filterArrayCode .= '            '.var_export($element->getColumnName(), true).' => '.$element->getVariableName().",\n";
1326
            } elseif ($element instanceof ObjectBeanPropertyDescriptor) {
1327
                $foreignKey = $element->getForeignKey();
1328
                $columns = SafeFunctions::arrayCombine($foreignKey->getLocalColumns(), $foreignKey->getForeignColumns());
1329
                ++$count;
1330
                $foreignTable = $this->schema->getTable($foreignKey->getForeignTableName());
1331
                foreach ($columns as $localColumn => $foreignColumn) {
1332
                    // 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.
1333
                    $targetedElement = new ScalarBeanPropertyDescriptor($foreignTable, $foreignTable->getColumn($foreignColumn), $this->namingStrategy, $this->annotationParser);
1334
                    if ($first || $element->isCompulsory() && $index->isUnique()) {
1335
                        // First parameter for index is not nullable
1336
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => '.$element->getVariableName().'->'.$targetedElement->getGetterName()."(),\n";
1337
                    } else {
1338
                        // Other parameters for index is not nullable
1339
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => ('.$element->getVariableName().' !== null) ? '.$element->getVariableName().'->'.$targetedElement->getGetterName()."() : null,\n";
1340
                    }
1341
                }
1342
            }
1343
            $commentArguments[] = substr($element->getVariableName(), 1);
1344
            if ($first) {
1345
                $first = false;
1346
            }
1347
        }
1348
1349
        //$paramsString = implode("\n", $params);
1350
1351
1352
        $methodName = $this->namingStrategy->getFindByIndexMethodName($index, $elements);
1353
1354
        $method = new MethodGenerator($methodName);
1355
1356
        if ($index->isUnique()) {
1357
            $parameters[] = new ParameterGenerator('additionalTablesFetch', 'array', []);
1358
            $params[] = new ParamTag('additionalTablesFetch', [ 'string[]' ], 'A list of additional tables to fetch (for performance improvement)');
1359
            $params[] = new ReturnTag([ '\\'.$beanNamespace.'\\'.$beanClassName, 'null' ]);
1360
            $method->setReturnType('?\\'.$beanNamespace.'\\'.$beanClassName);
1361
1362
            $docBlock = new DocBlockGenerator("Get a $beanClassName filtered by ".implode(', ', $commentArguments). '.', null, $params);
1363
            $docBlock->setWordWrap(false);
1364
1365
            $body = "\$filter = [
1366
".$filterArrayCode."        ];
1367
return \$this->findOne(\$filter, [], \$additionalTablesFetch);
1368
";
1369
        } else {
1370
            $parameters[] = (new ParameterGenerator('orderBy'))->setDefaultValue(null);
1371
            $params[] = new ParamTag('orderBy', [ 'mixed' ], 'The order string');
1372
            $parameters[] = new ParameterGenerator('additionalTablesFetch', 'array', []);
1373
            $params[] = new ParamTag('additionalTablesFetch', [ 'string[]' ], 'A list of additional tables to fetch (for performance improvement)');
1374
            $parameters[] = (new ParameterGenerator('mode', '?int'))->setDefaultValue(null);
1375
            $params[] = new ParamTag('mode', [ 'int', 'null' ], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.');
1376
            $method->setReturnType($this->resultIteratorNamespace . '\\' . $this->getResultIteratorClassName());
1377
1378
            $docBlock = new DocBlockGenerator("Get a list of $beanClassName filtered by ".implode(', ', $commentArguments).".", null, $params);
1379
            $docBlock->setWordWrap(false);
1380
1381
            $body = "\$filter = [
1382
".$filterArrayCode."        ];
1383
return \$this->find(\$filter, [], \$orderBy, \$additionalTablesFetch, \$mode);
1384
";
1385
        }
1386
1387
        $method->setParameters($parameters);
1388
        $method->setDocBlock($docBlock);
1389
        $method->setBody($body);
1390
1391
        return $method;
1392
    }
1393
1394
    /**
1395
     * Generates the code for the getUsedTable protected method.
1396
     *
1397
     * @return MethodGenerator
1398
     */
1399
    private function generateGetUsedTablesCode(): MethodGenerator
1400
    {
1401
        $hasParentRelationship = $this->schemaAnalyzer->getParentRelationship($this->table->getName()) !== null;
1402
        if ($hasParentRelationship) {
1403
            $code = sprintf('$tables = parent::getUsedTables();
1404
$tables[] = %s;
1405
1406
return $tables;', var_export($this->table->getName(), true));
1407
        } else {
1408
            $code = sprintf('        return [ %s ];', var_export($this->table->getName(), true));
1409
        }
1410
1411
        $method = new MethodGenerator('getUsedTables');
1412
        $method->setDocBlock('Returns an array of used tables by this bean (from parent to child relationship).');
1413
        $method->getDocBlock()->setTag(new ReturnTag(['string[]']));
1414
        $method->setReturnType('array');
1415
        $method->setBody($code);
1416
1417
        return $method;
1418
    }
1419
1420
    private function generateOnDeleteCode(): ?MethodGenerator
1421
    {
1422
        $code = '';
1423
        $relationships = $this->getPropertiesForTable($this->table);
1424
        foreach ($relationships as $relationship) {
1425
            if ($relationship instanceof ObjectBeanPropertyDescriptor) {
1426
                $tdbmFk = ForeignKey::createFromFk($relationship->getForeignKey());
1427
                $code .= '$this->setRef('.var_export($tdbmFk->getCacheKey(), true).', null, '.var_export($this->table->getName(), true).");\n";
1428
            }
1429
        }
1430
1431
        if (!$code) {
1432
            return null;
1433
        }
1434
1435
        $method = new MethodGenerator('onDelete');
1436
        $method->setDocBlock('Method called when the bean is removed from database.');
1437
        $method->setReturnType('void');
1438
        $method->setBody('parent::onDelete();
1439
'.$code);
1440
1441
        return $method;
1442
    }
1443
1444
    /**
1445
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1446
     * @return MethodGenerator
1447
     */
1448
    private function generateGetManyToManyRelationshipDescriptorCode(array $pivotTableMethodsDescriptors): ?MethodGenerator
1449
    {
1450
        if (empty($pivotTableMethodsDescriptors)) {
1451
            return null;
1452
        }
1453
1454
        $method = new MethodGenerator('_getManyToManyRelationshipDescriptor');
1455
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PUBLIC);
1456
        $method->setDocBlock('Get the paths used for many to many relationships methods.');
1457
        $method->getDocBlock()->setTag(new GenericTag('internal'));
1458
        $method->setReturnType(ManyToManyRelationshipPathDescriptor::class);
1459
1460
        $parameter = new ParameterGenerator('pathKey');
1461
        $parameter->setType('string');
1462
        $method->setParameter($parameter);
1463
1464
        $code = 'switch ($pathKey) {'."\n";
1465
        foreach ($pivotTableMethodsDescriptors as $pivotTableMethodsDescriptor) {
1466
            $code .= '    case '.var_export($pivotTableMethodsDescriptor->getManyToManyRelationshipKey(), true).":\n";
1467
            $code .= '        return '.$pivotTableMethodsDescriptor->getManyToManyRelationshipInstantiationCode().";\n";
1468
        }
1469
        $code .= "    default:\n";
1470
        $code .= "        return parent::_getManyToManyRelationshipDescriptor(\$pathKey);\n";
1471
        $code .= "}\n";
1472
1473
        $method->setBody($code);
1474
1475
        return $method;
1476
    }
1477
1478
    /**
1479
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1480
     * @return MethodGenerator
1481
     */
1482
    private function generateGetManyToManyRelationshipDescriptorKeysCode(array $pivotTableMethodsDescriptors): ?MethodGenerator
1483
    {
1484
        if (empty($pivotTableMethodsDescriptors)) {
1485
            return null;
1486
        }
1487
1488
        $method = new MethodGenerator('_getManyToManyRelationshipDescriptorKeys');
1489
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PUBLIC);
1490
        $method->setReturnType('array');
1491
        $method->setDocBlock('Returns the list of keys supported for many to many relationships');
1492
        $method->getDocBlock()->setTag(new GenericTag('internal'));
1493
        $method->getDocBlock()->setTag(new ReturnTag('string[]'));
1494
1495
        $keys = [];
1496
        foreach ($pivotTableMethodsDescriptors as $pivotTableMethodsDescriptor) {
1497
            $keys[] = var_export($pivotTableMethodsDescriptor->getManyToManyRelationshipKey(), true);
1498
        }
1499
1500
        $code = 'return array_merge(parent::_getManyToManyRelationshipDescriptorKeys(), ['.implode(', ', $keys).']);';
1501
1502
        $method->setBody($code);
1503
1504
        return $method;
1505
    }
1506
1507
    /**
1508
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1509
     * @return MethodGenerator
1510
     */
1511
    private function generateCloneCode(array $pivotTableMethodsDescriptors): MethodGenerator
1512
    {
1513
        $precode = '';
1514
        $postcode = '';
1515
1516
        foreach ($this->beanPropertyDescriptors as $beanPropertyDescriptor) {
1517
            $postcode .= $beanPropertyDescriptor->getCloneRule();
1518
        }
1519
1520
        //cloning many to many relationships
1521
        foreach ($pivotTableMethodsDescriptors as $beanMethodDescriptor) {
1522
            $precode .= $beanMethodDescriptor->getCloneRule()."\n";
1523
        }
1524
1525
        $method = new MethodGenerator('__clone');
1526
        $method->setBody($precode."parent::__clone();\n".$postcode);
1527
1528
        return $method;
1529
    }
1530
1531
    /**
1532
     * Returns the bean class name (without the namespace).
1533
     *
1534
     * @return string
1535
     */
1536
    public function getBeanClassName() : string
1537
    {
1538
        return $this->namingStrategy->getBeanClassName($this->table->getName());
1539
    }
1540
1541
    /**
1542
     * Returns the base bean class name (without the namespace).
1543
     *
1544
     * @return string
1545
     */
1546
    public function getBaseBeanClassName() : string
1547
    {
1548
        return $this->namingStrategy->getBaseBeanClassName($this->table->getName());
1549
    }
1550
1551
    /**
1552
     * Returns the DAO class name (without the namespace).
1553
     *
1554
     * @return string
1555
     */
1556
    public function getDaoClassName() : string
1557
    {
1558
        return $this->namingStrategy->getDaoClassName($this->table->getName());
1559
    }
1560
1561
    /**
1562
     * Returns the base DAO class name (without the namespace).
1563
     *
1564
     * @return string
1565
     */
1566
    public function getBaseDaoClassName() : string
1567
    {
1568
        return $this->namingStrategy->getBaseDaoClassName($this->table->getName());
1569
    }
1570
1571
    /**
1572
     * Returns the ResultIterator class name (without the namespace).
1573
     *
1574
     * @return string
1575
     */
1576
    public function getResultIteratorClassName() : string
1577
    {
1578
        return $this->namingStrategy->getResultIteratorClassName($this->table->getName());
1579
    }
1580
1581
    /**
1582
     * Returns the base ResultIterator class name (without the namespace).
1583
     *
1584
     * @return string
1585
     */
1586
    public function getBaseResultIteratorClassName() : string
1587
    {
1588
        return $this->namingStrategy->getBaseResultIteratorClassName($this->table->getName());
1589
    }
1590
1591
    /**
1592
     * Returns the table used to build this bean.
1593
     *
1594
     * @return Table
1595
     */
1596
    public function getTable(): Table
1597
    {
1598
        return $this->table;
1599
    }
1600
1601
    /**
1602
     * Returns the extended bean class name (without the namespace), or null if the bean is not extended.
1603
     *
1604
     * @return string
1605
     */
1606
    public function getExtendedBeanClassName(): ?string
1607
    {
1608
        $parentFk = $this->schemaAnalyzer->getParentRelationship($this->table->getName());
1609
        if ($parentFk !== null) {
1610
            return $this->namingStrategy->getBeanClassName($parentFk->getForeignTableName());
1611
        } else {
1612
            return null;
1613
        }
1614
    }
1615
1616
    /**
1617
     * @return string
1618
     */
1619
    public function getBeanNamespace(): string
1620
    {
1621
        return $this->beanNamespace;
1622
    }
1623
1624
    /**
1625
     * @return string
1626
     */
1627
    public function getGeneratedBeanNamespace(): string
1628
    {
1629
        return $this->generatedBeanNamespace;
1630
    }
1631
1632
    /**
1633
     * @param ForeignKeyConstraint[] $fks
1634
     */
1635
    private function generateGetForeignKeys(array $fks): MethodGenerator
1636
    {
1637
        $fkArray = [];
1638
1639
        foreach ($fks as $fk) {
1640
            $tdbmFk = ForeignKey::createFromFk($fk);
1641
            $fkArray[$tdbmFk->getCacheKey()] = [
1642
                ForeignKey::FOREIGN_TABLE => $fk->getForeignTableName(),
1643
                ForeignKey::LOCAL_COLUMNS => $fk->getUnquotedLocalColumns(),
1644
                ForeignKey::FOREIGN_COLUMNS => $fk->getUnquotedForeignColumns(),
1645
            ];
1646
        }
1647
1648
        ksort($fkArray);
1649
        foreach ($fkArray as $tableFks) {
1650
            ksort($tableFks);
1651
        }
1652
1653
        $code = <<<EOF
1654
if (\$tableName === %s) {
1655
    if (self::\$foreignKeys === null) {
1656
        self::\$foreignKeys = new ForeignKeys(%s);
1657
    }
1658
    return self::\$foreignKeys;
1659
}
1660
return parent::getForeignKeys(\$tableName);
1661
EOF;
1662
        $code = sprintf($code, var_export($this->getTable()->getName(), true), $this->psr2VarExport($fkArray, '    '));
1663
1664
        $method = new MethodGenerator('getForeignKeys');
1665
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
1666
        $method->setStatic(true);
1667
        $method->setDocBlock('Internal method used to retrieve the list of foreign keys attached to this bean.');
1668
        $method->setReturnType(ForeignKeys::class);
1669
1670
        $parameter = new ParameterGenerator('tableName');
1671
        $parameter->setType('string');
1672
        $method->setParameter($parameter);
1673
1674
1675
        $method->setBody($code);
1676
1677
        return $method;
1678
    }
1679
1680
    /**
1681
     * @param mixed $var
1682
     * @param string $indent
1683
     * @return string
1684
     */
1685
    private function psr2VarExport($var, string $indent=''): string
1686
    {
1687
        if (is_array($var)) {
1688
            $indexed = array_keys($var) === range(0, count($var) - 1);
1689
            $r = [];
1690
            foreach ($var as $key => $value) {
1691
                $r[] = "$indent    "
1692
                    . ($indexed ? '' : $this->psr2VarExport($key) . ' => ')
1693
                    . $this->psr2VarExport($value, "$indent    ");
1694
            }
1695
            return "[\n" . implode(",\n", $r) . "\n" . $indent . ']';
1696
        }
1697
        return var_export($var, true);
1698
    }
1699
}
1700