Passed
Pull Request — master (#132)
by Dorian
04:52
created

ScalarBeanPropertyDescriptor::isJsonSkipped()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
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\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
            $resourceTypeCheck .= <<<EOF
214
215
if (!\is_resource($%s)) {
216
    throw \TheCodingMachine\TDBM\TDBMInvalidArgumentException::badType('resource', $%s, __METHOD__);
217
}
218
EOF;
219
            $resourceTypeCheck = sprintf($resourceTypeCheck, $this->column->getName(), $this->column->getName());
220
        }
221
222
223
        $paramType = null;
224
        if ($this->isTypeHintable()) {
225
            $paramType = ($isNullable?'?':'').$normalizedType;
226
        }
227
228
        $getter = new MethodGenerator($columnGetterName);
229
        $getter->setDocBlock(sprintf('The getter for the "%s" column.', $this->column->getName()));
230
231
        $types = [ $normalizedType ];
232
        if ($isNullable) {
233
            $types[] = 'null';
234
        }
235
        $getter->getDocBlock()->setTag(new ReturnTag($types));
236
237
        $getter->setReturnType($paramType);
238
239
        $getter->setBody(sprintf('return $this->get(%s, %s);',
240
            var_export($this->column->getName(), true),
241
            var_export($this->table->getName(), true)));
242
243
        if ($this->isGetterProtected()) {
244
            $getter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
245
        }
246
247
        $setter = new MethodGenerator($columnSetterName);
248
        $setter->setDocBlock(sprintf('The setter for the "%s" column.', $this->column->getName()));
249
250
        $types = [ $normalizedType ];
251
        if ($isNullable) {
252
            $types[] = 'null';
253
        }
254
        $setter->getDocBlock()->setTag(new ParamTag($this->column->getName(), $types));
255
256
        $parameter = new ParameterGenerator($this->column->getName(), $paramType);
257
        $setter->setParameter($parameter);
258
        $setter->setReturnType('void');
259
260
        $setter->setBody(sprintf('%s
261
$this->set(%s, $%s, %s);',
262
            $resourceTypeCheck,
263
            var_export($this->column->getName(), true),
264
            $this->column->getName(),
265
            var_export($this->table->getName(), true)));
266
267
        if ($this->isSetterProtected()) {
268
            $setter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED);
269
        }
270
271
        return [$getter, $setter];
272
    }
273
274
    /**
275
     * Returns the part of code useful when doing json serialization.
276
     *
277
     * @return string
278
     */
279
    public function getJsonSerializeCode(): string
280
    {
281
        $normalizedType = $this->getPhpType();
282
283
        if (!$this->canBeSerialized()) {
284
            return '';
285
        }
286
287
        if ($this->isJsonSkipped()) {
288
            return '';
289
        }
290
291
        // Do not export the property is the getter is protected.
292
        if ($this->isGetterProtected()) {
293
            return '';
294
        }
295
296
        $index = var_export($this->namingStrategy->getJsonProperty($this), true);
297
        $getter = $this->getGetterName();
298
299
        if ($normalizedType == '\\DateTimeImmutable') {
300
            $format = $this->getJsonFormat();
301
            if ($this->column->getNotnull()) {
302
                return "\$array[$index] = \$this->$getter()->format('$format');";
303
            } else {
304
                $variableName = $this->namingStrategy->getVariableName($this);
305
                if ($variableName === '$array') {
306
                    $variableName = '$date';
307
                }
308
                return "\$array[$index] = ($variableName = \$this->$getter()) ? ${variableName}->format('$format') : null;";
309
            }
310
        } else {
311
            return "\$array[$index] = \$this->$getter();";
312
        }
313
    }
314
315
    /**
316
     * Returns the column name.
317
     *
318
     * @return string
319
     */
320
    public function getColumnName(): string
321
    {
322
        return $this->column->getName();
323
    }
324
325
    /**
326
     * The code to past in the __clone method.
327
     * @return null|string
328
     */
329
    public function getCloneRule(): ?string
330
    {
331
        $uuidAnnotation = $this->getUuidAnnotation();
332
        if ($uuidAnnotation !== null && $this->isPrimaryKey()) {
333
            return sprintf("\$this->%s(%s);\n", $this->getSetterName(), $this->getUuidCode($uuidAnnotation));
334
        }
335
        return null;
336
    }
337
338
    /**
339
     * tells is this type is suitable for Json Serialization
340
     *
341
     * @return bool
342
     */
343
    public function canBeSerialized() : bool
344
    {
345
        $type = $this->column->getType();
346
347
        $unserialisableTypes = [
348
            Type::BLOB,
349
            Type::BINARY
350
        ];
351
352
        return \in_array($type->getName(), $unserialisableTypes, true) === false;
353
    }
354
355
    /**
356
     * Tells if this property is a type-hintable in PHP (resource isn't for example)
357
     *
358
     * @return bool
359
     */
360
    public function isTypeHintable() : bool
361
    {
362
        $type = $this->getPhpType();
363
        $invalidScalarTypes = [
364
            'resource'
365
        ];
366
367
        return \in_array($type, $invalidScalarTypes, true) === false;
368
    }
369
370
    private function isGetterProtected(): bool
371
    {
372
        /** @var Annotation\ProtectedGetter|null $annotation */
373
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\ProtectedGetter::class);
374
        return $annotation !== null;
375
    }
376
377
    private function isSetterProtected(): bool
378
    {
379
        /** @var Annotation\ProtectedSetter|null $annotation */
380
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\ProtectedSetter::class);
381
        return $annotation !== null;
382
    }
383
384
    private function isJsonSkipped(): bool
385
    {
386
        /** @var Annotation\JsonSkip|null $annotation */
387
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\JsonSkip::class);
388
        return $annotation !== null;
389
    }
390
391
    private function getJsonFormat(): string
392
    {
393
        /** @var Annotation\JsonFormat|null $annotation */
394
        $annotation = $this->getAnnotations()->findAnnotation(Annotation\JsonFormat::class);
395
        if ($annotation === null) {
396
            return 'c';
397
        } else {
398
            return $annotation->format;
399
        }
400
    }
401
}
402