Completed
Push — 4.0 ( b1b9bd...e1def4 )
by David
21:09
created

BeanDescriptor::getProperties()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 21
rs 9.0534
cc 4
eloc 13
nc 2
nop 1
1
<?php
2
3
namespace Mouf\Database\TDBM\Utils;
4
5
use Doctrine\DBAL\Schema\Column;
6
use Doctrine\DBAL\Schema\Schema;
7
use Doctrine\DBAL\Schema\Table;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Mouf\Database\SchemaAnalyzer\SchemaAnalyzer;
10
use Mouf\Database\TDBM\TDBMException;
11
use Mouf\Database\TDBM\TDBMSchemaAnalyzer;
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
    /**
39
     * @var TDBMSchemaAnalyzer
40
     */
41
    private $tdbmSchemaAnalyzer;
42
43 View Code Duplication
    public function __construct(Table $table, SchemaAnalyzer $schemaAnalyzer, Schema $schema, TDBMSchemaAnalyzer $tdbmSchemaAnalyzer)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
44
    {
45
        $this->table = $table;
46
        $this->schemaAnalyzer = $schemaAnalyzer;
47
        $this->schema = $schema;
48
        $this->tdbmSchemaAnalyzer = $tdbmSchemaAnalyzer;
49
        $this->initBeanPropertyDescriptors();
50
    }
51
52
    private function initBeanPropertyDescriptors()
53
    {
54
        $this->beanPropertyDescriptors = $this->getProperties($this->table);
55
    }
56
57
    /**
58
     * Returns the foreignkey the column is part of, if any. null otherwise.
59
     *
60
     * @param Table  $table
61
     * @param Column $column
62
     *
63
     * @return ForeignKeyConstraint|null
64
     */
65
    private function isPartOfForeignKey(Table $table, Column $column)
66
    {
67
        $localColumnName = $column->getName();
68
        foreach ($table->getForeignKeys() as $foreignKey) {
69
            foreach ($foreignKey->getColumns() as $columnName) {
70
                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...
71
                    return $foreignKey;
72
                }
73
            }
74
        }
75
76
        return;
77
    }
78
79
    /**
80
     * @return AbstractBeanPropertyDescriptor[]
81
     */
82
    public function getBeanPropertyDescriptors()
83
    {
84
        return $this->beanPropertyDescriptors;
85
    }
86
87
    /**
88
     * Returns the list of columns that are not nullable and not autogenerated for a given table and its parent.
89
     *
90
     * @return AbstractBeanPropertyDescriptor[]
91
     */
92
    public function getConstructorProperties()
93
    {
94
        $constructorProperties = array_filter($this->beanPropertyDescriptors, function (AbstractBeanPropertyDescriptor $property) {
95
           return $property->isCompulsory();
96
        });
97
98
        return $constructorProperties;
99
    }
100
101
    /**
102
     * Returns the list of properties exposed as getters and setters in this class.
103
     *
104
     * @return AbstractBeanPropertyDescriptor[]
105
     */
106
    public function getExposedProperties()
107
    {
108
        $exposedProperties = array_filter($this->beanPropertyDescriptors, function (AbstractBeanPropertyDescriptor $property) {
109
            return $property->getTable()->getName() == $this->table->getName();
110
        });
111
112
        return $exposedProperties;
113
    }
114
115
    /**
116
     * Returns the list of properties for this table (including parent tables).
117
     *
118
     * @param Table $table
119
     *
120
     * @return AbstractBeanPropertyDescriptor[]
121
     */
122
    private function getProperties(Table $table)
123
    {
124
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
125
        if ($parentRelationship) {
126
            $parentTable = $this->schema->getTable($parentRelationship->getForeignTableName());
127
            $properties = $this->getProperties($parentTable);
128
            // we merge properties by overriding property names.
129
            $localProperties = $this->getPropertiesForTable($table);
130
            foreach ($localProperties as $name => $property) {
131
                // We do not override properties if this is a primary key!
132
                if ($property->isPrimaryKey()) {
133
                    continue;
134
                }
135
                $properties[$name] = $property;
136
            }
137
        } else {
138
            $properties = $this->getPropertiesForTable($table);
139
        }
140
141
        return $properties;
142
    }
