Completed
Push — 4.2 ( edfa0f...bbb68b )
by David
59s
created

BeanDescriptor::generatePhpCode()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 61
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 61
rs 8.9392
c 0
b 0
f 0
cc 4
eloc 31
nc 8
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Mouf\Database\TDBM\Utils;
4
5
use Doctrine\DBAL\Schema\Column;
6
use Doctrine\DBAL\Schema\Index;
7
use Doctrine\DBAL\Schema\Schema;
8
use Doctrine\DBAL\Schema\Table;
9
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
10
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
11
use Mouf\Database\TDBM\TDBMException;
12
use Mouf\Database\TDBM\TDBMSchemaAnalyzer;
13
14
/**
15
 * This class represents a bean.
16
 */
17
class BeanDescriptor
18
{
19
    /**
20
     * @var Table
21
     */
22
    private $table;
23
24
    /**
25
     * @var SchemaAnalyzer
26
     */
27
    private $schemaAnalyzer;
28
29
    /**
30
     * @var Schema
31
     */
32
    private $schema;
33
34
    /**
35
     * @var AbstractBeanPropertyDescriptor[]
36
     */
37
    private $beanPropertyDescriptors = [];
38
39
    /**
40
     * @var TDBMSchemaAnalyzer
41
     */
42
    private $tdbmSchemaAnalyzer;
43
44
    public function __construct(Table $table, SchemaAnalyzer $schemaAnalyzer, Schema $schema, TDBMSchemaAnalyzer $tdbmSchemaAnalyzer)
45
    {
46
        $this->table = $table;
47
        $this->schemaAnalyzer = $schemaAnalyzer;
48
        $this->schema = $schema;
49
        $this->tdbmSchemaAnalyzer = $tdbmSchemaAnalyzer;
50
        $this->initBeanPropertyDescriptors();
51
    }
52
53
    private function initBeanPropertyDescriptors()
54
    {
55
        $this->beanPropertyDescriptors = $this->getProperties($this->table);
56
    }
57
58
    /**
59
     * Returns the foreign-key the column is part of, if any. null otherwise.
60
     *
61
     * @param Table  $table
62
     * @param Column $column
63
     *
64
     * @return ForeignKeyConstraint|null
65
     */
66
    private function isPartOfForeignKey(Table $table, Column $column)
67
    {
68
        $localColumnName = $column->getName();
69
        foreach ($table->getForeignKeys() as $foreignKey) {
70
            foreach ($foreignKey->getColumns() as $columnName) {
71
                if ($columnName === $localColumnName) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $columnName (integer) and $localColumnName (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
72
                    return $foreignKey;
73
                }
74
            }
75
        }
76
77
        return;
78
    }
79
80
    /**
81
     * @return AbstractBeanPropertyDescriptor[]
82
     */
83
    public function getBeanPropertyDescriptors()
84
    {
85
        return $this->beanPropertyDescriptors;
86
    }
87
88
    /**
89
     * Returns the list of columns that are not nullable and not autogenerated for a given table and its parent.
90
     *
91
     * @return AbstractBeanPropertyDescriptor[]
92
     */
93
    public function getConstructorProperties()
94
    {
95
        $constructorProperties = array_filter($this->beanPropertyDescriptors, function (AbstractBeanPropertyDescriptor $property) {
96
            return $property->isCompulsory();
97
        });
98
99
        return $constructorProperties;
100
    }
101
102
    /**
103
     * Returns the list of columns that have default values for a given table.
104
     *
105
     * @return AbstractBeanPropertyDescriptor[]
106
     */
107
    public function getPropertiesWithDefault()
108
    {
109
        $properties = $this->getPropertiesForTable($this->table);
110
        $defaultProperties = array_filter($properties, function (AbstractBeanPropertyDescriptor $property) {
111
            return $property->hasDefault();
112
        });
113
114
        return $defaultProperties;
115
    }
116
117
    /**
118
     * Returns the list of properties exposed as getters and setters in this class.
119
     *
120
     * @return AbstractBeanPropertyDescriptor[]
121
     */
122
    public function getExposedProperties()
123
    {
124
        $exposedProperties = array_filter($this->beanPropertyDescriptors, function (AbstractBeanPropertyDescriptor $property) {
125
            return $property->getTable()->getName() == $this->table->getName();
126
        });
127
128
        return $exposedProperties;
129
    }
130
131
    /**
132
     * Returns the list of properties for this table (including parent tables).
133
     *
134
     * @param Table $table
135
     *
136
     * @return AbstractBeanPropertyDescriptor[]
137
     */
138
    private function getProperties(Table $table)
139
    {
140
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
141
        if ($parentRelationship) {
142
            $parentTable = $this->schema->getTable($parentRelationship->getForeignTableName());
143
            $properties = $this->getProperties($parentTable);
144
            // we merge properties by overriding property names.
145
            $localProperties = $this->getPropertiesForTable($table);
146
            foreach ($localProperties as $name => $property) {
147
                // We do not override properties if this is a primary key!
148
                if ($property->isPrimaryKey()) {
149
                    continue;
150
                }
151
                $properties[$name] = $property;
152
            }
153
        } else {
154
            $properties = $this->getPropertiesForTable($table);
155
        }
156
157
        return $properties;
158
    }
159
160
    /**
161
     * Returns the list of properties for this table (ignoring parent tables).
162
     *
163
     * @param Table $table
164
     *
165
     * @return AbstractBeanPropertyDescriptor[]
166
     */
167
    private function getPropertiesForTable(Table $table)
168
    {
169
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
170
        if ($parentRelationship) {
171
            $ignoreColumns = $parentRelationship->getLocalColumns();
172
        } else {
173
            $ignoreColumns = [];
174
        }
175
176
        $beanPropertyDescriptors = [];
177
178
        foreach ($table->getColumns() as $column) {
179
            if (array_search($column->getName(), $ignoreColumns) !== false) {
180
                continue;
181
            }
182
183
            $fk = $this->isPartOfForeignKey($table, $column);
184
            if ($fk !== null) {
185
                // Check that previously added descriptors are not added on same FK (can happen with multi key FK).
186
                foreach ($beanPropertyDescriptors as $beanDescriptor) {
187
                    if ($beanDescriptor instanceof ObjectBeanPropertyDescriptor && $beanDescriptor->getForeignKey() === $fk) {
188
                        continue 2;
189
                    }
190
                }
191
                // Check that this property is not an inheritance relationship
192
                $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
193
                if ($parentRelationship === $fk) {
194
                    continue;
195
                }
196
197
                $beanPropertyDescriptors[] = new ObjectBeanPropertyDescriptor($table, $fk, $this->schemaAnalyzer);
198
            } else {
199
                $beanPropertyDescriptors[] = new ScalarBeanPropertyDescriptor($table, $column);
200
            }
201
        }
202
203
        // Now, let's get the name of all properties and let's check there is no duplicate.
204
        /** @var $names AbstractBeanPropertyDescriptor[] */
205
        $names = [];
206
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
207
            $name = $beanDescriptor->getUpperCamelCaseName();
208
            if (isset($names[$name])) {
209
                $names[$name]->useAlternativeName();
210
                $beanDescriptor->useAlternativeName();
211
            } else {
212
                $names[$name] = $beanDescriptor;
213
            }
214
        }
215
216
        // Final check (throw exceptions if problem arises)
217
        $names = [];
218
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
219
            $name = $beanDescriptor->getUpperCamelCaseName();
220
            if (isset($names[$name])) {
221
                throw new TDBMException('Unsolvable name conflict while generating method name');
222
            } else {
223
                $names[$name] = $beanDescriptor;
224
            }
225
        }
