Passed
Pull Request — 5.1 (#190)
by
unknown
08:39
created

BeanDescriptor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\TDBM\Utils;
5
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\Index;
8
use Doctrine\DBAL\Schema\Schema;
9
use Doctrine\DBAL\Schema\Table;
10
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
11
use JsonSerializable;
12
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
13
use PhpParser\Comment\Doc;
14
use Ramsey\Uuid\Uuid;
15
use TheCodingMachine\TDBM\AbstractTDBMObject;
16
use TheCodingMachine\TDBM\AlterableResultIterator;
17
use TheCodingMachine\TDBM\ConfigurationInterface;
18
use TheCodingMachine\TDBM\ResultIterator;
19
use TheCodingMachine\TDBM\SafeFunctions;
20
use TheCodingMachine\TDBM\Schema\ForeignKey;
21
use TheCodingMachine\TDBM\Schema\ForeignKeys;
22
use TheCodingMachine\TDBM\TDBMException;
23
use TheCodingMachine\TDBM\TDBMSchemaAnalyzer;
24
use TheCodingMachine\TDBM\TDBMService;
25
use TheCodingMachine\TDBM\Utils\Annotation\AbstractTraitAnnotation;
26
use TheCodingMachine\TDBM\Utils\Annotation\AddInterfaceOnDao;
27
use TheCodingMachine\TDBM\Utils\Annotation\AddTrait;
28
use TheCodingMachine\TDBM\Utils\Annotation\AddTraitOnDao;
29
use TheCodingMachine\TDBM\Utils\Annotation\AnnotationParser;
30
use TheCodingMachine\TDBM\Utils\Annotation\AddInterface;
31
use Zend\Code\Generator\AbstractMemberGenerator;
32
use Zend\Code\Generator\ClassGenerator;
33
use Zend\Code\Generator\DocBlock\Tag;
34
use Zend\Code\Generator\DocBlock\Tag\GenericTag;
35
use Zend\Code\Generator\DocBlock\Tag\ParamTag;
36
use Zend\Code\Generator\DocBlock\Tag\ReturnTag;
37
use Zend\Code\Generator\DocBlock\Tag\ThrowsTag;
38
use Zend\Code\Generator\DocBlock\Tag\VarTag;
39
use Zend\Code\Generator\DocBlockGenerator;
40
use Zend\Code\Generator\FileGenerator;
41
use Zend\Code\Generator\MethodGenerator;
42
use Zend\Code\Generator\ParameterGenerator;
43
use Zend\Code\Generator\PropertyGenerator;
44
use function implode;
45
use function var_export;
46
47
/**
48
 * This class represents a bean.
49
 */
50
class BeanDescriptor implements BeanDescriptorInterface
51
{
52
    /**
53
     * @var Table
54
     */
55
    private $table;
56
57
    /**
58
     * @var SchemaAnalyzer
59
     */
60
    private $schemaAnalyzer;
61
62
    /**
63
     * @var Schema
64
     */
65
    private $schema;
66
67
    /**
68
     * @var AbstractBeanPropertyDescriptor[]
69
     */
70
    private $beanPropertyDescriptors = [];
71
72
    /**
73
     * @var TDBMSchemaAnalyzer
74
     */
75
    private $tdbmSchemaAnalyzer;
76
77
    /**
78
     * @var NamingStrategyInterface
79
     */
80
    private $namingStrategy;
81
    /**
82
     * @var string
83
     */
84
    private $beanNamespace;
85
    /**
86
     * @var string
87
     */
88
    private $generatedBeanNamespace;
89
    /**
90
     * @var AnnotationParser
91
     */
92
    private $annotationParser;
93
    /**
94
     * @var string
95
     */
96
    private $daoNamespace;
97
    /**
98
     * @var string
99
     */
100
    private $generatedDaoNamespace;
101
    /**
102
     * @var CodeGeneratorListenerInterface
103
     */
104
    private $codeGeneratorListener;
105
    /**
106
     * @var ConfigurationInterface
107
     */
108
    private $configuration;
109
    /**
110
     * @var BeanRegistry
111
     */
112
    private $registry;
113
    /**
114
     * @var MethodDescriptorInterface[][]
115
     */
116
    private $descriptorsByMethodName = [];
117
    /**
118
     * @var DirectForeignKeyMethodDescriptor[]|null
119
     */
120
    private $directForeignKeysDescriptors = null;
121
    /**
122
     * @var PivotTableMethodsDescriptor[]|null
123
     */
124
    private $pivotTableDescriptors = null;
125
126
    public function __construct(
127
        Table $table,
128
        string $beanNamespace,
129
        string $generatedBeanNamespace,
130
        string $daoNamespace,
131
        string $generatedDaoNamespace,
132
        SchemaAnalyzer $schemaAnalyzer,
133
        Schema $schema,
134
        TDBMSchemaAnalyzer $tdbmSchemaAnalyzer,
135
        NamingStrategyInterface $namingStrategy,
136
        AnnotationParser $annotationParser,
137
        CodeGeneratorListenerInterface $codeGeneratorListener,
138
        ConfigurationInterface $configuration,
139
        BeanRegistry $registry
140
    ) {
141
        $this->table = $table;
142
        $this->beanNamespace = $beanNamespace;
143
        $this->generatedBeanNamespace = $generatedBeanNamespace;
144
        $this->daoNamespace = $daoNamespace;
145
        $this->generatedDaoNamespace = $generatedDaoNamespace;
146
        $this->schemaAnalyzer = $schemaAnalyzer;
147
        $this->schema = $schema;
148
        $this->tdbmSchemaAnalyzer = $tdbmSchemaAnalyzer;
149
        $this->namingStrategy = $namingStrategy;
150
        $this->annotationParser = $annotationParser;
151
        $this->codeGeneratorListener = $codeGeneratorListener;
152
        $this->configuration = $configuration;
153
        $this->registry = $registry;
154
    }
155
156
    public function initBeanPropertyDescriptors(): void
157
    {
158
        $this->beanPropertyDescriptors = $this->getProperties($this->table);
159
160
        //init the list of method names with regular properties names
161
        foreach ($this->beanPropertyDescriptors as $beanPropertyDescriptor) {
162
            $this->checkForDuplicate($beanPropertyDescriptor);
163
        }
164
    }
165
166
    /**
167
     * Returns the foreign-key the column is part of, if any. null otherwise.
168
     *
169
     * @param Table  $table
170
     * @param Column $column
171
     *
172
     * @return ForeignKeyConstraint|null
173
     */
174
    private function isPartOfForeignKey(Table $table, Column $column) : ?ForeignKeyConstraint
175
    {
176
        $localColumnName = $column->getName();
177
        foreach ($table->getForeignKeys() as $foreignKey) {
178
            foreach ($foreignKey->getUnquotedLocalColumns() as $columnName) {
179
                if ($columnName === $localColumnName) {
180
                    return $foreignKey;
181
                }
182
            }
183
        }
184
185
        return null;
186
    }
187
188
    /**
189
     * @return AbstractBeanPropertyDescriptor[]
190
     */
191
    public function getBeanPropertyDescriptors(): array
192
    {
193
        return $this->beanPropertyDescriptors;
194
    }
195
196
    /**
197
     * Returns the list of columns that are not nullable and not autogenerated for a given table and its parent.
198
     *
199
     * @return AbstractBeanPropertyDescriptor[]
200
     */
201
    public function getConstructorProperties(): array
202
    {
203
        $constructorProperties = array_filter($this->beanPropertyDescriptors, function (AbstractBeanPropertyDescriptor $property) {
204
            return !$property instanceof InheritanceReferencePropertyDescriptor && $property->isCompulsory();
205
        });
206
207
        return $constructorProperties;
208
    }
209
210
    /**
211
     * Returns the list of columns that have default values for a given table.
212
     *
213
     * @return AbstractBeanPropertyDescriptor[]
214
     */
215
    public function getPropertiesWithDefault(): array
216
    {
217
        $properties = $this->getPropertiesForTable($this->table);
218
        $defaultProperties = array_filter($properties, function (AbstractBeanPropertyDescriptor $property) {
219
            return $property->hasDefault();
220
        });
221
222
        return $defaultProperties;
223
    }
224
225
    /**
226
     * Returns the list of properties exposed as getters and setters in this class.
227
     *
228
     * @return AbstractBeanPropertyDescriptor[]
229
     */
230
    public function getExposedProperties(): array
231
    {
232
        $exposedProperties = array_filter($this->beanPropertyDescriptors, function (AbstractBeanPropertyDescriptor $property) {
233
            return !$property instanceof InheritanceReferencePropertyDescriptor && $property->getTable()->getName() === $this->table->getName();
234
        });
235
236
        return $exposedProperties;
237
    }
238
239
    /**
240
     * Returns the list of properties for this table (including parent tables).
241
     *
242
     * @param Table $table
243
     *
244
     * @return AbstractBeanPropertyDescriptor[]
245
     */
246
    private function getProperties(Table $table): array
247
    {
248
        // Security check: a table MUST have a primary key
249
        TDBMDaoGenerator::getPrimaryKeyColumnsOrFail($table);
250
251
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
252
        if ($parentRelationship) {
253
            $parentTable = $this->schema->getTable($parentRelationship->getForeignTableName());
254
            $properties = $this->getProperties($parentTable);
255
            // we merge properties by overriding property names.
256
            $localProperties = $this->getPropertiesForTable($table);
257
            foreach ($localProperties as $name => $property) {
258
                // We do not override properties if this is a primary key!
259
                if (!$property instanceof InheritanceReferencePropertyDescriptor && $property->isPrimaryKey()) {
260
                    continue;
261
                }
262
                $properties[$name] = $property;
263
            }
264
        } else {
265
            $properties = $this->getPropertiesForTable($table);
266
        }
267
268
        return $properties;
269
    }
270
271
    /**
272
     * Returns the list of properties for this table (ignoring parent tables).
273
     *
274
     * @param Table $table
275
     *
276
     * @return AbstractBeanPropertyDescriptor[]
277
     */
278
    private function getPropertiesForTable(Table $table): array
279
    {
280
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
281
        if ($parentRelationship) {
282
            $ignoreColumns = $parentRelationship->getUnquotedForeignColumns();
283
        } else {
284
            $ignoreColumns = [];
285
        }
286
287
        $beanPropertyDescriptors = [];
288
        foreach ($table->getColumns() as $column) {
289
            if (in_array($column->getName(), $ignoreColumns, true)) {
290
                continue;
291
            }
292
293
            $fk = $this->isPartOfForeignKey($table, $column);
294
            if ($fk !== null) {
295
                // Check that previously added descriptors are not added on same FK (can happen with multi key FK).
296
                foreach ($beanPropertyDescriptors as $beanDescriptor) {
297
                    if ($beanDescriptor instanceof ObjectBeanPropertyDescriptor && $beanDescriptor->getForeignKey() === $fk) {
298
                        continue 2;
299
                    }
300
                }
301
                $propertyDescriptor = new ObjectBeanPropertyDescriptor($table, $fk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser, $this->registry->getBeanForTableName($fk->getForeignTableName()));
302
                // Check that this property is not an inheritance relationship
303
                $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
304
                if ($parentRelationship !== null && $parentRelationship->getName() === $fk->getName()) {
305
                    $beanPropertyDescriptors[] = new InheritanceReferencePropertyDescriptor(
306
                        $table,
307
                        $column,
308
                        $this->namingStrategy,
309
                        $this->annotationParser,
310
                        $propertyDescriptor
311
                    );
312
                } else {
313
                    $beanPropertyDescriptors[] = $propertyDescriptor;
314
                }
315
            } else {
316
                $beanPropertyDescriptors[] = new ScalarBeanPropertyDescriptor($table, $column, $this->namingStrategy, $this->annotationParser);
317
            }
318
        }
319
320
        // Now, let's get the name of all properties and let's check there is no duplicate.
321
        /* @var $names AbstractBeanPropertyDescriptor[] */
322
        $names = [];
323
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
324
            $name = $beanDescriptor->getGetterName();
325
            if (isset($names[$name])) {
326
                $names[$name]->useAlternativeName();
327
                $beanDescriptor->useAlternativeName();
328
            } else {
329
                $names[$name] = $beanDescriptor;
330
            }
331
        }
332
333
        // Final check (throw exceptions if problem arises)
334
        $names = [];
335
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
336
            $name = $beanDescriptor->getGetterName();
337
            if (isset($names[$name])) {
338
                throw new TDBMException('Unsolvable name conflict while generating method name');
339
            } else {
340
                $names[$name] = $beanDescriptor;
341
            }
342
        }
343
344
        // Last step, let's rebuild the list with a map:
345
        $beanPropertyDescriptorsMap = [];
346
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
347
            $beanPropertyDescriptorsMap[$beanDescriptor->getVariableName()] = $beanDescriptor;
348
        }
