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 getColumnName() 0 3 1
A hasDefault() 0 4 3
C getGetterSetterCode() 0 78 10
A isPrimaryKey() 0 7 2
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 TheCodingMachine\TDBM\TDBMException;
12
use TheCodingMachine\TDBM\Utils\Annotation\AnnotationParser;
13
use TheCodingMachine\TDBM\Utils\Annotation\Annotations;
14
use TheCodingMachine\TDBM\Utils\Annotation;
15
use Laminas\Code\Generator\AbstractMemberGenerator;
16
use Laminas\Code\Generator\DocBlock\Tag\ParamTag;
17
use Laminas\Code\Generator\DocBlock\Tag\ReturnTag;
18
use Laminas\Code\Generator\DocBlockGenerator;
19
use Laminas\Code\Generator\MethodGenerator;
20
use Laminas\Code\Generator\ParameterGenerator;
21
22
/**
23
 * This class represent a property in a bean (a property has a getter, a setter, etc...).
24
 */
25
class ScalarBeanPropertyDescriptor extends AbstractBeanPropertyDescriptor
26
{
27
    /**
28
     * @var Column
29
     */
30
    private $column;
31
32
    /**
33
     * @var Annotations
34
     */
35
    private $annotations;
36
37
    /**
38
     * @var AnnotationParser
39
     */
40
    private $annotationParser;
41
42
    /**
43
     * ScalarBeanPropertyDescriptor constructor.
44
     * @param Table $table
45
     * @param Column $column
46
     * @param NamingStrategyInterface $namingStrategy
47
     */
48
    public function __construct(Table $table, Column $column, NamingStrategyInterface $namingStrategy, AnnotationParser $annotationParser)
49
    {
50
        parent::__construct($table, $namingStrategy);
51
        $this->table = $table;
52
        $this->column = $column;
53
        $this->annotationParser = $annotationParser;
54
    }
55
56
    /**
57
     * Returns the name of the class linked to this property or null if this is not a foreign key.
58
     *
59
     * @return null|string
60
     */
61
    public function getClassName(): ?string
62
    {
63
        return null;
64
    }
65
66
    /**
67
     * Returns the PHP type for the property (it can be a scalar like int, bool, or class names, like \DateTimeInterface, App\Bean\User....)
68
     *
69
     * @return string
70
     */
71
    public function getPhpType(): string
72
    {
73
        $type = $this->column->getType();
74
        return TDBMDaoGenerator::dbalTypeToPhpType($type);
75
    }
76
77
    /**
78
     * Returns the Database type for the property
79
     *
80
     * @return Type
81
     */
82
    public function getDatabaseType(): Type
83
    {
84
        return $this->column->getType();
85
    }
86
87
    /**
88
     * Returns true if the property is compulsory (and therefore should be fetched in the constructor).
89
     *
90
     * @return bool
91
     */
92
    public function isCompulsory(): bool
93
    {
94
        return $this->column->getNotnull() && !$this->isAutoincrement() && $this->column->getDefault() === null && !$this->hasUuidAnnotation();
95
    }
96
97
    private function isAutoincrement(): bool
98
    {
99
        return $this->column->getAutoincrement() || $this->getAutoincrementAnnotation() !== null;
100
    }
101
102
    private function hasUuidAnnotation(): bool
103
    {
104
        return $this->getUuidAnnotation() !== null;
105
    }
106
107
    private function getUuidAnnotation(): ?Annotation\UUID
108
    {
109
        /** @var Annotation\UUID|null $annotation */
110
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\UUID::class);
111
        return $annotation;
112
    }
113
114
    private function getAutoincrementAnnotation(): ?Annotation\Autoincrement
115
    {
116
        /** @var Annotation\Autoincrement|null $annotation */
117
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\Autoincrement::class);
118
        return $annotation;
119
    }
120
121
    private function getAnnotations(): Annotations
122
    {
123
        if ($this->annotations === null) {
124
            $this->annotations = $this->annotationParser->getColumnAnnotations($this->column, $this->table);
125
        }
126
        return $this->annotations;
127
    }
128
129
    /**
130
     * Returns true if the property has a default value (or if the @UUID annotation is set for the column)
131
     *
132
     * @return bool
133
     */
134
    public function hasDefault(): bool
135
    {
136
        // MariaDB 10.3 issue: it returns "NULL" (the string) instead of *null*
137
        return ($this->column->getDefault() !== null && $this->column->getDefault() !== 'NULL') || $this->hasUuidAnnotation();
138
    }
139
140
    /**
141
     * Returns the code that assigns a value to its default value.
142
     *
143
     * @return string
144
     */
145
    public function assignToDefaultCode(): string