226
227
        // Last step, let's rebuild the list with a map:
228
        $beanPropertyDescriptorsMap = [];
229
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
230
            $beanPropertyDescriptorsMap[$beanDescriptor->getLowerCamelCaseName()] = $beanDescriptor;
231
        }
232
233
        return $beanPropertyDescriptorsMap;
234
    }
235
236
    public function generateBeanConstructor()
237
    {
238
        $constructorProperties = $this->getConstructorProperties();
239
240
        $constructorCode = '    /**
241
     * The constructor takes all compulsory arguments.
242
     *
243
%s
244
     */
245
    public function __construct(%s)
246
    {
247
%s%s
248
    }
249
    ';
250
251
        $paramAnnotations = [];
252
        $arguments = [];
253
        $assigns = [];
254
        $parentConstructorArguments = [];
255
256
        foreach ($constructorProperties as $property) {
257
            $className = $property->getClassName();
258
            if ($className) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $className of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
259
                $arguments[] = $className.' '.$property->getVariableName();
260
            } else {
261
                $arguments[] = $property->getVariableName();
262
            }
263
            $paramAnnotations[] = $property->getParamAnnotation();
264
            if ($property->getTable()->getName() === $this->table->getName()) {
265
                $assigns[] = $property->getConstructorAssignCode();
266
            } else {
267
                $parentConstructorArguments[] = $property->getVariableName();
268
            }
269
        }
