Completed
Push — master ( f50fbb...681f17 )
by David
20s queued 12s
created

ScalarBeanPropertyDescriptor::isReadOnly()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

390
            /** @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...
391
            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

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