349
350
        return $beanPropertyDescriptorsMap;
351
    }
352
353
    private function generateBeanConstructor() : MethodGenerator
354
    {
355
        $constructorProperties = $this->getConstructorProperties();
356
357
        $constructor = new MethodGenerator('__construct', [], MethodGenerator::FLAG_PUBLIC);
358
        $constructorDocBlock = new DocBlockGenerator('The constructor takes all compulsory arguments.');
359
        $constructorDocBlock->setWordWrap(false);
360
        $constructor->setDocBlock($constructorDocBlock);
361
362
        $assigns = [];
363
        $parentConstructorArguments = [];
364
365
        foreach ($constructorProperties as $property) {
366
            $parameter = new ParameterGenerator(ltrim($property->getVariableName(), '$'));
367
            if ($property->isTypeHintable()) {
368
                $parameter->setType($property->getPhpType());
369
            }
370
            $constructor->setParameter($parameter);
371
372
            $constructorDocBlock->setTag($property->getParamAnnotation());
373
374
            if ($property->getTable()->getName() === $this->table->getName()) {
375
                $assigns[] = $property->getConstructorAssignCode()."\n";
376
            } else {
377
                $parentConstructorArguments[] = $property->getVariableName();
378
            }
379
        }
380
381
        $parentConstructorCode = sprintf("parent::__construct(%s);\n", implode(', ', $parentConstructorArguments));
382
383
        foreach ($this->getPropertiesWithDefault() as $property) {
384
            $assigns[] = $property->assignToDefaultCode()."\n";
385
        }
386
387
        $body = $parentConstructorCode . implode('', $assigns);
388
389
        $constructor->setBody($body);
390
391
        return $constructor;
392
    }