143
144
    /**
145
     * Returns the list of properties for this table (ignoring parent tables).
146
     *
147
     * @param Table $table
148
     *
149
     * @return AbstractBeanPropertyDescriptor[]
150
     */
151
    private function getPropertiesForTable(Table $table)
152
    {
153
        $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
154
        if ($parentRelationship) {
155
            $ignoreColumns = $parentRelationship->getLocalColumns();
156
        } else {
157
            $ignoreColumns = [];
158
        }
159
160
        $beanPropertyDescriptors = [];
161
162
        foreach ($table->getColumns() as $column) {
163
            if (array_search($column->getName(), $ignoreColumns) !== false) {
164
                continue;
165
            }
166
167
            $fk = $this->isPartOfForeignKey($table, $column);
168
            if ($fk !== null) {
169
                // Check that previously added descriptors are not added on same FK (can happen with multi key FK).
170
                foreach ($beanPropertyDescriptors as $beanDescriptor) {
171
                    if ($beanDescriptor instanceof ObjectBeanPropertyDescriptor && $beanDescriptor->getForeignKey() === $fk) {
172
                        continue 2;
173
                    }
174
                }
175
                // Check that this property is not an inheritance relationship
176
                $parentRelationship = $this->schemaAnalyzer->getParentRelationship($table->getName());
177
                if ($parentRelationship === $fk) {
178
                    continue;
179
                }
180
181
                $beanPropertyDescriptors[] = new ObjectBeanPropertyDescriptor($table, $fk, $this->schemaAnalyzer);
182
            } else {
183
                $beanPropertyDescriptors[] = new ScalarBeanPropertyDescriptor($table, $column);
184
            }
185
        }
186
187
        // Now, let's get the name of all properties and let's check there is no duplicate.
188
        /** @var $names AbstractBeanPropertyDescriptor[] */
189
        $names = [];
190
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
191
            $name = $beanDescriptor->getUpperCamelCaseName();
192
            if (isset($names[$name])) {
193
                $names[$name]->useAlternativeName();
194
                $beanDescriptor->useAlternativeName();
195
            } else {
196
                $names[$name] = $beanDescriptor;
197
            }
198
        }
199
200
        // Final check (throw exceptions if problem arises)
201
        $names = [];
202
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
203
            $name = $beanDescriptor->getUpperCamelCaseName();
204
            if (isset($names[$name])) {
205
                throw new TDBMException('Unsolvable name conflict while generating method name');
206
            } else {
207
                $names[$name] = $beanDescriptor;
208
            }
209
        }
210
211
        // Last step, let's rebuild the list with a map:
212
        $beanPropertyDescriptorsMap = [];
213
        foreach ($beanPropertyDescriptors as $beanDescriptor) {
214
            $beanPropertyDescriptorsMap[$beanDescriptor->getLowerCamelCaseName()] = $beanDescriptor;
215
        }
216
217
        return $beanPropertyDescriptorsMap;
218
    }
219
220
    public function generateBeanConstructor()
221
    {
222
        $constructorProperties = $this->getConstructorProperties();
223
224
        $constructorCode = '    /**
225
     * The constructor takes all compulsory arguments.
226
     *
227
%s
228
     */
229
    public function __construct(%s) {
230
%s%s
231
    }
232
    ';
233
234
        $paramAnnotations = [];
235
        $arguments = [];
236
        $assigns = [];
237
        $parentConstructorArguments = [];
238
239
        foreach ($constructorProperties as $property) {
240
            $className = $property->getClassName();
241
            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...
242
                $arguments[] = $className.' '.$property->getVariableName();
243
            } else {
244
                $arguments[] = $property->getVariableName();
245
            }
246
            $paramAnnotations[] = $property->getParamAnnotation();
247
            if ($property->getTable()->getName() === $this->table->getName()) {
248
                $assigns[] = $property->getConstructorAssignCode();
249
            } else {
250
                $parentConstructorArguments[] = $property->getVariableName();
251
            }
252
        }