270
271
        $parentConstructorCode = sprintf("        parent::__construct(%s);\n", implode(', ', $parentConstructorArguments));
272
273
        foreach ($this->getPropertiesWithDefault() as $property) {
274
            $assigns[] = $property->assignToDefaultCode();
275
        }
276
277
        return sprintf($constructorCode, implode("\n", $paramAnnotations), implode(', ', $arguments), $parentConstructorCode, implode("\n", $assigns));
278
    }
279
280
    public function getDirectForeignKeysDescriptors()
281
    {
282
        $fks = $this->tdbmSchemaAnalyzer->getIncomingForeignKeys($this->table->getName());
283
284
        $descriptors = [];
285
286
        foreach ($fks as $fk) {
287
            $descriptors[] = new DirectForeignKeyMethodDescriptor($fk, $this->table);
288
        }
289
290
        return $descriptors;
291
    }
292
293
    private function getPivotTableDescriptors()
294
    {
295
        $descs = [];
296
        foreach ($this->schemaAnalyzer->detectJunctionTables(true) as $table) {
297
            // There are exactly 2 FKs since this is a pivot table.
298
            $fks = array_values($table->getForeignKeys());
299
300
            if ($fks[0]->getForeignTableName() === $this->table->getName()) {
301
                list($localFk, $remoteFk) = $fks;
302
            } elseif ($fks[1]->getForeignTableName() === $this->table->getName()) {
303
                list($remoteFk, $localFk) = $fks;
304
            } else {
305
                continue;
306
            }
307
308
            $descs[] = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk);
309
        }
310
311
        return $descs;
312
    }
313
314
    /**
315
     * Returns the list of method descriptors (and applies the alternative name if needed).
316
     *
317
     * @return MethodDescriptorInterface[]
318
     */
319
    private function getMethodDescriptors()
320
    {
321
        $directForeignKeyDescriptors = $this->getDirectForeignKeysDescriptors();
322
        $pivotTableDescriptors = $this->getPivotTableDescriptors();
323
324
        $descriptors = array_merge($directForeignKeyDescriptors, $pivotTableDescriptors);
325
326
        // Descriptors by method names
327
        $descriptorsByMethodName = [];
328
329
        foreach ($descriptors as $descriptor) {
330
            $descriptorsByMethodName[$descriptor->getName()][] = $descriptor;
331
        }
332
333
        foreach ($descriptorsByMethodName as $descriptorsForMethodName) {
334
            if (count($descriptorsForMethodName) > 1) {
335
                foreach ($descriptorsForMethodName as $descriptor) {
336
                    $descriptor->useAlternativeName();
337
                }
338
            }
339
        }
340
341
        return $descriptors;
342
    }
