Completed
Push — master ( 1b9acd...4573dd )
by David
26s queued 11s
created

getGetterSetterCode()   C

Complexity

Conditions 10
Paths 288

Size

Total Lines 74
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 47
dl 0
loc 74
rs 5.743
c 0
b 0
f 0
cc 10
nc 288
nop 0

How to fix   Long Method    Complexity   

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