393
394
    /**
395
     * Returns the descriptors of one-to-many relationships (the foreign keys pointing on this beans)
396
     *
397
     * @return DirectForeignKeyMethodDescriptor[]
398
     */
399
    private function getDirectForeignKeysDescriptors(): array
400
    {
401
        if ($this->directForeignKeysDescriptors !== null) {
402
            return $this->directForeignKeysDescriptors;
403
        }
404
        $fks = $this->tdbmSchemaAnalyzer->getIncomingForeignKeys($this->table->getName());
405
406
        $descriptors = [];
407
408
        foreach ($fks as $fk) {
409
            $desc = new DirectForeignKeyMethodDescriptor($fk, $this->table, $this->namingStrategy, $this->annotationParser, $this->beanNamespace);
410
            $this->checkForDuplicate($desc);
411
            $descriptors[] = $desc;
412
        }
413
414
        $this->directForeignKeysDescriptors = $descriptors;
415
        return $this->directForeignKeysDescriptors;
416
    }
417
418
    /**
419
     * @return PivotTableMethodsDescriptor[]
420
     */
421
    private function getPivotTableDescriptors(): array
422
    {
423
        if ($this->pivotTableDescriptors !== null) {
424
            return $this->pivotTableDescriptors;
425
        }
426
        $descs = [];
427
        foreach ($this->schemaAnalyzer->detectJunctionTables(true) as $table) {
428
            // There are exactly 2 FKs since this is a pivot table.
429
            $fks = array_values($table->getForeignKeys());
430
431
            if ($fks[0]->getForeignTableName() === $this->table->getName()) {
432
                list($localFk, $remoteFk) = $fks;
433
                $desc = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser);
434
                $this->checkForDuplicate($desc);
435
                $descs[] = $desc;
436
            }
437
            if ($fks[1]->getForeignTableName() === $this->table->getName()) {
438
                list($remoteFk, $localFk) = $fks;
439
                $desc = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser);
440
                $this->checkForDuplicate($desc);
441
                $descs[] = $desc;
442
            }
443
        }
444
445
        $this->pivotTableDescriptors = $descs;
446
        return $this->pivotTableDescriptors;
447
    }
448
449
    /**
450
     * Check the method name isn't already used and flag the associated descriptors to use their alternative names if it is the case
451
     */
452
    private function checkForDuplicate(MethodDescriptorInterface $descriptor): void
453
    {
454
        $name = $descriptor->getName();
455
        if (!isset($this->descriptorsByMethodName[$name])) {
456
            $this->descriptorsByMethodName[$name] = [];
457
        }
458
        $this->descriptorsByMethodName[$name][] = $descriptor;
459
        if (count($this->descriptorsByMethodName[$name]) > 1) {
460
            foreach ($this->descriptorsByMethodName[$name] as $duplicateDescriptor) {
461
                $duplicateDescriptor->useAlternativeName();
462
            }
463
        }
464
    }
465
466
    /**
467
     * Returns the list of method descriptors (and applies the alternative name if needed).
468
     *
469
     * @return RelationshipMethodDescriptorInterface[]
470
     */
471
    public function getMethodDescriptors(): array
472
    {
473
        $directForeignKeyDescriptors = $this->getDirectForeignKeysDescriptors();
474
        $pivotTableDescriptors = $this->getPivotTableDescriptors();
475
476
        return array_merge($directForeignKeyDescriptors, $pivotTableDescriptors);
477
    }
478
479
    public function generateJsonSerialize(): MethodGenerator
480
    {
481
        $tableName = $this->table->getName();
482
        $parentFk = $this->schemaAnalyzer->getParentRelationship($tableName);
483
484
        $method = new MethodGenerator('jsonSerialize');
485
        $method->setDocBlock(new DocBlockGenerator(
486
            'Serializes the object for JSON encoding.',
487
            null,
488
            [
489
                new ParamTag('$stopRecursion', ['bool'], 'Parameter used internally by TDBM to stop embedded objects from embedding other objects.'),
490
                new ReturnTag(['array'])
491
            ]
492
        ));
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);
836
EOF;
837
838
        $findAllMethod = new MethodGenerator(
839
            'findAll',
840
            [],
841
            MethodGenerator::FLAG_PUBLIC,
842
            $findAllBody,
843
            (new DocBlockGenerator(
844
                "Get all $beanClassWithoutNameSpace records.",
845
                null,
846
                [
847
                    new ReturnTag([ '\\'.$beanClassName.'[]', '\\'.ResultIterator::class ])
848
                ]
849
            ))->setWordWrap(false)
850
        );
851
        $findAllMethod->setReturnType('\\'.ResultIterator::class);
852
        $findAllMethod = $this->codeGeneratorListener->onBaseDaoFindAllGenerated($findAllMethod, $this, $this->configuration, $class);
853
        if ($findAllMethod !== null) {
854
            $class->addMethodFromGenerator($findAllMethod);
855
        }
856
857
        if (count($primaryKeyColumns) > 0) {
858
            $lazyLoadingParameterName = 'lazyLoading';
859
            $parameters = [];
860
            $parametersTag = [];
861
            $primaryKeyFilter = [];
862
863
            foreach ($primaryKeyColumns as $primaryKeyColumn) {
864
                if ($primaryKeyColumn === $lazyLoadingParameterName) {
865
                    throw new TDBMException('Primary Column name `' . $lazyLoadingParameterName . '` is not allowed.');
866
                }
867
                $phpType = TDBMDaoGenerator::dbalTypeToPhpType($this->table->getColumn($primaryKeyColumn)->getType());
868
                $parameters[] = new ParameterGenerator($primaryKeyColumn, $phpType);
869
                $parametersTag[] = new ParamTag($primaryKeyColumn, [$phpType]);
870
                $primaryKeyFilter[] = "'$primaryKeyColumn' => \$$primaryKeyColumn";
871
            }
872
            $parameters[] = new ParameterGenerator($lazyLoadingParameterName, 'bool', false);
873
            $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.');
874
            $parametersTag[] = new ReturnTag(['\\'.$beanClassName]);
875
            $parametersTag[] = new ThrowsTag('\\'.TDBMException::class);
876
877
            $getByIdMethod = new MethodGenerator(
878
                'getById',
879
                $parameters,
880
                MethodGenerator::FLAG_PUBLIC,
881
                "return \$this->tdbmService->findObjectByPk('$tableName', [" . implode(', ', $primaryKeyFilter) . "], [], \$$lazyLoadingParameterName);",
882
                (new DocBlockGenerator(
883
                    "Get $beanClassWithoutNameSpace specified by its ID (its primary key).",
884
                    'If the primary key does not exist, an exception is thrown.',
885
                    $parametersTag
886
                ))->setWordWrap(false)
887
            );
888
            $getByIdMethod->setReturnType($beanClassName);
889
            $getByIdMethod = $this->codeGeneratorListener->onBaseDaoGetByIdGenerated($getByIdMethod, $this, $this->configuration, $class);
890
            if ($getByIdMethod) {
891
                $class->addMethodFromGenerator($getByIdMethod);
892
            }
893
        }
