Passed
Pull Request — master (#81)
by Kevin
03:07
created

ScalarBeanPropertyDescriptor::getAnnotations()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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