146
    {
147
        $str = '$this->%s(%s);';
148
149
        $uuidAnnotation = $this->getUuidAnnotation();
150
        if ($uuidAnnotation !== null) {
151
            $defaultCode = $this->getUuidCode($uuidAnnotation);
152
        } else {
153
            $default = $this->column->getDefault();
154
            $type = $this->column->getType();
155
156
            if (in_array($type->getName(), [
157
                'datetime',
158
                'datetime_immutable',
159
                'datetimetz',
160
                'datetimetz_immutable',
161
                'date',
162
                'date_immutable',
163
                'time',
164
                'time_immutable',
165
            ], true)) {
166
                if ($default !== null && in_array(strtoupper($default), ['CURRENT_TIMESTAMP' /* MySQL */, 'NOW()' /* PostgreSQL */, 'SYSDATE' /* Oracle */ , 'CURRENT_TIMESTAMP()' /* MariaDB 10.3 */], true)) {
167
                    $defaultCode = 'new \DateTimeImmutable()';
168
                } else {
169
                    throw new TDBMException('Unable to set default value for date in "'.$this->table->getName().'.'.$this->column->getName().'". Database passed this default value: "'.$default.'"');
170
                }
171
            } else {
172
                $defaultValue = $type->convertToPHPValue($this->column->getDefault(), new MySQL57Platform());
173
                $defaultCode = var_export($defaultValue, true);
174
            }
175
        }
176
177
        return sprintf($str, $this->getSetterName(), $defaultCode);
178
    }
179
180
    private function getUuidCode(Annotation\UUID $uuidAnnotation): string
181
    {
182
        $comment = $uuidAnnotation->value;
183
        switch ($comment) {
184
            case '':
185
            case 'v1':
186
                return 'Uuid::uuid1()->toString()';
187
            case 'v4':
188
                return 'Uuid::uuid4()->toString()';
189
            default:
190
                throw new TDBMException('@UUID annotation accepts either "v1" or "v4" parameter. Unexpected parameter: ' . $comment);
191
        }
192
    }
193
194
    /**
195
     * Returns true if the property is the primary key.
196
     *
197
     * @return bool
198
     */
199
    public function isPrimaryKey(): bool
200
    {
201
        $primaryKey = $this->table->getPrimaryKey();
202
        if ($primaryKey === null) {
203
            return false;
204
        }
205
        return in_array($this->column->getName(), $primaryKey->getUnquotedColumns());
206
    }
207
208
    /**
209
     * Returns the PHP code for getters and setters.
210
     *
211
     * @return (MethodGenerator|null)[]
212
     */
213
    public function getGetterSetterCode(): array
214
    {
215
        $normalizedType = $this->getPhpType();
216
217
        $columnGetterName = $this->getGetterName();
218
        $columnSetterName = $this->getSetterName();
219
        $variableName = ltrim($this->getSafeVariableName(), '$');
220
221
        // 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).
222
        $isNullable = !$this->column->getNotnull() || $this->isAutoincrement();
223
224
        $resourceTypeCheck = '';
225
        if ($normalizedType === 'resource') {
226
            $checkNullable = '';
227
            if ($isNullable) {
228
                $checkNullable = sprintf('$%s !== null && ', $variableName);
229
            }
230
            $resourceTypeCheck .= <<<EOF
231
if (%s!\is_resource($%s)) {
232
    throw \TheCodingMachine\TDBM\TDBMInvalidArgumentException::badType('resource', $%s, __METHOD__);
233
}
234
EOF;
235
            $resourceTypeCheck = sprintf($resourceTypeCheck, $checkNullable, $variableName, $variableName);
236
        }
237
238
        $types = [ $normalizedType ];
239
        if ($isNullable) {
240
            $types[] = 'null';
241
        }
242
243
        $paramType = null;
244
        if ($this->isTypeHintable()) {
245
            $paramType = ($isNullable ? '?' : '').$normalizedType;
246
        }
247
248
        $getter = new MethodGenerator($columnGetterName);
249
        $getterDocBlock = new DocBlockGenerator(sprintf('The getter for the "%s" column.', $this->column->getName()));
250
        $getterDocBlock->setTag(new ReturnTag($types))->setWordWrap(false);
251
        $getter->setDocBlock($getterDocBlock);
252
        $getter->setReturnType($paramType);
253
254
        $getter->setBody(sprintf(
255
            'return $this->get(%s, %s);',
256
            var_export($this->column->getName(), true),
257
            var_export($this->table->getName(), true)
258
        ));
259
260
        if ($this->isGetterProtected()) {
261
            $getter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
262
        }
263
264
        if (!$this->isReadOnly()) {
265
            $setter = new MethodGenerator($columnSetterName);
266
            $setterDocBlock = new DocBlockGenerator(sprintf('The setter for the "%s" column.', $this->column->getName()));
267
            $setterDocBlock->setTag(new ParamTag($variableName, $types))->setWordWrap(false);
268
            $setter->setDocBlock($setterDocBlock);
269
270
            $parameter = new ParameterGenerator($variableName, $paramType);
271
            $setter->setParameter($parameter);
272
            $setter->setReturnType('void');
273
274
            $setter->setBody(sprintf(
275
                '%s
276
$this->set(%s, $%s, %s);',
277
                $resourceTypeCheck,
278
                var_export($this->column->getName(), true),
279
                $variableName,
280
                var_export($this->table->getName(), true)
281
            ));
282
283
            if ($this->isSetterProtected()) {
284
                $setter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
285
            }
286
        } else {
287
            $setter = null;
288
        }
289
290
        return [$getter, $setter];
291
    }