894
895
        $deleteMethodBody = <<<EOF
896
if (\$cascade === true) {
897
    \$this->tdbmService->deleteCascade(\$obj);
898
} else {
899
    \$this->tdbmService->delete(\$obj);
900
}
901
EOF;
902
903
904
        $deleteMethod = new MethodGenerator(
905
            'delete',
906
            [
907
                new ParameterGenerator('obj', $beanClassName),
908
                new ParameterGenerator('cascade', 'bool', false)
909
            ],
910
            MethodGenerator::FLAG_PUBLIC,
911
            $deleteMethodBody,
912
            (new DocBlockGenerator(
913
                "Get all $beanClassWithoutNameSpace records.",
914
                null,
915
                [
916
                    new ParamTag('obj', ['\\'.$beanClassName], 'The object to delete'),
917
                    new ParamTag('cascade', ['bool'], 'If true, it will delete all objects linked to $obj'),
918
                ]
919
            ))->setWordWrap(false)
920
        );
921
        $deleteMethod->setReturnType('void');
922
        $deleteMethod = $this->codeGeneratorListener->onBaseDaoDeleteGenerated($deleteMethod, $this, $this->configuration, $class);
923
        if ($deleteMethod !== null) {
924
            $class->addMethodFromGenerator($deleteMethod);
925
        }
926
927
        $findMethodBody = <<<EOF
