Completed
Pull Request — 3.4 (#46)
by David
20:58
created

BeanDescriptor::getIncomingForeignKeys()   C

Complexity

Conditions 8
Paths 7

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 25
rs 5.3846
cc 8
eloc 15
nc 7
nop 0
1
<?php
2
3
4
namespace Mouf\Database\TDBM\Utils;
5
6
use Doctrine\DBAL\Schema\Column;
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
13
/**
14
 * This class represents a bean
15
 */
16
class BeanDescriptor
17
{
18
    /**
19
     * @var Table
20
     */
21
    private $table;
22
23
    /**
24
     * @var SchemaAnalyzer
25
     */
26
    private $schemaAnalyzer;
27
28
    /**
29
     * @var Schema
30
     */
31
    private $schema;
32
33
    /**
34
     * @var AbstractBeanPropertyDescriptor[]
35
     */
36
    private $beanPropertyDescriptors = [];
37
38
    public function __construct(Table $table, SchemaAnalyzer $schemaAnalyzer, Schema $schema) {
39
        $this->table = $table;
40
        $this->schemaAnalyzer = $schemaAnalyzer;
41
        $this->schema = $schema;
42
        $this->initBeanPropertyDescriptors();
43
    }
44
45
    private function initBeanPropertyDescriptors() {
46
        $this->beanPropertyDescriptors = $this->getProperties($this->table);
47
    }
48
49
    /**
50
     * Returns the foreignkey the column is part of, if any. null otherwise.
51
     *
52
     * @param Table $table
53
     * @param Column $column
54
     * @return ForeignKeyConstraint|null
55
     */
56
    private function isPartOfForeignKey(Table $table, Column $column) {
57
        $localColumnName = $column->getName();
58
        foreach ($table->getForeignKeys() as $foreignKey) {
59
            foreach ($foreignKey->getColumns() as $columnName) {
60
                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...
61
                    return $foreignKey;
62
                }
63
            }
64
        }
65
        return null;
66
    }
67
68
    /**
69
     * @return AbstractBeanPropertyDescriptor[]
70
     */
71
    public function getBeanPropertyDescriptors()
72
    {
73
        return $this->beanPropertyDescriptors;
74
    }
75
76
    /**
77
     * Returns the list of columns that are not nullable and not autogenerated for a given table and its parent.
78
     *
79
     * @return AbstractBeanPropertyDescriptor[]
80
     */
81
    public function getConstructorProperties() {
82
83
        $constructorProperties = array_filter($this->beanPropertyDescriptors, function(AbstractBeanPropertyDescriptor $property) {
84
           return $property->isCompulsory();
85
        });
86
87
        return $constructorProperties;
88
    }
89
90
    /**
91
     * Returns the list of properties exposed as getters and setters in this class.
92
     *
93
     * @return AbstractBeanPropertyDescriptor[]
94
     */
95
    public function getExposedProperties() {
96
        $exposedProperties = array_filter($this->beanPropertyDescriptors, function(AbstractBeanPropertyDescriptor $property) {
97
            return $property->getTable()->getName() == $this->table->getName();
98
        });
99
100
        return $exposedProperties;
101
    }
102
103
    /**
104
     * Returns the list of foreign keys pointing to the table represented by this bean, excluding foreign keys
105
     * from junction tables and from inheritance.
106
     *
107
     * @return ForeignKeyConstraint[]
108
     */
109
    public function getIncomingForeignKeys() {
110
111
        $junctionTables = $this->schemaAnalyzer->detectJunctionTables();
112
        $junctionTableNames = array_map(function(Table $table) { return $table->getName(); }, $junctionTables);
113
        $childrenRelationships = $this->schemaAnalyzer->getChildrenRelationships($this->table->getName());
114
115
        $fks = [];
116
        foreach ($this->schema->getTables() as $table) {
117
            foreach ($table->getForeignKeys() as $fk) {
118
                if ($fk->getForeignTableName() === $this->table->getName()) {
119
                    if (in_array($fk->getLocalTableName(), $junctionTableNames)) {
120
                        continue;
121
                    }
122
                    foreach ($childrenRelationships as $childFk) {
123
                        if ($fk->getLocalTableName() === $childFk->getLocalTableName() && $fk->getLocalColumns() === $childFk->getLocalColumns()) {
124
                            continue 2;
125
                        }
126
                    }
127
                    $fks[] = $fk;
128
                }
129
            }
130
        }
131
132
        return $fks;
133
    }
134
135
    /**
136
     * Returns the list of properties for this table (including parent tables).
137
     *
138
     * @param Table $table
139
     * @return AbstractBeanPropertyDescriptor[]
140
     */
141
    private function getProperties(Table $table)
142
    {
143
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
144
        if ($parentRelationship) {
145
            $parentTable = $this->schema->getTable($parentRelationship->getForeignTableName());
146
            $properties = $this->getProperties($parentTable);
147
            // we merge properties by overriding property names.
148
            $localProperties = $this->getPropertiesForTable($table);
149
            foreach ($localProperties as $name => $property) {
150
                // We do not override properties if this is a primary key!
151
                if ($property->isPrimaryKey()) {
152
                    continue;
153
                }
154
                $properties[$name] = $property;
155
            }
156
            //$properties = array_merge($properties, $localProperties);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
157
158
        } else {
159
            $properties = $this->getPropertiesForTable($table);
160
        }
161
162
        return $properties;
163
    }
164
165
        /**
166
     * Returns the list of properties for this table (ignoring parent tables).
167
     *
168
     * @param Table $table
169
     * @return AbstractBeanPropertyDescriptor[]
170
     */
171
    private function getPropertiesForTable(Table $table)
172
    {
173
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
174
        if ($parentRelationship) {
175
            $ignoreColumns = $parentRelationship->getLocalColumns();
176
        } else {
177
            $ignoreColumns = [];
178
        }
179
180
        $beanPropertyDescriptors = [];
181
182
        foreach ($table->getColumns() as $column) {
183
            if (array_search($column->getName(), $ignoreColumns) !== false) {
184
                continue;
185
            }
186
187
            $fk = $this->isPartOfForeignKey($table, $column);
188
            if ($fk !== null) {
189
                // Check that previously added descriptors are not added on same FK (can happen with multi key FK).
190
                foreach ($beanPropertyDescriptors as $beanDescriptor) {
191
                    if ($beanDescriptor instanceof ObjectBeanPropertyDescriptor && $beanDescriptor->getForeignKey() === $fk) {
192
                        continue 2;
193
                    }
194
                }
195
                // Check that this property is not an inheritance relationship
196
                $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
197
                if ($parentRelationship === $fk) {
198
                    continue;
199
                }
200
201
                $beanPropertyDescriptors[] = new ObjectBeanPropertyDescriptor($table, $fk, $this->schemaAnalyzer);
202
            } else {
203
                $beanPropertyDescriptors[] = new ScalarBeanPropertyDescriptor($table, $column);
204
            }
205
        }
206
207
        // Now, let's get the name of all properties and let's check there is no duplicate.
208
        /** @var $names AbstractBeanPropertyDescriptor[] */
209
        $names = [];
210
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
211
            $name = $beanDescriptor->getUpperCamelCaseName();
212
            if (isset($names[$name])) {
213
                $names[$name]->useAlternativeName();
214
                $beanDescriptor->useAlternativeName();
215
            } else {
216
                $names[$name] = $beanDescriptor;
217
            }
218
        }
219
220
        // Final check (throw exceptions if problem arises)
221
        $names = [];
222
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
223
            $name = $beanDescriptor->getUpperCamelCaseName();
224
            if (isset($names[$name])) {
225
                throw new TDBMException("Unsolvable name conflict while generating method name");
226
            } else {
227
                $names[$name] = $beanDescriptor;
228
            }
229
        }
