Passed
Push — phpstan ( 80c800 )
by David
03:18
created

ScalarBeanPropertyDescriptor::isPrimaryKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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