292
293
    /**
294
     * Returns the part of code useful when doing json serialization.
295
     *
296
     * @return string
297
     */
298
    public function getJsonSerializeCode(): string
299
    {
300
        if ($this->findAnnotation(Annotation\JsonIgnore::class)) {
301
            return '';
302
        }
303
304
        if (!$this->canBeSerialized()) {
305
            return '';
306
        }
307
308
        // Do not export the property is the getter is protected.
309
        if ($this->isGetterProtected()) {
310
            return '';
311
        }
312
313
        /** @var Annotation\JsonKey|null $jsonKey */
314
        $jsonKey = $this->findAnnotation(Annotation\JsonKey::class);
315
        $index = $jsonKey ? $jsonKey->key : $this->namingStrategy->getJsonProperty($this);
316
        $getter = $this->getGetterName();
317
        switch ($this->getPhpType()) {
318
            case '\\DateTimeImmutable':
319
                /** @var Annotation\JsonFormat|null $jsonFormat */
320
                $jsonFormat = $this->findAnnotation(Annotation\JsonFormat::class);
321
                $format = $jsonFormat ? $jsonFormat->datetime : 'c';
322
                if ($this->column->getNotnull()) {
323
                    return "\$array['$index'] = \$this->$getter()->format('$format');";
324
                } else {
325
                    return "\$array['$index'] = (\$date = \$this->$getter()) ? \$date->format('$format') : null;";
326
                }
327
                // no break
328
            case 'int':
329
            case 'float':
330
                /** @var Annotation\JsonFormat|null $jsonFormat */
331
                $jsonFormat = $this->findAnnotation(Annotation\JsonFormat::class);
332
                if ($jsonFormat) {
333
                    $args = [$jsonFormat->decimals, $jsonFormat->point, $jsonFormat->separator];
334
                    for ($i = 2; $i >= 0; --$i) {
335
                        if ($args[$i] === null) {
336
                            unset($args[$i]);
337
                        } else {
338
                            break;
339
                        }
340
                    }
341
                    $args = array_map(function ($v) {
342
                        return var_export($v, true);
343
                    }, $args);
344
                    $args = empty($args) ? '' : ', ' . implode(', ', $args);
345
                    $unit = $jsonFormat->unit ? ' . ' . var_export($jsonFormat->unit, true) : '';
346
                    if ($this->column->getNotnull()) {
347
                        return "\$array['$index'] = number_format(\$this->$getter()$args)$unit;";
348
                    } else {
349
                        return "\$array['$index'] = \$this->$getter() !== null ? number_format(\$this->$getter()$args)$unit : null;";
350
                    }
351
                }
352
                // no break
353
            default:
354
                return "\$array['$index'] = \$this->$getter();";
355
        }
356
    }
357
358
    /**
359
     * Returns the column name.
360
     *
361
     * @return string
362
     */
363
    public function getColumnName(): string
364
    {
365
        return $this->column->getName();
366
    }
367
368
    /**
369
     * The code to past in the __clone method.
370
     * @return null|string
371
     */
372
    public function getCloneRule(): ?string
373
    {
374
        $uuidAnnotation = $this->getUuidAnnotation();
375
        if ($uuidAnnotation !== null && $this->isPrimaryKey()) {
376
            return sprintf("\$this->%s(%s);\n", $this->getSetterName(), $this->getUuidCode($uuidAnnotation));
377
        }
378
        return null;
379
    }
380
381
    /**
382
     * tells is this type is suitable for Json Serialization
383
     *
384
     * @return bool
385
     */
386
    public function canBeSerialized(): bool
387
    {
388
        $type = $this->column->getType();
389
390
        $unserialisableTypes = [
391
            Type::BLOB,
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::BLOB has been deprecated: Use {@see Types::BLOB} instead. ( Ignorable by Annotation )

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

391
            /** @scrutinizer ignore-deprecated */ Type::BLOB,

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

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

Loading history...
392
            Type::BINARY
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::BINARY has been deprecated: Use {@see Types::BINARY} instead. ( Ignorable by Annotation )

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

392
            /** @scrutinizer ignore-deprecated */ Type::BINARY

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

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

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