928
if (\$this->defaultSort && \$orderBy == null) {
929
    \$orderBy = '$tableName.'.\$this->defaultSort.' '.\$this->defaultDirection;
930
}
931
return \$this->tdbmService->findObjects('$tableName', \$filter, \$parameters, \$orderBy, \$additionalTablesFetch, \$mode);
932
EOF;
933
934
935
        $findMethod = new MethodGenerator(
936
            'find',
937
            [
938
                (new ParameterGenerator('filter'))->setDefaultValue(null),
939
                new ParameterGenerator('parameters', 'array', []),
940
                (new ParameterGenerator('orderBy'))->setDefaultValue(null),
941
                new ParameterGenerator('additionalTablesFetch', 'array', []),
942
                (new ParameterGenerator('mode', '?int'))->setDefaultValue(null),
943
            ],
944
            MethodGenerator::FLAG_PROTECTED,
945
            $findMethodBody,
946
            (new DocBlockGenerator(
947
                "Get all $beanClassWithoutNameSpace records.",
948
                null,
949
                [
950
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
951
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
952
                    new ParamTag('orderBy', ['mixed'], 'The order string'),
953
                    new ParamTag('additionalTablesFetch', ['string[]'], 'A list of additional tables to fetch (for performance improvement)'),
954
                    new ParamTag('mode', ['int', 'null'], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.'),
955
                    new ReturnTag(['\\' . $beanClassName . '[]', '\\'.ResultIterator::class])
956
                ]
957
            ))->setWordWrap(false)
958
        );
959
        $findMethod->setReturnType('\\'.ResultIterator::class);
960
        $findMethod = $this->codeGeneratorListener->onBaseDaoFindGenerated($findMethod, $this, $this->configuration, $class);
961
        if ($findMethod !== null) {
962
            $class->addMethodFromGenerator($findMethod);
963
        }
964
965
        $findFromSqlMethodBody = <<<EOF
966
if (\$this->defaultSort && \$orderBy == null) {
967
    \$orderBy = '$tableName.'.\$this->defaultSort.' '.\$this->defaultDirection;
968
}
969
return \$this->tdbmService->findObjectsFromSql('$tableName', \$from, \$filter, \$parameters, \$orderBy, \$mode);
970
EOF;
971
972
        $findFromSqlMethod = new MethodGenerator(
973
            'findFromSql',
974
            [
975
                new ParameterGenerator('from', 'string'),
976
                (new ParameterGenerator('filter'))->setDefaultValue(null),
977
                new ParameterGenerator('parameters', 'array', []),
978
                (new ParameterGenerator('orderBy'))->setDefaultValue(null),
979
                new ParameterGenerator('additionalTablesFetch', 'array', []),
980
                (new ParameterGenerator('mode', '?int'))->setDefaultValue(null),
981
            ],
982
            MethodGenerator::FLAG_PROTECTED,
983
            $findFromSqlMethodBody,
984
            (new DocBlockGenerator(
985
                "Get a list of $beanClassWithoutNameSpace specified by its filters.",
986
                "Unlike the `find` method that guesses the FROM part of the statement, here you can pass the \$from part.
987
988
You should not put an alias on the main table name. So your \$from variable should look like:
989
990
   \"$tableName JOIN ... ON ...\"",
991
                [
992
                    new ParamTag('from', ['string'], 'The sql from statement'),
993
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
994
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
995
                    new ParamTag('orderBy', ['mixed'], 'The order string'),
996
                    new ParamTag('additionalTablesFetch', ['string[]'], 'A list of additional tables to fetch (for performance improvement)'),
997
                    new ParamTag('mode', ['int', 'null'], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.'),
998
                    new ReturnTag(['\\'.$beanClassName . '[]', '\\'.ResultIterator::class])
999
                ]
1000
            ))->setWordWrap(false)
1001
        );
1002
        $findFromSqlMethod->setReturnType('\\'.ResultIterator::class);
1003
        $findFromSqlMethod = $this->codeGeneratorListener->onBaseDaoFindFromSqlGenerated($findFromSqlMethod, $this, $this->configuration, $class);
1004
        if ($findFromSqlMethod !== null) {
1005
            $class->addMethodFromGenerator($findFromSqlMethod);
1006
        }
1007
1008
        $findFromRawSqlMethodBody = <<<EOF
1009
return \$this->tdbmService->findObjectsFromRawSql('$tableName', \$sql, \$parameters, \$mode, null, \$countSql);
1010
EOF;
1011
1012
        $findFromRawSqlMethod = new MethodGenerator(
1013
            'findFromRawSql',
1014
            [
1015
                new ParameterGenerator('sql', 'string'),
1016
                new ParameterGenerator('parameters', 'array', []),
1017
                (new ParameterGenerator('countSql', '?string'))->setDefaultValue(null),
1018
                (new ParameterGenerator('mode', '?int'))->setDefaultValue(null),
1019
            ],
1020
            MethodGenerator::FLAG_PROTECTED,
1021
            $findFromRawSqlMethodBody,
1022
            (new DocBlockGenerator(
1023
                "Get a list of $beanClassWithoutNameSpace from a SQL query.",
1024
                "Unlike the `find` and `findFromSql` methods, here you can pass the whole \$sql query.
1025
1026
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:
1027
1028
   \"SELECT $tableName .* FROM ...\"",
1029
                [
1030
                    new ParamTag('sql', ['string'], 'The sql query'),
1031
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the query'),
1032
                    new ParamTag('countSql', ['string', 'null'], 'The sql query that provides total count of rows (automatically computed if not provided)'),
1033
                    new ParamTag('mode', ['int', 'null'], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.'),
1034
                    new ReturnTag(['\\'.$beanClassName . '[]', '\\'.ResultIterator::class])
1035
                ]
1036
            ))->setWordWrap(false)
1037
        );
1038
        $findFromRawSqlMethod->setReturnType('\\'.ResultIterator::class);
1039
        $findFromRawSqlMethod = $this->codeGeneratorListener->onBaseDaoFindFromRawSqlGenerated($findFromRawSqlMethod, $this, $this->configuration, $class);
1040
        if ($findFromRawSqlMethod !== null) {
1041
            $class->addMethodFromGenerator($findFromRawSqlMethod);
1042
        }
1043
1044
        $findOneMethodBody = <<<EOF
1045
return \$this->tdbmService->findObject('$tableName', \$filter, \$parameters, \$additionalTablesFetch);
1046
EOF;
1047
1048
1049
        $findOneMethod = new MethodGenerator(
1050
            'findOne',
1051
            [
1052
                (new ParameterGenerator('filter'))->setDefaultValue(null),
1053
                new ParameterGenerator('parameters', 'array', []),
1054
                new ParameterGenerator('additionalTablesFetch', 'array', []),
1055
            ],
1056
            MethodGenerator::FLAG_PROTECTED,
1057
            $findOneMethodBody,
1058
            (new DocBlockGenerator(
1059
                "Get a single $beanClassWithoutNameSpace specified by its filters.",
1060
                null,
1061
                [
1062
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
1063
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
1064
                    new ParamTag('additionalTablesFetch', ['string[]'], 'A list of additional tables to fetch (for performance improvement)'),
1065
                    new ReturnTag(['\\'.$beanClassName, 'null'])
1066
                ]
1067
            ))->setWordWrap(false)
1068
        );
1069
        $findOneMethod->setReturnType("?$beanClassName");
1070
        $findOneMethod = $this->codeGeneratorListener->onBaseDaoFindOneGenerated($findOneMethod, $this, $this->configuration, $class);
1071
        if ($findOneMethod !== null) {
1072
            $class->addMethodFromGenerator($findOneMethod);
1073
        }
1074
1075
        $findOneFromSqlMethodBody = <<<EOF
1076
return \$this->tdbmService->findObjectFromSql('$tableName', \$from, \$filter, \$parameters);
1077
EOF;
1078
1079
        $findOneFromSqlMethod = new MethodGenerator(
1080
            'findOneFromSql',
1081
            [
1082
                new ParameterGenerator('from', 'string'),
1083
                (new ParameterGenerator('filter'))->setDefaultValue(null),
1084
                new ParameterGenerator('parameters', 'array', []),
1085
            ],
1086
            MethodGenerator::FLAG_PROTECTED,
1087
            $findOneFromSqlMethodBody,
1088
            (new DocBlockGenerator(
1089
                "Get a single $beanClassWithoutNameSpace specified by its filters.",
1090
                "Unlike the `findOne` method that guesses the FROM part of the statement, here you can pass the \$from part.
1091
1092
You should not put an alias on the main table name. So your \$from variable should look like:
1093
1094
    \"$tableName JOIN ... ON ...\"",
1095
                [
1096
                    new ParamTag('from', ['string'], 'The sql from statement'),
1097
                    new ParamTag('filter', ['mixed'], 'The filter bag (see TDBMService::findObjects for complete description)'),
1098
                    new ParamTag('parameters', ['mixed[]'], 'The parameters associated with the filter'),
1099
                    new ReturnTag(['\\'.$beanClassName, 'null'])
1100
                ]
1101
            ))->setWordWrap(false)
1102
        );
1103
        $findOneFromSqlMethod->setReturnType("?$beanClassName");
1104
        $findOneFromSqlMethod = $this->codeGeneratorListener->onBaseDaoFindOneFromSqlGenerated($findOneFromSqlMethod, $this, $this->configuration, $class);
1105
        if ($findOneFromSqlMethod !== null) {
1106
            $class->addMethodFromGenerator($findOneFromSqlMethod);
1107
        }
1108
1109
1110
        $setDefaultSortMethod = new MethodGenerator(
1111
            'setDefaultSort',
1112
            [
1113
                new ParameterGenerator('defaultSort', 'string'),
1114
            ],
1115
            MethodGenerator::FLAG_PUBLIC,
1116
            '$this->defaultSort = $defaultSort;',
1117
            new DocBlockGenerator(
1118
                "Sets the default column for default sorting.",
1119
                null,
1120
                [
1121
                    new ParamTag('defaultSort', ['string']),
1122
                ]
1123
            )
1124
        );
1125
        $setDefaultSortMethod->setReturnType('void');
1126
        $setDefaultSortMethod = $this->codeGeneratorListener->onBaseDaoSetDefaultSortGenerated($setDefaultSortMethod, $this, $this->configuration, $class);
1127
        if ($setDefaultSortMethod !== null) {
1128
            $class->addMethodFromGenerator($setDefaultSortMethod);
1129
        }
1130
1131
        foreach ($findByDaoCodeMethods as $method) {
1132
            $class->addMethodFromGenerator($method);
1133
        }
1134
1135
        $file = $this->codeGeneratorListener->onBaseDaoGenerated($file, $this, $this->configuration);
1136
1137
        return $file;
1138
    }
1139
1140
    /**
1141
     * Tries to find a @defaultSort annotation in one of the columns.
1142
     *
1143
     * @param Table $table
1144
     *
1145
     * @return mixed[] First item: column name, Second item: column order (asc/desc)
1146
     */
1147
    private function getDefaultSortColumnFromAnnotation(Table $table): array
1148
    {
1149
        $defaultSort = null;
1150
        $defaultSortDirection = null;
1151
        foreach ($table->getColumns() as $column) {
1152
            $comments = $column->getComment();
1153
            $matches = [];
1154
            if ($comments !== null && preg_match('/@defaultSort(\((desc|asc)\))*/', $comments, $matches) != 0) {
1155
                $defaultSort = $column->getName();
1156
                if (count($matches) === 3) {
1157
                    $defaultSortDirection = $matches[2];
1158
                } else {
1159
                    $defaultSortDirection = 'ASC';
1160
                }
1161
            }
1162
        }
1163
1164
        return [$defaultSort, $defaultSortDirection];
1165
    }
1166
1167
    /**
1168
     * @param string $beanNamespace
1169
     * @param string $beanClassName
1170
     *
1171
     * @return MethodGenerator[]
1172
     */
1173
    private function generateFindByDaoCode(string $beanNamespace, string $beanClassName, ClassGenerator $class): array
1174
    {
1175
        $methods = [];
1176
        foreach ($this->removeDuplicateIndexes($this->table->getIndexes()) as $index) {
1177
            if (!$index->isPrimary()) {
1178
                $method = $this->generateFindByDaoCodeForIndex($index, $beanNamespace, $beanClassName);
1179
1180
                if ($method !== null) {
1181
                    $method = $this->codeGeneratorListener->onBaseDaoFindByIndexGenerated($method, $index, $this, $this->configuration, $class);
1182
                    if ($method !== null) {
1183
                        $methods[] = $method;
1184
                    }
1185
                }
1186
            }
1187
        }
1188
        usort($methods, static function (MethodGenerator $methodA, MethodGenerator $methodB) {
1189
            return $methodA->getName() <=> $methodB->getName();
1190
        });
1191
1192
        return $methods;
1193
    }
1194
1195
    /**
1196
     * Remove identical indexes (indexes on same columns)
1197
     *
1198
     * @param Index[] $indexes
1199
     * @return Index[]
1200
     */
1201
    private function removeDuplicateIndexes(array $indexes): array
1202
    {
1203
        $indexesByKey = [];
1204
        foreach ($indexes as $index) {
1205
            $key = implode('__`__', $index->getUnquotedColumns());
1206
            // Unique Index have precedence over non unique one
1207
            if (!isset($indexesByKey[$key]) || $index->isUnique()) {
1208
                $indexesByKey[$key] = $index;
1209
            }
1210
        }
1211
1212
        return array_values($indexesByKey);
1213
    }
1214
1215
    /**
1216
     * @param Index  $index
1217
     * @param string $beanNamespace
1218
     * @param string $beanClassName
1219
     *
1220
     * @return MethodGenerator|null
1221
     */
1222
    private function generateFindByDaoCodeForIndex(Index $index, string $beanNamespace, string $beanClassName): ?MethodGenerator
1223
    {
1224
        $columns = $index->getColumns();
1225
1226
        /**
1227
         * The list of elements building this index (expressed as columns or foreign keys)
1228
         * @var AbstractBeanPropertyDescriptor[]
1229
         */
1230
        $elements = [];
1231
1232
        foreach ($columns as $column) {
1233
            $fk = $this->isPartOfForeignKey($this->table, $this->table->getColumn($column));
1234
            if ($fk !== null) {
1235
                if (!isset($elements[$fk->getName()])) {
1236
                    $elements[$fk->getName()] = new ObjectBeanPropertyDescriptor($this->table, $fk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser, $this->registry->getBeanForTableName($fk->getForeignTableName()));
1237
                }
1238
            } else {
1239
                $elements[] = new ScalarBeanPropertyDescriptor($this->table, $this->table->getColumn($column), $this->namingStrategy, $this->annotationParser);
1240
            }
1241
        }
1242
        $elements = array_values($elements);
1243
1244
        // If the index is actually only a foreign key, let's bypass it entirely.
1245
        if (count($elements) === 1 && $elements[0] instanceof ObjectBeanPropertyDescriptor) {
1246
            return null;
1247
        }
1248
1249
        $parameters = [];
1250
        $first = true;
1251
        /** @var AbstractBeanPropertyDescriptor $element */
1252
        foreach ($elements as $element) {
1253
            $parameter = new ParameterGenerator(ltrim($element->getVariableName(), '$'));
1254
            if (!$first && !($element->isCompulsory() && $index->isUnique())) {
1255
                $parameterType = '?';
1256
            } else {
1257
                $parameterType = '';
1258
            }
1259
            $parameterType .= $element->getPhpType();
1260
            $parameter->setType($parameterType);
1261
            if (!$first && !($element->isCompulsory() && $index->isUnique())) {
1262
                $parameter->setDefaultValue(null);
1263
            }
1264
            if ($first) {
1265
                $first = false;
1266
            }
1267
            $parameters[] = $parameter;
1268
        }
1269
1270
        $params = [];
1271
        $filterArrayCode = '';
1272
        $commentArguments = [];
1273
        $first = true;
1274
        foreach ($elements as $element) {
1275
            $params[] = $element->getParamAnnotation();
1276
            if ($element instanceof ScalarBeanPropertyDescriptor) {
1277
                $filterArrayCode .= '            '.var_export($element->getColumnName(), true).' => '.$element->getVariableName().",\n";
1278
            } elseif ($element instanceof ObjectBeanPropertyDescriptor) {
1279
                $foreignKey = $element->getForeignKey();
1280
                $columns = SafeFunctions::arrayCombine($foreignKey->getLocalColumns(), $foreignKey->getForeignColumns());
1281
                $foreignTable = $this->schema->getTable($foreignKey->getForeignTableName());
1282
                foreach ($columns as $localColumn => $foreignColumn) {
1283
                    // 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.
1284
                    $targetedElement = new ScalarBeanPropertyDescriptor($foreignTable, $foreignTable->getColumn($foreignColumn), $this->namingStrategy, $this->annotationParser);
1285
                    if ($first || ($element->isCompulsory() && $index->isUnique())) {
1286
                        // First parameter for index is not nullable
1287
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => '.$element->getVariableName().'->'.$targetedElement->getGetterName()."(),\n";
1288
                    } else {
1289
                        // Other parameters for index is not nullable
1290
                        $filterArrayCode .= '            '.var_export($localColumn, true).' => ('.$element->getVariableName().' !== null) ? '.$element->getVariableName().'->'.$targetedElement->getGetterName()."() : null,\n";
1291
                    }
1292
                }
1293
            }
1294
            $commentArguments[] = substr($element->getVariableName(), 1);
1295
            if ($first) {
1296
                $first = false;
1297
            }
1298
        }
1299
1300
        $methodName = $this->namingStrategy->getFindByIndexMethodName($index, $elements);
1301
1302
        $method = new MethodGenerator($methodName);
1303
1304
        if ($index->isUnique()) {
1305
            $parameters[] = new ParameterGenerator('additionalTablesFetch', 'array', []);
1306
            $params[] = new ParamTag('additionalTablesFetch', [ 'string[]' ], 'A list of additional tables to fetch (for performance improvement)');
1307
            $params[] = new ReturnTag([ '\\'.$beanNamespace.'\\'.$beanClassName, 'null' ]);
1308
            $method->setReturnType('?\\'.$beanNamespace.'\\'.$beanClassName);
1309
1310
            $docBlock = new DocBlockGenerator("Get a $beanClassName filtered by ".implode(', ', $commentArguments). '.', null, $params);
1311
            $docBlock->setWordWrap(false);
1312
1313
            $body = "\$filter = [
1314
".$filterArrayCode."        ];
1315
return \$this->findOne(\$filter, [], \$additionalTablesFetch);
1316
";
1317
        } else {
1318
            $parameters[] = (new ParameterGenerator('orderBy'))->setDefaultValue(null);
1319
            $params[] = new ParamTag('orderBy', [ 'mixed' ], 'The order string');
1320
            $parameters[] = new ParameterGenerator('additionalTablesFetch', 'array', []);
1321
            $params[] = new ParamTag('additionalTablesFetch', [ 'string[]' ], 'A list of additional tables to fetch (for performance improvement)');
1322
            $parameters[] = (new ParameterGenerator('mode', '?int'))->setDefaultValue(null);
1323
            $params[] = new ParamTag('mode', [ 'int', 'null' ], 'Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.');
1324
            $params[] = new ReturnTag([ '\\'.$beanNamespace.'\\'.$beanClassName.'[]', '\\'.ResultIterator::class ]);
1325
            $method->setReturnType('\\'.ResultIterator::class);
1326
1327
            $docBlock = new DocBlockGenerator("Get a list of $beanClassName filtered by ".implode(', ', $commentArguments).".", null, $params);
1328
            $docBlock->setWordWrap(false);
1329
1330
            $body = "\$filter = [
1331
".$filterArrayCode."        ];
1332
return \$this->find(\$filter, [], \$orderBy, \$additionalTablesFetch, \$mode);
1333
";
1334
        }
1335
1336
        $method->setParameters($parameters);
1337
        $method->setDocBlock($docBlock);
1338
        $method->setBody($body);
1339
1340
        return $method;
1341
    }
