Passed
Pull Request — master (#186)
by David
15:40
created

ScalarBeanPropertyDescriptor::getAnnotations()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

375
            /** @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...
376
            Type::BINARY
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::BINARY has been deprecated: Use {@see DefaultTypes::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

376
            /** @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...
377
        ];
378
379
        return \in_array($type->getName(), $unserialisableTypes, true) === false;
380
    }
381
382
    /**
383
     * Tells if this property is a type-hintable in PHP (resource isn't for example)
384
     *
385
     * @return bool
386
     */
387
    public function isTypeHintable() : bool
388
    {
389
        $type = $this->getPhpType();
390
        $invalidScalarTypes = [
391
            'resource'
392
        ];
393
394
        return \in_array($type, $invalidScalarTypes, true) === false;
395
    }
396
397
    private function isGetterProtected(): bool
398
    {
399
        return $this->findAnnotation(Annotation\ProtectedGetter::class) !== null;
400
    }
401
402
    private function isSetterProtected(): bool
403
    {
404
        return $this->findAnnotation(Annotation\ProtectedSetter::class) !== null;
405
    }
406
407
    /**
408
     * @param string $type
409
     * @return null|object
410
     */
411
    private function findAnnotation(string $type)
412
    {
413
        return $this->getAnnotations()->findAnnotation($type);
414
    }
415
}
416