343
344
    public function generateJsonSerialize()
345
    {
346
        $tableName = $this->table->getName();
347
        $parentFk = $this->schemaAnalyzer->getParentRelationship($tableName);
348
        if ($parentFk !== null) {
349
            $initializer = '$array = parent::jsonSerialize($stopRecursion);';
350
        } else {
351
            $initializer = '$array = [];';
352
        }
353
354
        $str = '
355
    /**
356
     * Serializes the object for JSON encoding.
357
     *
358
     * @param bool $stopRecursion Parameter used internally by TDBM to stop embedded objects from embedding other objects.
359
     * @return array
360
     */
361
    public function jsonSerialize($stopRecursion = false)
362
    {
363
        %s
364
%s
365
%s
366
        return $array;
367
    }
368
';
369
370
        $propertiesCode = '';
371
        foreach ($this->beanPropertyDescriptors as $beanPropertyDescriptor) {
372
            $propertiesCode .= $beanPropertyDescriptor->getJsonSerializeCode();
373
        }
374
375
        // Many2many relationships
376
        $methodsCode = '';
377
        foreach ($this->getMethodDescriptors() as $methodDescriptor) {
378
            $methodsCode .= $methodDescriptor->getJsonSerializeCode();
379
        }
380
381
        return sprintf($str, $initializer, $propertiesCode, $methodsCode);
382
    }
383
384
    /**
385
     * Returns as an array the class we need to extend from and the list of use statements.
386
     *
387
     * @return array
388
     */
389
    private function generateExtendsAndUseStatements(ForeignKeyConstraint $parentFk = null)
390
    {
391
        $classes = [];
392
        if ($parentFk !== null) {
393
            $extends = TDBMDaoGenerator::getBeanNameFromTableName($parentFk->getForeignTableName());
394
            $classes[] = $extends;
395
        }
396
397
        foreach ($this->getBeanPropertyDescriptors() as $beanPropertyDescriptor) {
398
            $className = $beanPropertyDescriptor->getClassName();
399
            if (null !== $className) {
400
                $classes[] = $beanPropertyDescriptor->getClassName();
401
            }
402
        }
403
404
        foreach ($this->getMethodDescriptors() as $descriptor) {
405
            $classes = array_merge($classes, $descriptor->getUsedClasses());
406
        }
407
408
        $classes = array_unique($classes);
409
410
        return $classes;
411
    }
412
413
    /**
414
     * Writes the PHP bean file with all getters and setters from the table passed in parameter.
415
     *
416
     * @param string $beannamespace The namespace of the bean
417
     */
418
    public function generatePhpCode($beannamespace)