1342
1343
    /**
1344
     * Generates the code for the getUsedTable protected method.
1345
     *
1346
     * @return MethodGenerator
1347
     */
1348
    private function generateGetUsedTablesCode(): MethodGenerator
1349
    {
1350
        $hasParentRelationship = $this->schemaAnalyzer->getParentRelationship($this->table->getName()) !== null;
1351
        if ($hasParentRelationship) {
1352
            $code = sprintf('$tables = parent::getUsedTables();
1353
$tables[] = %s;
1354
1355
return $tables;', var_export($this->table->getName(), true));
1356
        } else {
1357
            $code = sprintf('        return [ %s ];', var_export($this->table->getName(), true));
1358
        }
1359
1360
        $method = new MethodGenerator('getUsedTables');
1361
        $method->setDocBlock(new DocBlockGenerator(
1362
            'Returns an array of used tables by this bean (from parent to child relationship).',
1363
            null,
1364
            [new ReturnTag(['string[]'])]
1365
        ));
1366
        $method->setReturnType('array');
1367
        $method->setBody($code);
1368
1369
        return $method;
1370
    }
1371
1372
    private function generateOnDeleteCode(): ?MethodGenerator
1373
    {
1374
        $code = '';
1375
        $relationships = $this->getPropertiesForTable($this->table);
1376
        foreach ($relationships as $relationship) {
1377
            if ($relationship instanceof ObjectBeanPropertyDescriptor) {
1378
                $tdbmFk = ForeignKey::createFromFk($relationship->getForeignKey());
1379
                $code .= sprintf(
1380
                    "\$this->setRef(%s, null, %s);\n",
1381
                    var_export($tdbmFk->getCacheKey(), true),
1382
                    var_export($this->table->getName(), true)
1383
                );
1384
            }
1385
        }
1386
1387
        if (!$code) {
1388
            return null;
1389
        }
1390
1391
        $method = new MethodGenerator('onDelete');
1392
        $method->setDocBlock(new DocBlockGenerator('Method called when the bean is removed from database.'));
1393
        $method->setReturnType('void');
1394
        $method->setBody('parent::onDelete();
1395
'.$code);
1396
1397
        return $method;
1398
    }
1399
1400
    /**
1401
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1402
     * @return MethodGenerator
1403
     */
1404
    private function generateGetManyToManyRelationshipDescriptorCode(array $pivotTableMethodsDescriptors): ?MethodGenerator
1405
    {
1406
        if (empty($pivotTableMethodsDescriptors)) {
1407
            return null;
1408
        }
1409
1410
        $method = new MethodGenerator('_getManyToManyRelationshipDescriptor');
1411
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PUBLIC);
1412
        $method->setDocBlock(new DocBlockGenerator(
1413
            'Get the paths used for many to many relationships methods.',
1414
            null,
1415
            [new GenericTag('internal')]
1416
        ));
1417
        $method->setReturnType(ManyToManyRelationshipPathDescriptor::class);
1418
1419
        $parameter = new ParameterGenerator('pathKey');
1420
        $parameter->setType('string');
1421
        $method->setParameter($parameter);
1422
1423
        $code = 'switch ($pathKey) {'."\n";
1424
        foreach ($pivotTableMethodsDescriptors as $pivotTableMethodsDescriptor) {
1425
            $code .= '    case '.var_export($pivotTableMethodsDescriptor->getManyToManyRelationshipKey(), true).":\n";
1426
            $code .= '        return '.$pivotTableMethodsDescriptor->getManyToManyRelationshipInstantiationCode().";\n";
1427
        }
1428
        $code .= "    default:\n";
1429
        $code .= "        return parent::_getManyToManyRelationshipDescriptor(\$pathKey);\n";
1430
        $code .= "}\n";
1431
1432
        $method->setBody($code);
1433
1434
        return $method;
1435
    }
1436
1437
    /**
1438
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1439
     * @return MethodGenerator
1440
     */
1441
    private function generateGetManyToManyRelationshipDescriptorKeysCode(array $pivotTableMethodsDescriptors): ?MethodGenerator
1442
    {
1443
        if (empty($pivotTableMethodsDescriptors)) {
1444
            return null;
1445
        }
1446
1447
        $method = new MethodGenerator('_getManyToManyRelationshipDescriptorKeys');
1448
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PUBLIC);
1449
        $method->setReturnType('array');
1450
        $method->setDocBlock(new DocBlockGenerator(
1451
            'Returns the list of keys supported for many to many relationships',
1452
            null,
1453
            [new GenericTag('internal'), new ReturnTag('string[]')]
1454
        ));
1455
1456
        $keys = [];
1457
        foreach ($pivotTableMethodsDescriptors as $pivotTableMethodsDescriptor) {
1458
            $keys[] = var_export($pivotTableMethodsDescriptor->getManyToManyRelationshipKey(), true);
1459
        }
1460
1461
        $code = 'return array_merge(parent::_getManyToManyRelationshipDescriptorKeys(), ['.implode(', ', $keys).']);';
1462
1463
        $method->setBody($code);
1464
1465
        return $method;
1466
    }