230
231
        // Last step, let's rebuild the list with a map:
232
        $beanPropertyDescriptorsMap = [];
233
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
234
            $beanPropertyDescriptorsMap[$beanDescriptor->getLowerCamelCaseName()] = $beanDescriptor;
235
        }
236
237
        return $beanPropertyDescriptorsMap;
238
    }
239
240
    public function generateBeanConstructor() {
241
        $constructorProperties = $this->getConstructorProperties();
242
243
        $constructorCode = "    /**
244
     * The constructor takes all compulsory arguments.
245
     *
246
%s
247
     */
248
    public function __construct(%s) {
249
%s%s
250
    }
251
    ";
252
253
        $paramAnnotations = [];
254
        $arguments = [];
255
        $assigns = [];
256
        $parentConstructorArguments = [];
257
258
        foreach ($constructorProperties as $property) {
259
            $className = $property->getClassName();
260
            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...
261
                $arguments[] = $className.' '.$property->getVariableName();
262
            } else {
263
                $arguments[] = $property->getVariableName();
264
            }
265
            $paramAnnotations[] = $property->getParamAnnotation();
266
            if ($property->getTable()->getName() === $this->table->getName()) {
267
                $assigns[] = $property->getConstructorAssignCode();
268
            } else {
269
                $parentConstructorArguments[] = $property->getVariableName();
270
            }
271
        }
272
273
        $parentConstrutorCode = sprintf("        parent::__construct(%s);\n", implode(', ', $parentConstructorArguments));
274
275
        return sprintf($constructorCode, implode("\n", $paramAnnotations), implode(", ", $arguments), $parentConstrutorCode, implode("\n", $assigns));
276
    }