253
254
        $parentConstrutorCode = sprintf("        parent::__construct(%s);\n", implode(', ', $parentConstructorArguments));
255
256
        return sprintf($constructorCode, implode("\n", $paramAnnotations), implode(', ', $arguments), $parentConstrutorCode, implode("\n", $assigns));
257
    }
258
259
    public function generateDirectForeignKeysCode()
260
    {
261
        $fks = $this->tdbmSchemaAnalyzer->getIncomingForeignKeys($this->table->getName());
262
263
        $fksByTable = [];
264
265
        foreach ($fks as $fk) {
266
            $fksByTable[$fk->getLocalTableName()][] = $fk;
267
        }
268
269
        /* @var $fksByMethodName ForeignKeyConstraint[] */
270
        $fksByMethodName = [];
271
272
        foreach ($fksByTable as $tableName => $fksForTable) {
273
            if (count($fksForTable) > 1) {
274
                foreach ($fksForTable as $fk) {
275
                    $methodName = 'get'.TDBMDaoGenerator::toCamelCase($fk->getLocalTableName()).'By';
276
277
                    $camelizedColumns = array_map(['Mouf\\Database\\TDBM\\Utils\\TDBMDaoGenerator', 'toCamelCase'], $fk->getLocalColumns());
278
279
                    $methodName .= implode('And', $camelizedColumns);
280
281
                    $fksByMethodName[$methodName] = $fk;
282
                }
283
            } else {
284
                $methodName = 'get'.TDBMDaoGenerator::toCamelCase($fksForTable[0]->getLocalTableName());
285
                $fksByMethodName[$methodName] = $fksForTable[0];
286
            }
287
        }
288
289
        $code = '';
290
291
        foreach ($fksByMethodName as $methodName => $fk) {
292
            $getterCode = '    /**
293
     * Returns the list of %s pointing to this bean via the %s column.
294
     *
295
     * @return %s[]|ResultIterator
296
     */
297
    public function %s()
298
    {
299
        return $this->tdbmService->findObjects(%s, %s, %s);
300
    }
301
302
';
303
304
            list($sql, $parametersCode) = $this->getFilters($fk);
305
306
            $beanClass = TDBMDaoGenerator::getBeanNameFromTableName($fk->getLocalTableName());
307
            $code .= sprintf($getterCode,
308
                $beanClass,
309
                implode(', ', $fk->getColumns()),
310
                $beanClass,
311
                $methodName,
312
                var_export($fk->getLocalTableName(), true),
313
                $sql,
314
                $parametersCode
315
            );
316
        }
317
318
        return $code;
319
    }
320
321
    private function getFilters(ForeignKeyConstraint $fk)
322
    {
323
        $sqlParts = [];
324
        $counter = 0;
325
        $parameters = [];
326
327
        $pkColumns = $this->table->getPrimaryKeyColumns();
328
329
        foreach ($fk->getLocalColumns() as $columnName) {
330
            $paramName = 'tdbmparam'.$counter;
331
            $sqlParts[] = $fk->getLocalTableName().'.'.$columnName.' = :'.$paramName;
332
333
            $pkColumn = $pkColumns[$counter];
334
            $parameters[] = sprintf('%s => $this->get(%s, %s)', var_export($paramName, true), var_export($pkColumn, true), var_export($this->table->getName(), true));
335
            ++$counter;
336
        }
337
        $sql = "'".implode(' AND ', $sqlParts)."'";
338
        $parametersCode = '[ '.implode(', ', $parameters).' ]';
339
340
        return [$sql, $parametersCode];
341
    }
342
343
    /**
344
     * Generate code section about pivot tables.
345
     *
346
     * @return string
347
     */
348
    public function generatePivotTableCode()
349
    {
350
        $finalDescs = $this->getPivotTableDescriptors();
351
352
        $code = '';
353
354
        foreach ($finalDescs as $desc) {
355
            $code .= $this->getPivotTableCode($desc['name'], $desc['table'], $desc['localFK'], $desc['remoteFK']);
356
        }
357
358
        return $code;
359
    }
