ScalarBeanPropertyDescriptor   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Importance

Changes 10
Bugs 1 Features 3
Metric Value
eloc 170
c 10
b 1
f 3
dl 0
loc 406
rs 3.2
wmc 65

24 Methods

Rating   Name   Duplication   Size   Complexity  
A hasDefault() 0 4 3
A isPrimaryKey() 0 7 2
A getColumnName() 0 3 1
C getGetterSetterCode() 0 78 10
A getPhpType() 0 4 1
A getCloneRule() 0 7 3
A isAutoincrement() 0 3 2
A assignToDefaultCode() 0 33 5
A isSetterProtected() 0 3 1
A getUuidCode() 0 11 4
A hasUuidAnnotation() 0 3 1
A getClassName() 0 3 1
A isGetterProtected() 0 3 1
A isTypeHintable() 0 8 1
A isReadOnly() 0 3 1
A getDatabaseType() 0 3 1
A isCompulsory() 0 3 4
A findAnnotation() 0 3 1
C getJsonSerializeCode() 0 57 16
A getAutoincrementAnnotation() 0 5 1
A __construct() 0 6 1
A getAnnotations() 0 6 2
A getUuidAnnotation() 0 5 1
A canBeSerialized() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like ScalarBeanPropertyDescriptor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ScalarBeanPropertyDescriptor, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace TheCodingMachine\TDBM\Utils;
6
7
use Doctrine\DBAL\Platforms\MySQL57Platform;
8
use Doctrine\DBAL\Schema\Column;
9
use Doctrine\DBAL\Schema\Table;
10
use Doctrine\DBAL\Types\Type;
11
use Doctrine\DBAL\Types\Types;
12
use TheCodingMachine\TDBM\TDBMException;
13
use TheCodingMachine\TDBM\Utils\Annotation\AnnotationParser;
14
use TheCodingMachine\TDBM\Utils\Annotation\Annotations;
15
use TheCodingMachine\TDBM\Utils\Annotation;
16
use Laminas\Code\Generator\AbstractMemberGenerator;
17
use Laminas\Code\Generator\DocBlock\Tag\ParamTag;
18
use Laminas\Code\Generator\DocBlock\Tag\ReturnTag;
19
use Laminas\Code\Generator\DocBlockGenerator;
20
use Laminas\Code\Generator\MethodGenerator;
21
use Laminas\Code\Generator\ParameterGenerator;
22
23
/**
24
 * This class represent a property in a bean (a property has a getter, a setter, etc...).
25
 */
26
class ScalarBeanPropertyDescriptor extends AbstractBeanPropertyDescriptor
27
{
28
    /**
29
     * @var Column
30
     */
31
    private $column;
32
33
    /**
34
     * @var Annotations
35
     */
36
    private $annotations;
37
38
    /**
39
     * @var AnnotationParser
40
     */
41
    private $annotationParser;
42
43
    /**
44
     * ScalarBeanPropertyDescriptor constructor.
45
     * @param Table $table
46
     * @param Column $column
47
     * @param NamingStrategyInterface $namingStrategy
48
     */
49
    public function __construct(Table $table, Column $column, NamingStrategyInterface $namingStrategy, AnnotationParser $annotationParser)
50
    {
51
        parent::__construct($table, $namingStrategy);
52
        $this->table = $table;
53
        $this->column = $column;
54
        $this->annotationParser = $annotationParser;
55
    }
56
57
    /**
58
     * Returns the name of the class linked to this property or null if this is not a foreign key.
59
     *
60
     * @return null|string
61
     */
62
    public function getClassName(): ?string
63
    {
64
        return null;
65
    }
66
67
    /**
68
     * Returns the PHP type for the property (it can be a scalar like int, bool, or class names, like \DateTimeInterface, App\Bean\User....)
69
     *
70
     * @return string
71
     */
72
    public function getPhpType(): string
73
    {
74
        $type = $this->column->getType();
75
        return TDBMDaoGenerator::dbalTypeToPhpType($type);
76
    }
77
78
    /**
79
     * Returns the Database type for the property
80
     *
81
     * @return Type
82
     */
83
    public function getDatabaseType(): Type
84
    {
85
        return $this->column->getType();
86
    }
87
88
    /**
89
     * Returns true if the property is compulsory (and therefore should be fetched in the constructor).
90
     *
91
     * @return bool
92
     */
93
    public function isCompulsory(): bool
94
    {
95
        return $this->column->getNotnull() && !$this->isAutoincrement() && $this->column->getDefault() === null && !$this->hasUuidAnnotation();
96
    }
97
98
    private function isAutoincrement(): bool
99
    {
100
        return $this->column->getAutoincrement() || $this->getAutoincrementAnnotation() !== null;
101
    }
102
103
    private function hasUuidAnnotation(): bool
104
    {
105
        return $this->getUuidAnnotation() !== null;
106
    }
107
108
    private function getUuidAnnotation(): ?Annotation\UUID
109
    {
110
        /** @var Annotation\UUID|null $annotation */
111
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\UUID::class);
112
        return $annotation;
113
    }