419
    {
420
        $tableName = $this->table->getName();
421
        $baseClassName = TDBMDaoGenerator::getBaseBeanNameFromTableName($tableName);
422
        $className = TDBMDaoGenerator::getBeanNameFromTableName($tableName);
423
        $parentFk = $this->schemaAnalyzer->getParentRelationship($tableName);
424
425
        $classes = $this->generateExtendsAndUseStatements($parentFk);
426
427
        $uses = array_map(function ($className) use ($beannamespace) {
428
            return 'use '.$beannamespace.'\\'.$className.";\n";
429
        }, $classes);
430
        $use = implode('', $uses);
431
432
        if ($parentFk !== null) {
433
            $extends = TDBMDaoGenerator::getBeanNameFromTableName($parentFk->getForeignTableName());
434
        } else {
435
            $extends = 'AbstractTDBMObject';
436
            $use .= "use Mouf\\Database\\TDBM\\AbstractTDBMObject;\n";
437
        }
438
439
        $str = "<?php
440
namespace {$beannamespace}\\Generated;
441
442
use Mouf\\Database\\TDBM\\ResultIterator;
443
use Mouf\\Database\\TDBM\\ResultArray;
444
use Mouf\\Database\\TDBM\\AlterableResultIterator;
445
$use
446
/*
447
 * This file has been automatically generated by TDBM.
448
 * DO NOT edit this file, as it might be overwritten.
449
 * If you need to perform changes, edit the $className class instead!
450
 */
451
452
/**
453
 * The $baseClassName class maps the '$tableName' table in database.
454
 */
455
class $baseClassName extends $extends implements \\JsonSerializable
456
{
457
";
458
459
        $str .= $this->generateBeanConstructor();
460
461
        foreach ($this->getExposedProperties() as $property) {
462
            $str .= $property->getGetterSetterCode();
463
        }
464
465
        foreach ($this->getMethodDescriptors() as $methodDescriptor) {
466
            $str .= $methodDescriptor->getCode();
467
        }
468
        $str .= $this->generateJsonSerialize();
469
470
        $str .= $this->generateGetUsedTablesCode();
471
472
        $str .= $this->generateOnDeleteCode();
473
474
        $str .= '}
475
';
476
477
        return $str;
478
    }
479
480
    /**
481
     * @param string $beanNamespace
482
     * @param string $beanClassName
483
     *
484
     * @return array first element: list of used beans, second item: PHP code as a string
485
     */
486
    public function generateFindByDaoCode($beanNamespace, $beanClassName)
487
    {
488
        $code = '';
489
        $usedBeans = [];
490
        foreach ($this->table->getIndexes() as $index) {
491
            if (!$index->isPrimary()) {
492
                list($usedBeansForIndex, $codeForIndex) = $this->generateFindByDaoCodeForIndex($index, $beanNamespace, $beanClassName);
493
                $code .= $codeForIndex;
494
                $usedBeans = array_merge($usedBeans, $usedBeansForIndex);
495
            }
496
        }
497
498
        return [$usedBeans, $code];
499
    }
500
501
    /**
502
     * @param Index  $index
503
     * @param string $beanNamespace
504
     * @param string $beanClassName
505
     *
506
     * @return array first element: list of used beans, second item: PHP code as a string
507
     */
508
    private function generateFindByDaoCodeForIndex(Index $index, $beanNamespace, $beanClassName)
509
    {
510
        $columns = $index->getColumns();
511
        $usedBeans = [];
512
513
        /*
514
         * The list of elements building this index (expressed as columns or foreign keys)
515
         * @var AbstractBeanPropertyDescriptor[]
516
         */
517
        $elements = [];
518
519
        foreach ($columns as $column) {
520
            $fk = $this->isPartOfForeignKey($this->table, $this->table->getColumn($column));
521
            if ($fk !== null) {
522
                if (!in_array($fk, $elements)) {
523
                    $elements[] = new ObjectBeanPropertyDescriptor($this->table, $fk, $this->schemaAnalyzer);
524
                }
525
            } else {
526
                $elements[] = new ScalarBeanPropertyDescriptor($this->table, $this->table->getColumn($column));
527
            }
528
        }
529
530
        // If the index is actually only a foreign key, let's bypass it entirely.
531
        if (count($elements) === 1 && $elements[0] instanceof ObjectBeanPropertyDescriptor) {
532
            return [[], ''];
533
        }
534
535
        $methodNameComponent = [];
536
        $functionParameters = [];
537
        $first = true;
538
        foreach ($elements as $element) {
539
            $methodNameComponent[] = $element->getUpperCamelCaseName();
540
            $functionParameter = $element->getClassName();
541
            if ($functionParameter) {
542
                $usedBeans[] = $beanNamespace.'\\'.$functionParameter;
543
                $functionParameter .= ' ';
544
            }
545
            $functionParameter .= $element->getVariableName();
546
            if ($first) {
547
                $first = false;
548
            } else {
549
                $functionParameter .= ' = null';
550
            }
551
            $functionParameters[] = $functionParameter;
552
        }
553
        if ($index->isUnique()) {
554
            $methodName = 'findOneBy'.implode('And', $methodNameComponent);
555
            $calledMethod = 'findOne';
556
            $returnType = "{$beanClassName}";
557
        } else {
558
            $methodName = 'findBy'.implode('And', $methodNameComponent);
559
            $returnType = "{$beanClassName}[]|ResultIterator|ResultArray";
560
            $calledMethod = 'find';
561
        }
562
        $functionParametersString = implode(', ', $functionParameters);
563
564
        $count = 0;
565
566
        $params = [];
567
        $filterArrayCode = '';
568
        $commentArguments = [];
569
        foreach ($elements as $element) {
570
            $params[] = $element->getParamAnnotation();
571
            if ($element instanceof ScalarBeanPropertyDescriptor) {
572
                $filterArrayCode .= '            '.var_export($element->getColumnName(), true).' => '.$element->getVariableName().",\n";
573
            } else {
574
                ++$count;
575
                $filterArrayCode .= '            '.$count.' => '.$element->getVariableName().",\n";
576
            }
577
            $commentArguments[] = substr($element->getVariableName(), 1);
578
        }
579
        $paramsString = implode("\n", $params);
580
581
        $code = "
582
    /**
583
     * Get a list of $beanClassName filtered by ".implode(', ', $commentArguments).".
584
     *
585
$paramsString
586
     * @param mixed \$orderBy The order string
587
     * @param array \$additionalTablesFetch A list of additional tables to fetch (for performance improvement)
588
     * @param string \$mode Either TDBMService::MODE_ARRAY or TDBMService::MODE_CURSOR (for large datasets). Defaults to TDBMService::MODE_ARRAY.
589
     * @return $returnType
590
     */
591
    public function $methodName($functionParametersString, \$orderBy = null, array \$additionalTablesFetch = array(), \$mode = null)
592
    {
593
        \$filter = [
594
".$filterArrayCode."        ];
595
        return \$this->$calledMethod(\$filter, [], \$orderBy, \$additionalTablesFetch, \$mode);
596
    }
597
";
598
599
        return [$usedBeans, $code];
600
    }
601
602
    /**
603
     * Generates the code for the getUsedTable protected method.
604
     *
605
     * @return string
606
     */
607
    private function generateGetUsedTablesCode()
608
    {
609
        $hasParentRelationship = $this->schemaAnalyzer->getParentRelationship($this->table->getName()) !== null;
610
        if ($hasParentRelationship) {
611
            $code = sprintf('        $tables = parent::getUsedTables();
612
        $tables[] = %s;
613
        
614
        return $tables;', var_export($this->table->getName(), true));
615
        } else {
616
            $code = sprintf('        return [ %s ];', var_export($this->table->getName(), true));
617
        }
618
619
        return sprintf('
620
    /**
621
     * Returns an array of used tables by this bean (from parent to child relationship).
622
     *
623
     * @return string[]
624
     */
625
    protected function getUsedTables()
626
    {
627
%s    
628
    }
629
', $code);
630
    }
631
632
    private function generateOnDeleteCode()
633
    {
634
        $code = '';
635
        $relationships = $this->getPropertiesForTable($this->table);
636
        foreach ($relationships as $relationship) {
637
            if ($relationship instanceof ObjectBeanPropertyDescriptor) {
638
                $code .= sprintf('        $this->setRef('.var_export($relationship->getForeignKey()->getName(), true).', null, '.var_export($this->table->getName(), true).");\n");
639
            }
640
        }
641
642
        if ($code) {
643
            return sprintf('
644
    /**
645
     * Method called when the bean is removed from database.
646
     *
647
     */
648
    protected function onDelete()
649
    {
650
        parent::onDelete();
651
%s
652
    }
653
', $code);
654
        }
655
656
        return '';
657
    }
658
}
659