360
361
    private function getPivotTableDescriptors()
362
    {
363
        $descs = [];
364
        foreach ($this->schemaAnalyzer->detectJunctionTables() as $table) {
365
            // There are exactly 2 FKs since this is a pivot table.
366
            $fks = array_values($table->getForeignKeys());
367
368
            if ($fks[0]->getForeignTableName() === $this->table->getName()) {
369
                $localFK = $fks[0];
370
                $remoteFK = $fks[1];
371
            } elseif ($fks[1]->getForeignTableName() === $this->table->getName()) {
372
                $localFK = $fks[1];
373
                $remoteFK = $fks[0];
374
            } else {
375
                continue;
376
            }
377
378
            $descs[$remoteFK->getForeignTableName()][] = [
379
                'table' => $table,
380
                'localFK' => $localFK,
381
                'remoteFK' => $remoteFK,
382
            ];
383
        }
384
385
        $finalDescs = [];
386
        foreach ($descs as $descArray) {
387
            if (count($descArray) > 1) {
388
                foreach ($descArray as $desc) {
389
                    $desc['name'] = TDBMDaoGenerator::toCamelCase($desc['remoteFK']->getForeignTableName()).'By'.TDBMDaoGenerator::toCamelCase($desc['table']->getName());
390
                    $finalDescs[] = $desc;
391
                }
392
            } else {
393
                $desc = $descArray[0];
394
                $desc['name'] = TDBMDaoGenerator::toCamelCase($desc['remoteFK']->getForeignTableName());
395
                $finalDescs[] = $desc;
396
            }
397
        }
398
399
        return $finalDescs;
400
    }
401
402
    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...
403
    {
404
        $singularName = TDBMDaoGenerator::toSingular($name);
405
        $remoteBeanName = TDBMDaoGenerator::getBeanNameFromTableName($remoteFK->getForeignTableName());
406
        $variableName = '$'.TDBMDaoGenerator::toVariableName($remoteBeanName);
407
408
        $str = '    /**
409
     * Returns the list of %s associated to this bean via the %s pivot table.
410
     *
411
     * @return %s[]
412
     */
413
    public function get%s() {
414
        return $this->_getRelationships(%s);
415
    }
416
';
417
418
        $getterCode = sprintf($str, $remoteBeanName, $table->getName(), $remoteBeanName, $name, var_export($remoteFK->getLocalTableName(), true));
419
420
        $str = '    /**
421
     * Adds a relationship with %s associated to this bean via the %s pivot table.
422
     *
423
     * @param %s %s
424
     */
425
    public function add%s(%s %s) {
426
        return $this->addRelationship(%s, %s);
427
    }
428
';
429
430
        $adderCode = sprintf($str, $remoteBeanName, $table->getName(), $remoteBeanName, $variableName, $singularName, $remoteBeanName, $variableName, var_export($remoteFK->getLocalTableName(), true), $variableName);
431
432
        $str = '    /**
433
     * Deletes the relationship with %s associated to this bean via the %s pivot table.
434
     *
435
     * @param %s %s
436
     */
437
    public function remove%s(%s %s) {
438
        return $this->_removeRelationship(%s, %s);
439
    }
440
';
441
442
        $removerCode = sprintf($str, $remoteBeanName, $table->getName(), $remoteBeanName, $variableName, $singularName, $remoteBeanName, $variableName, var_export($remoteFK->getLocalTableName(), true), $variableName);
443
444
        $str = '    /**
445
     * Returns whether this bean is associated with %s via the %s pivot table.
446
     *
447
     * @param %s %s
448
     * @return bool
449
     */
450
    public function has%s(%s %s) {
451
        return $this->hasRelationship(%s, %s);
452
    }
453
';
454
455
        $hasCode = sprintf($str, $remoteBeanName, $table->getName(), $remoteBeanName, $variableName, $singularName, $remoteBeanName, $variableName, var_export($remoteFK->getLocalTableName(), true), $variableName);
456
457
        $code = $getterCode.$adderCode.$removerCode.$hasCode;
458
459
        return $code;
460
    }