277
278
    public function generateDirectForeignKeysCode() {
279
        $fks = $this->getIncomingForeignKeys();
280
281
        $fksByTable = [];
282
283
        foreach ($fks as $fk) {
284
            $fksByTable[$fk->getLocalTableName()][] = $fk;
285
        }
286
287
        /* @var $fksByMethodName ForeignKeyConstraint[] */
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
288
        $fksByMethodName = [];
289
290
        foreach ($fksByTable as $tableName => $fksForTable) {
291
            if (count($fksForTable) > 1) {
292
                foreach ($fksForTable as $fk) {
293
                    $methodName = 'get'.TDBMDaoGenerator::toCamelCase($fk->getLocalTableName()).'By';
294
295
                    $camelizedColumns = array_map(['Mouf\\Database\\TDBM\\Utils\\TDBMDaoGenerator', 'toCamelCase'], $fk->getLocalColumns());
296
297
                    $methodName .= implode('And', $camelizedColumns);
298
299
                    $fksByMethodName[$methodName] = $fk;
300
                }
301
            } else {
302
                $methodName = 'get'.TDBMDaoGenerator::toCamelCase($fksForTable[0]->getLocalTableName());
303
                $fksByMethodName[$methodName] = $fk;
0 ignored issues
show
Bug introduced by
The variable $fk does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
304
            }
305
        }
306
307
        $code = '';
308
309
        foreach ($fksByMethodName as $methodName => $fk) {
310
            $getterCode = '    /**
311
     * Returns the list of %s pointing to this bean via the %s column.
312
     *
313
     * @return %s[]|Resultiterator
314
     */
315
    public function %s()
316
    {
317
        return $this->tdbmService->findObjects(%s, %s, %s);
318
    }
319
320
';
321
322
            list($sql, $parametersCode) = $this->getFilters($fk);
323
324
            $beanClass = TDBMDaoGenerator::getBeanNameFromTableName($fk->getLocalTableName());
325
            $code .= sprintf($getterCode,
326
                $beanClass,
327
                implode(', ', $fk->getColumns()),
328
                $beanClass,
329
                $methodName,
330
                var_export($fk->getLocalTableName(), true),
331
                $sql,
332
                $parametersCode
333
            );
334
        }
335
336
        return $code;
337
    }
338
339
    private function getFilters(ForeignKeyConstraint $fk) {
340
        $sqlParts = [];
341
        $counter = 0;
342
        $parameters = [];
343
344
        $pkColumns = $this->table->getPrimaryKeyColumns();
345
346
        foreach ($fk->getLocalColumns() as $columnName) {
347
            $paramName = "tdbmparam".$counter;
348
            $sqlParts[] = $fk->getLocalTableName().'.'.$columnName." = :".$paramName;
349
350
            $pkColumn = $pkColumns[$counter];
351
            $parameters[] = sprintf('%s => $this->get(%s, %s)', var_export($paramName, true), var_export($pkColumn, true), var_export($this->table->getName(), true));
352
            $counter++;
353
        }
354
        $sql = "'".implode(' AND ', $sqlParts)."'";
355
        $parametersCode = '[ '.implode(', ', $parameters).' ]';
356
357
        return [$sql, $parametersCode];
358
    }
359
360
    /**
361
     * Generate code section about pivot tables
362
     *
363
     * @return string;
0 ignored issues
show
Documentation introduced by
The doc-type string; could not be parsed: Expected "|" or "end of type", but got ";" at position 6. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
364
     */
365
    public function generatePivotTableCode() {
366
        $descs = [];
367
        foreach ($this->schemaAnalyzer->detectJunctionTables() as $table) {
368
            // There are exactly 2 FKs since this is a pivot table.
369
            $fks = array_values($table->getForeignKeys());
370
371
            if ($fks[0]->getForeignTableName() === $this->table->getName()) {
372
                $localFK = $fks[0];
373
                $remoteFK = $fks[1];
374
            } elseif ($fks[1]->getForeignTableName() === $this->table->getName()) {
375
                $localFK = $fks[1];
376
                $remoteFK = $fks[0];
377
            } else {
378
                continue;
379
            }
380
381
            $descs[$remoteFK->getForeignTableName()][] = [
382
                'table' => $table,
383
                'localFK' => $localFK,
384
                'remoteFK' => $remoteFK
385
            ];
386
387
        }
388
389
        $finalDescs = [];
390
        foreach ($descs as $descArray) {
391
            if (count($descArray) > 1) {
392
                foreach ($descArray as $desc) {
393
                    $desc['name'] = TDBMDaoGenerator::toCamelCase($desc['remoteFK']->getForeignTableName())."By".TDBMDaoGenerator::toCamelCase($desc['table']->getName());
394
                    $finalDescs[] = $desc;
395
                }
396
            } else {
397
                $desc = $descArray[0];
398
                $desc['name'] = TDBMDaoGenerator::toCamelCase($desc['remoteFK']->getForeignTableName());
399
                $finalDescs[] = $desc;
400
            }
401
        }
402
403
404
        $code = '';
405
406
        foreach ($finalDescs as $desc) {
407
            $code .= $this->getPivotTableCode($desc['name'], $desc['table'], $desc['localFK'], $desc['remoteFK']);
408
        }
409
410
        return $code;
411
    }