1467
1468
    /**
1469
     * @param PivotTableMethodsDescriptor[] $pivotTableMethodsDescriptors
1470
     * @return MethodGenerator
1471
     */
1472
    private function generateCloneCode(array $pivotTableMethodsDescriptors): MethodGenerator
1473
    {
1474
        $precode = '';
1475
        $postcode = '';
1476
1477
        foreach ($this->beanPropertyDescriptors as $beanPropertyDescriptor) {
1478
            $postcode .= $beanPropertyDescriptor->getCloneRule();
1479
        }
1480
1481
        //cloning many to many relationships
1482
        foreach ($pivotTableMethodsDescriptors as $beanMethodDescriptor) {
1483
            $precode .= $beanMethodDescriptor->getCloneRule()."\n";
1484
        }
1485
1486
        $method = new MethodGenerator('__clone');
1487
        $method->setBody($precode."parent::__clone();\n".$postcode);
1488
1489
        return $method;
1490
    }
1491
1492
    /**
1493
     * Returns the bean class name (without the namespace).
1494
     *
1495
     * @return string
1496
     */
1497
    public function getBeanClassName() : string
1498
    {
1499
        return $this->namingStrategy->getBeanClassName($this->table->getName());
1500
    }
1501
1502
    /**
1503
     * Returns the base bean class name (without the namespace).
1504
     *
1505
     * @return string
1506
     */