461
462
    public function generateJsonSerialize()
463
    {
464
        $tableName = $this->table->getName();
465
        $parentFk = $this->schemaAnalyzer->getParentRelationship($tableName);
466
        if ($parentFk !== null) {
467
            $initializer = '$array = parent::jsonSerialize();';
468
        } else {
469
            $initializer = '$array = [];';
470
        }
471
472
        $str = '
473
    /**
474
     * Serializes the object for JSON encoding
475
     *
476
     * @param bool $stopRecursion Parameter used internally by TDBM to stop embedded objects from embedding other objects.
477
     * @return array
478
     */
479
    public function jsonSerialize($stopRecursion = false)
480
    {
481
        %s
482
%s
483
%s
484
        return $array;
485
    }
486
';
487
488
        $propertiesCode = '';
489
        foreach ($this->beanPropertyDescriptors as $beanPropertyDescriptor) {
490
            $propertiesCode .= $beanPropertyDescriptor->getJsonSerializeCode();
491
        }
492
493
        // Many to many relationships:
494
495
        $descs = $this->getPivotTableDescriptors();
496
497
        $many2manyCode = '';
498
499
        foreach ($descs as $desc) {
500
            $remoteFK = $desc['remoteFK'];
501
            $remoteBeanName = TDBMDaoGenerator::getBeanNameFromTableName($remoteFK->getForeignTableName());
502
            $variableName = '$'.TDBMDaoGenerator::toVariableName($remoteBeanName);
503
504
            $many2manyCode .= '        if (!$stopRecursion) {
505
            $array[\''.lcfirst($desc['name']).'\'] = array_map(function('.$remoteBeanName.' '.$variableName.') {
506
                return '.$variableName.'->jsonSerialize(true);
507
            }, $this->get'.$desc['name'].'());
508
        }
509
        ';
510
        }
511
512
        return sprintf($str, $initializer, $propertiesCode, $many2manyCode);
513
    }
514
515
    /**
516
     * Writes the PHP bean file with all getters and setters from the table passed in parameter.
517
     *
518
     * @param string $beannamespace The namespace of the bean
519
     */
520
    public function generatePhpCode($beannamespace)
521
    {
522
        $baseClassName = TDBMDaoGenerator::getBaseBeanNameFromTableName($this->table->getName());
523
        $className = TDBMDaoGenerator::getBeanNameFromTableName($this->table->getName());
524
        $tableName = $this->table->getName();
525
526
        $parentFk = $this->schemaAnalyzer->getParentRelationship($tableName);
527
        if ($parentFk !== null) {
528
            $extends = TDBMDaoGenerator::getBeanNameFromTableName($parentFk->getForeignTableName());
529
            $use = '';
530
        } else {
531
            $extends = 'AbstractTDBMObject';
532
            $use = "use Mouf\\Database\\TDBM\\AbstractTDBMObject;\n\n";
533
        }
534
535
        $str = "<?php
536
namespace {$beannamespace};
537
538
use Mouf\\Database\\TDBM\\ResultIterator;
539
$use
540
/*
541
 * This file has been automatically generated by TDBM.
542
 * DO NOT edit this file, as it might be overwritten.
543
 * If you need to perform changes, edit the $className class instead!
544
 */
545
546
/**
547
 * The $baseClassName class maps the '$tableName' table in database.
548
 */
549
class $baseClassName extends $extends implements \\JsonSerializable
550
{
551
";
552
553
        $str .= $this->generateBeanConstructor();
554
555
        foreach ($this->getExposedProperties() as $property) {
556
            $str .= $property->getGetterSetterCode();
557
        }
558
559
        $str .= $this->generateDirectForeignKeysCode();
560
        $str .= $this->generatePivotTableCode();
561
        $str .= $this->generateJsonSerialize();
562
563
        $str .= '}
564
';
565
566
        return $str;
567
    }
568
}
569