Passed
Pull Request — master (#116)
by David
03:39
created

getJsonSerializeCode()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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