Passed
Pull Request — master (#176)
by
unknown
02:38
created

BeanDescriptor::generateResultIteratorPhpCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 35
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 25
c 0
b 0
f 0
dl 0
loc 35
rs 9.52
cc 1
nc 1
nop 0
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM\Utils;
5
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\Index;
8
use Doctrine\DBAL\Schema\Schema;
9
use Doctrine\DBAL\Schema\Table;
10
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
11
use JsonSerializable;
12
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
13
use PhpParser\Comment\Doc;
14
use Ramsey\Uuid\Uuid;
15
use TheCodingMachine\TDBM\AbstractTDBMObject;
16
use TheCodingMachine\TDBM\AlterableResultIterator;
17
use TheCodingMachine\TDBM\ConfigurationInterface;
18
use TheCodingMachine\TDBM\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