1507
    public function getBaseBeanClassName() : string
1508
    {
1509
        return $this->namingStrategy->getBaseBeanClassName($this->table->getName());
1510
    }
1511
1512
    /**
1513
     * Returns the DAO class name (without the namespace).
1514
     *
1515
     * @return string
1516
     */
1517
    public function getDaoClassName() : string
1518
    {
1519
        return $this->namingStrategy->getDaoClassName($this->table->getName());
1520
    }
1521
1522
    /**
1523
     * Returns the base DAO class name (without the namespace).
1524
     *
1525
     * @return string
1526
     */
1527
    public function getBaseDaoClassName() : string
1528
    {
1529
        return $this->namingStrategy->getBaseDaoClassName($this->table->getName());
1530
    }
1531
1532
    /**
1533
     * Returns the table used to build this bean.
1534
     *
1535
     * @return Table
1536
     */
1537
    public function getTable(): Table
1538
    {
1539
        return $this->table;
1540
    }
1541
1542
    /**
1543
     * Returns the extended bean class name (without the namespace), or null if the bean is not extended.
1544
     *
1545
     * @return string
1546
     */
1547
    public function getExtendedBeanClassName(): ?string
1548
    {
1549
        $parentFk = $this->schemaAnalyzer->getParentRelationship($this->table->getName());
1550
        if ($parentFk !== null) {
1551
            return $this->namingStrategy->getBeanClassName($parentFk->getForeignTableName());
1552
        } else {
1553
            return null;
1554
        }
1555
    }
1556
1557
    /**
1558
     * @return string
1559
     */
1560
    public function getBeanNamespace(): string
1561
    {
1562
        return $this->beanNamespace;
1563
    }
1564
1565
    /**
1566
     * @return string
1567
     */
1568
    public function getGeneratedBeanNamespace(): string
1569
    {
1570
        return $this->generatedBeanNamespace;
1571
    }
1572
1573
    /**
1574
     * @param ForeignKeyConstraint[] $fks
1575
     */
1576
    private function generateGetForeignKeys(array $fks): MethodGenerator
1577
    {
1578
        $fkArray = [];
1579
1580
        foreach ($fks as $fk) {
1581
            $tdbmFk = ForeignKey::createFromFk($fk);
1582
1583
            // Override column name in case of inheritance
1584
            $foreignTableName = $fk->getForeignTableName();
1585
            $foreignColumns = $fk->getUnquotedForeignColumns();
1586
            foreach ($foreignColumns as $key => $foreignColumn) {
1587
                $descriptor = $this->findScalarPropertyDescriptorInTable($foreignTableName, $foreignColumn);
1588
                if ($descriptor instanceof InheritanceReferencePropertyDescriptor) {
1589
                    $foreignColumns[$key] = $this->foreignColumnNameInInheritance($descriptor, $foreignColumn);
1590
                }
1591
            }
1592
1593
            $fkArray[$tdbmFk->getCacheKey()] = [
1594
                ForeignKey::FOREIGN_TABLE => $fk->getForeignTableName(),
1595
                ForeignKey::LOCAL_COLUMNS => $fk->getUnquotedLocalColumns(),
1596
                ForeignKey::FOREIGN_COLUMNS => $foreignColumns,
1597
            ];
1598
        }
1599
1600
        ksort($fkArray);
1601
        foreach ($fkArray as $tableFks) {
1602
            ksort($tableFks);
1603
        }
1604
1605
        $code = <<<EOF
1606
if (\$tableName === %s) {
1607
    if (self::\$foreignKeys === null) {
1608
        self::\$foreignKeys = new ForeignKeys(%s);
1609
    }
1610
    return self::\$foreignKeys;
1611
}
1612
return parent::getForeignKeys(\$tableName);
1613
EOF;
1614
        $code = sprintf($code, var_export($this->getTable()->getName(), true), $this->psr2VarExport($fkArray, '        '));
1615
1616
        $method = new MethodGenerator('getForeignKeys');
1617
        $method->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
1618
        $method->setStatic(true);
1619
        $method->setDocBlock(new DocBlockGenerator('Internal method used to retrieve the list of foreign keys attached to this bean.'));
1620
        $method->setReturnType(ForeignKeys::class);
1621
1622
        $parameter = new ParameterGenerator('tableName');
1623
        $parameter->setType('string');
1624
        $method->setParameter($parameter);
1625
1626
1627
        $method->setBody($code);
1628
1629
        return $method;
1630
    }
1631
1632
    private function findScalarPropertyDescriptorInTable(string $tableName, string $columnName): ?ScalarBeanPropertyDescriptor
1633
    {
1634
        $beanDescriptor = $this->registry->getBeanForTableName($tableName);
1635
        foreach ($beanDescriptor->getBeanPropertyDescriptors() as $descriptor) {
1636
            if ($descriptor instanceof ScalarBeanPropertyDescriptor && $descriptor->getColumnName() === $columnName) {
1637
                return $descriptor;
1638
            }
1639
        }
1640
        return null;
1641
    }
1642
1643
    /**
1644
     * Extract the foreign column name from a InheritanceReferencePropertyDescriptor
1645
     */
1646
    private function foreignColumnNameInInheritance(InheritanceReferencePropertyDescriptor $descriptor, string $column): string
1647
    {
1648
        $nonReferenceDescriptor = $descriptor->getNonScalarReferencedPropertyDescriptor();
1649
        if ($nonReferenceDescriptor instanceof ScalarBeanPropertyDescriptor) {
1650
            return $nonReferenceDescriptor->getColumnName();
1651
        }
1652
        if ($nonReferenceDescriptor instanceof ObjectBeanPropertyDescriptor) {
0 ignored issues
show
introduced by
$nonReferenceDescriptor is always a sub-type of TheCodingMachine\TDBM\Ut...tBeanPropertyDescriptor.
Loading history...
1653
            $foreignKey = $nonReferenceDescriptor->getForeignKey();
1654
            $localColumns = $foreignKey->getLocalColumns();
1655
            $foreignColumns = $foreignKey->getForeignColumns();
1656
            foreach ($localColumns as $key => $localColumn) {
1657
                if ($localColumn === $column) {
1658
                    return $foreignColumns[$key];
1659
                }
1660
            }
1661
        }
1662
        return $column;
1663
    }
1664
1665
    /**
1666
     * @param mixed $var
1667
     * @param string $indent
1668
     * @return string
1669
     */
1670
    private function psr2VarExport($var, string $indent=''): string
1671
    {
1672
        if (is_array($var)) {
1673
            $indexed = array_keys($var) === range(0, count($var) - 1);
1674
            $r = [];
1675
            foreach ($var as $key => $value) {
1676
                $r[] = "$indent    "
1677
                    . ($indexed ? '' : $this->psr2VarExport($key) . ' => ')
1678
                    . $this->psr2VarExport($value, "$indent    ");
1679
            }
1680
            return "[\n" . implode(",\n", $r) . "\n" . $indent . ']';
1681
        }
1682
        return var_export($var, true);
1683
    }
1684
}
1685