114
115
    private function getAutoincrementAnnotation(): ?Annotation\Autoincrement
116
    {
117
        /** @var Annotation\Autoincrement|null $annotation */
118
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\Autoincrement::class);
119
        return $annotation;
120
    }
121
122
    private function getAnnotations(): Annotations
123
    {
124
        if ($this->annotations === null) {
125
            $this->annotations = $this->annotationParser->getColumnAnnotations($this->column, $this->table);
126
        }
127
        return $this->annotations;
128
    }
129
130
    /**
131
     * Returns true if the property has a default value (or if the @UUID annotation is set for the column)
132
     *
133
     * @return bool
134
     */
135
    public function hasDefault(): bool
136
    {
137
        // MariaDB 10.3 issue: it returns "NULL" (the string) instead of *null*
138
        return ($this->column->getDefault() !== null && $this->column->getDefault() !== 'NULL') || $this->hasUuidAnnotation();
139
    }
140
141
    /**
142
     * Returns the code that assigns a value to its default value.
143
     *
144
     * @return string
145
     */
146
    public function assignToDefaultCode(): string
147
    {
148
        $str = '$this->%s(%s);';
149
150
        $uuidAnnotation = $this->getUuidAnnotation();
151
        if ($uuidAnnotation !== null) {
152
            $defaultCode = $this->getUuidCode($uuidAnnotation);
153
        } else {
154
            $default = $this->column->getDefault();
155
            $type = $this->column->getType();
156
157
            if (in_array($type->getName(), [
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Types\Type::getName() has been deprecated: this method will be removed in Doctrine DBAL 4.0, use {@see TypeRegistry::lookupName()} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

157
            if (in_array(/** @scrutinizer ignore-deprecated */ $type->getName(), [

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
158
                'datetime',
159
                'datetime_immutable',
160
                'datetimetz',
161
                'datetimetz_immutable',
162
                'date',
163
                'date_immutable',
164
                'time',
165
                'time_immutable',
166
            ], true)) {
167
                if ($default !== null && in_array(strtoupper($default), ['CURRENT_TIMESTAMP' /* MySQL */, 'NOW()' /* PostgreSQL */, 'SYSDATE' /* Oracle */ , 'CURRENT_TIMESTAMP()' /* MariaDB 10.3 */], true)) {
168
                    $defaultCode = 'new \DateTimeImmutable()';
169
                } else {
170
                    throw new TDBMException('Unable to set default value for date in "'.$this->table->getName().'.'.$this->column->getName().'". Database passed this default value: "'.$default.'"');
171
                }
172
            } else {
173
                $defaultValue = $type->convertToPHPValue($this->column->getDefault(), new MySQL57Platform());
0 ignored issues
show
Deprecated Code introduced by
The class Doctrine\DBAL\Platforms\MySQL57Platform has been deprecated: This class will be merged with {@see MySQLPlatform} in 4.0 because support for MySQL releases prior to 5.7 will be dropped. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

173
                $defaultValue = $type->convertToPHPValue($this->column->getDefault(), /** @scrutinizer ignore-deprecated */ new MySQL57Platform());
Loading history...
174
                $defaultCode = var_export($defaultValue, true);
175
            }
176
        }
177
178
        return sprintf($str, $this->getSetterName(), $defaultCode);
179
    }
180
181
    private function getUuidCode(Annotation\UUID $uuidAnnotation): string
182
    {
183
        $comment = $uuidAnnotation->value;
184
        switch ($comment) {
185
            case '':
186
            case 'v1':
187
                return 'Uuid::uuid1()->toString()';
188
            case 'v4':
189
                return 'Uuid::uuid4()->toString()';
190
            default:
191
                throw new TDBMException('@UUID annotation accepts either "v1" or "v4" parameter. Unexpected parameter: ' . $comment);
192
        }
193
    }
194
195
    /**
196
     * Returns true if the property is the primary key.
197
     *
198
     * @return bool
199
     */
200
    public function isPrimaryKey(): bool
201
    {
202
        $primaryKey = $this->table->getPrimaryKey();
203
        if ($primaryKey === null) {
204
            return false;
205
        }
206
        return in_array($this->column->getName(), $primaryKey->getUnquotedColumns());
207
    }
208
209
    /**
210
     * Returns the PHP code for getters and setters.
211
     *
212
     * @return (MethodGenerator|null)[]
213
     */
214
    public function getGetterSetterCode(): array
215
    {
216
        $normalizedType = $this->getPhpType();
217
218
        $columnGetterName = $this->getGetterName();
219
        $columnSetterName = $this->getSetterName();
220
        $variableName = ltrim($this->getSafeVariableName(), '$');
221
222
        // A column type can be forced if it is not nullable and not auto-incrementable (for auto-increment columns, we can get "null" as long as the bean is not saved).
223
        $isNullable = !$this->column->getNotnull() || $this->isAutoincrement();
224
225
        $resourceTypeCheck = '';
226
        if ($normalizedType === 'resource') {
227
            $checkNullable = '';
228
            if ($isNullable) {
229
                $checkNullable = sprintf('$%s !== null && ', $variableName);
230
            }
231
            $resourceTypeCheck .= <<<EOF
232
if (%s!\is_resource($%s)) {
233
    throw \TheCodingMachine\TDBM\TDBMInvalidArgumentException::badType('resource', $%s, __METHOD__);
234
}
235
EOF;
236
            $resourceTypeCheck = sprintf($resourceTypeCheck, $checkNullable, $variableName, $variableName);
237
        }
238
239
        $types = [ $normalizedType ];
240
        if ($isNullable) {
241
            $types[] = 'null';
242
        }
243
244
        $paramType = null;
245
        if ($this->isTypeHintable()) {
246
            $paramType = ($isNullable ? '?' : '').$normalizedType;
247
        }
248
249
        $getter = new MethodGenerator($columnGetterName);
250
        $getterDocBlock = new DocBlockGenerator(sprintf('The getter for the "%s" column.', $this->column->getName()));
251
        $getterDocBlock->setTag(new ReturnTag($types))->setWordWrap(false);
252
        $getter->setDocBlock($getterDocBlock);
253
        $getter->setReturnType($paramType);
254
255
        $getter->setBody(sprintf(
256
            'return $this->get(%s, %s);',
257
            var_export($this->column->getName(), true),
258
            var_export($this->table->getName(), true)
259
        ));
260
261
        if ($this->isGetterProtected()) {
262
            $getter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
263
        }
264
265
        if (!$this->isReadOnly()) {
266
            $setter = new MethodGenerator($columnSetterName);
267
            $setterDocBlock = new DocBlockGenerator(sprintf('The setter for the "%s" column.', $this->column->getName()));
268
            $setterDocBlock->setTag(new ParamTag($variableName, $types))->setWordWrap(false);
269
            $setter->setDocBlock($setterDocBlock);
270
271
            $parameter = new ParameterGenerator($variableName, $paramType);
272
            $setter->setParameter($parameter);
273
            $setter->setReturnType('void');
274
275
            $setter->setBody(sprintf(
276
                '%s
277
$this->set(%s, $%s, %s);',
278
                $resourceTypeCheck,
279
                var_export($this->column->getName(), true),
280
                $variableName,
281
                var_export($this->table->getName(), true)
282
            ));
283
284
            if ($this->isSetterProtected()) {
285
                $setter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
286
            }
287
        } else {
288
            $setter = null;
289
        }
290
291
        return [$getter, $setter];
292
    }
293
294
    /**
295
     * Returns the part of code useful when doing json serialization.
296
     *
297
     * @return string
298
     */
299
    public function getJsonSerializeCode(): string
300
    {
301
        if ($this->findAnnotation(Annotation\JsonIgnore::class)) {
302
            return '';
303
        }
304
305
        if (!$this->canBeSerialized()) {
306
            return '';
307
        }
308
309
        // Do not export the property is the getter is protected.
310
        if ($this->isGetterProtected()) {
311
            return '';
312
        }
313
314
        /** @var Annotation\JsonKey|null $jsonKey */
315
        $jsonKey = $this->findAnnotation(Annotation\JsonKey::class);
316
        $index = $jsonKey ? $jsonKey->key : $this->namingStrategy->getJsonProperty($this);
317
        $getter = $this->getGetterName();
318
        switch ($this->getPhpType()) {
319
            case '\\DateTimeImmutable':
320
                /** @var Annotation\JsonFormat|null $jsonFormat */
321
                $jsonFormat = $this->findAnnotation(Annotation\JsonFormat::class);
322
                $format = $jsonFormat ? $jsonFormat->datetime : 'c';
323
                if ($this->column->getNotnull()) {
324
                    return "\$array['$index'] = \$this->$getter()->format('$format');";
325
                } else {
326
                    return "\$array['$index'] = (\$date = \$this->$getter()) ? \$date->format('$format') : null;";
327
                }
328
                // no break
329
            case 'int':
330
            case 'float':
331
                /** @var Annotation\JsonFormat|null $jsonFormat */
332
                $jsonFormat = $this->findAnnotation(Annotation\JsonFormat::class);
333
                if ($jsonFormat) {
334
                    $args = [$jsonFormat->decimals, $jsonFormat->point, $jsonFormat->separator];
335
                    for ($i = 2; $i >= 0; --$i) {
336
                        if ($args[$i] === null) {
337
                            unset($args[$i]);
338
                        } else {
339
                            break;
340
                        }
341
                    }
342
                    $args = array_map(function ($v) {
343
                        return var_export($v, true);
344
                    }, $args);
345
                    $args = empty($args) ? '' : ', ' . implode(', ', $args);
346
                    $unit = $jsonFormat->unit ? ' . ' . var_export($jsonFormat->unit, true) : '';
347
                    if ($this->column->getNotnull()) {
348
                        return "\$array['$index'] = number_format(\$this->$getter()$args)$unit;";
349
                    } else {
350
                        return "\$array['$index'] = \$this->$getter() !== null ? number_format(\$this->$getter()$args)$unit : null;";
351
                    }
352
                }
353
                // no break
354
            default:
355
                return "\$array['$index'] = \$this->$getter();";
356
        }
357
    }
358
359
    /**
360
     * Returns the column name.
361
     *
362
     * @return string
363
     */
364
    public function getColumnName(): string
365
    {
366
        return $this->column->getName();
367
    }
368
369
    /**
370
     * The code to past in the __clone method.
371
     * @return null|string
372
     */
373
    public function getCloneRule(): ?string
374
    {
375
        $uuidAnnotation = $this->getUuidAnnotation();
376
        if ($uuidAnnotation !== null && $this->isPrimaryKey()) {
377
            return sprintf("\$this->%s(%s);\n", $this->getSetterName(), $this->getUuidCode($uuidAnnotation));
378
        }
379
        return null;
380
    }
381
382
    /**
383
     * tells is this type is suitable for Json Serialization
384
     *
385
     * @return bool
386
     */
387
    public function canBeSerialized(): bool
388
    {
389
        $type = $this->column->getType();
390
391
        $unserialisableTypes = [
392
            Types::BLOB,
393
            Types::BINARY
394
        ];
395
396
        return \in_array($type->getName(), $unserialisableTypes, true) === false;
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Types\Type::getName() has been deprecated: this method will be removed in Doctrine DBAL 4.0, use {@see TypeRegistry::lookupName()} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

396
        return \in_array(/** @scrutinizer ignore-deprecated */ $type->getName(), $unserialisableTypes, true) === false;

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
397
    }
398
399
    /**
400
     * Tells if this property is a type-hintable in PHP (resource isn't for example)
401
     *
402
     * @return bool
403
     */
404
    public function isTypeHintable(): bool
405
    {
406
        $type = $this->getPhpType();
407
        $invalidScalarTypes = [
408
            'resource'
409
        ];
410
411
        return \in_array($type, $invalidScalarTypes, true) === false;
412
    }
413
414
    private function isGetterProtected(): bool
415
    {
416
        return $this->findAnnotation(Annotation\ProtectedGetter::class) !== null;
417
    }
418
419
    private function isSetterProtected(): bool
420
    {
421
        return $this->findAnnotation(Annotation\ProtectedSetter::class) !== null;
422
    }
423
424
    public function isReadOnly(): bool
425
    {
426
        return $this->findAnnotation(Annotation\ReadOnlyColumn::class) !== null;
427
    }
428
429
    private function findAnnotation(string $type): ?object
430
    {
431
        return $this->getAnnotations()->findAnnotation($type);
432
    }
433
}
434