412
413
    public function getPivotTableCode($name, Table $table, ForeignKeyConstraint $localFK, ForeignKeyConstraint $remoteFK) {
0 ignored issues
show
Unused Code introduced by
The parameter $localFK is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
414
        $singularName = TDBMDaoGenerator::toSingular($name);
415
        $remoteBeanName = TDBMDaoGenerator::getBeanNameFromTableName($remoteFK->getForeignTableName());
416
        $variableName = '$'.TDBMDaoGenerator::toVariableName($remoteBeanName);
417
418
        $str = '    /**
419
     * Returns the list of %s associated to this bean via the %s pivot table.
420
     *
421
     * @return %s[]
422
     */
423
    public function get%s() {
424
        return $this->_getRelationships(%s);
425
    }
426
';
427
428
        $getterCode = sprintf($str, $remoteBeanName, $table->getName(), $remoteBeanName, $name, var_export($remoteFK->getLocalTableName(), true));
429
430
        $str = '    /**
431
     * Adds a relationship with %s associated to this bean via the %s pivot table.
432
     *
433
     * @param %s %s
434
     */
435
    public function add%s(%s %s) {
436
        return $this->addRelationship(%s, %s);
437
    }
438
';
439
440
        $adderCode = sprintf($str, $remoteBeanName, $table->getName(), $remoteBeanName, $variableName, $singularName, $remoteBeanName, $variableName, var_export($remoteFK->getLocalTableName(), true), $variableName);
441
442
        $str = '    /**
443
     * Deletes the relationship with %s associated to this bean via the %s pivot table.
444
     *
445
     * @param %s %s
446
     */
447
    public function remove%s(%s %s) {
448
        return $this->_removeRelationship(%s, %s);
449
    }
450
';
451
452
        $removerCode = sprintf($str, $remoteBeanName, $table->getName(), $remoteBeanName, $variableName, $singularName, $remoteBeanName, $variableName, var_export($remoteFK->getLocalTableName(), true), $variableName);
453
454
        $str = '    /**
455
     * Returns whether this bean is associated with %s via the %s pivot table.
456
     *
457
     * @param %s %s
458
     * @return bool
459
     */
460
    public function has%s(%s %s) {
461
        return $this->hasRelationship(%s, %s);
462
    }
463
';
464
465
        $hasCode = sprintf($str, $remoteBeanName, $table->getName(), $remoteBeanName, $variableName, $singularName, $remoteBeanName, $variableName, var_export($remoteFK->getLocalTableName(), true), $variableName);
466
467
468
        $code = $getterCode.$adderCode.$removerCode.$hasCode;
469
470
        return $code;
471
    }
472
473
    /**
474
     * Writes the PHP bean file with all getters and setters from the table passed in parameter.
475
     *
476
     * @param string $beannamespace The namespace of the bean
477
     */
478
    public function generatePhpCode($beannamespace) {
479
        $baseClassName = TDBMDaoGenerator::getBaseBeanNameFromTableName($this->table->getName());
480
        $className = TDBMDaoGenerator::getBeanNameFromTableName($this->table->getName());
481
        $tableName = $this->table->getName();
482
483
        $parentFk = $this->schemaAnalyzer->getParentRelationship($tableName);
484
        if ($parentFk !== null) {
485
            $extends = TDBMDaoGenerator::getBeanNameFromTableName($parentFk->getForeignTableName());
486
            $use = "";
487
        } else {
488
            $extends = "AbstractTDBMObject";
489
            $use = "use Mouf\\Database\\TDBM\\AbstractTDBMObject;\n\n";
490
        }
491
492
        $str = "<?php
493
namespace {$beannamespace};
494
495
use Mouf\\Database\\TDBM\\ResultIterator;
496
$use
497
/*
498
 * This file has been automatically generated by TDBM.
499
 * DO NOT edit this file, as it might be overwritten.
500
 * If you need to perform changes, edit the $className class instead!
501
 */
502
503
/**
504
 * The $baseClassName class maps the '$tableName' table in database.
505
 */
506
class $baseClassName extends $extends
507
{
508
";
509
510
        $str .= $this->generateBeanConstructor();
511
512
513
514
        foreach ($this->getExposedProperties() as $property) {
515
            $str .= $property->getGetterSetterCode();
516
        }
517
518
        $str .= $this->generateDirectForeignKeysCode();
519
        $str .= $this->generatePivotTableCode();
520
521
        $str .= "}
522
";
523
        return $str;
524
    }
525
}
526