1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace TheCodingMachine\TDBM\Utils; |
6
|
|
|
|
7
|
|
|
use Doctrine\DBAL\Platforms\MySQL57Platform; |
8
|
|
|
use Doctrine\DBAL\Schema\Column; |
9
|
|
|
use Doctrine\DBAL\Schema\Table; |
10
|
|
|
use Doctrine\DBAL\Types\Type; |
11
|
|
|
use TheCodingMachine\TDBM\TDBMException; |
12
|
|
|
use TheCodingMachine\TDBM\Utils\Annotation\AnnotationParser; |
13
|
|
|
use TheCodingMachine\TDBM\Utils\Annotation\Annotations; |
14
|
|
|
use TheCodingMachine\TDBM\Utils\Annotation; |
15
|
|
|
use Laminas\Code\Generator\AbstractMemberGenerator; |
16
|
|
|
use Laminas\Code\Generator\DocBlock\Tag\ParamTag; |
17
|
|
|
use Laminas\Code\Generator\DocBlock\Tag\ReturnTag; |
18
|
|
|
use Laminas\Code\Generator\DocBlockGenerator; |
19
|
|
|
use Laminas\Code\Generator\MethodGenerator; |
20
|
|
|
use Laminas\Code\Generator\ParameterGenerator; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* This class represent a property in a bean (a property has a getter, a setter, etc...). |
24
|
|
|
*/ |
25
|
|
|
class ScalarBeanPropertyDescriptor extends AbstractBeanPropertyDescriptor |
26
|
|
|
{ |
27
|
|
|
/** |
28
|
|
|
* @var Column |
29
|
|
|
*/ |
30
|
|
|
private $column; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var Annotations |
34
|
|
|
*/ |
35
|
|
|
private $annotations; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var AnnotationParser |
39
|
|
|
*/ |
40
|
|
|
private $annotationParser; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* ScalarBeanPropertyDescriptor constructor. |
44
|
|
|
* @param Table $table |
45
|
|
|
* @param Column $column |
46
|
|
|
* @param NamingStrategyInterface $namingStrategy |
47
|
|
|
*/ |
48
|
|
|
public function __construct(Table $table, Column $column, NamingStrategyInterface $namingStrategy, AnnotationParser $annotationParser) |
49
|
|
|
{ |
50
|
|
|
parent::__construct($table, $namingStrategy); |
51
|
|
|
$this->table = $table; |
52
|
|
|
$this->column = $column; |
53
|
|
|
$this->annotationParser = $annotationParser; |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Returns the name of the class linked to this property or null if this is not a foreign key. |
58
|
|
|
* |
59
|
|
|
* @return null|string |
60
|
|
|
*/ |
61
|
|
|
public function getClassName(): ?string |
62
|
|
|
{ |
63
|
|
|
return null; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Returns the PHP type for the property (it can be a scalar like int, bool, or class names, like \DateTimeInterface, App\Bean\User....) |
68
|
|
|
* |
69
|
|
|
* @return string |
70
|
|
|
*/ |
71
|
|
|
public function getPhpType(): string |
72
|
|
|
{ |
73
|
|
|
$type = $this->column->getType(); |
74
|
|
|
return TDBMDaoGenerator::dbalTypeToPhpType($type); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Returns the Database type for the property |
79
|
|
|
* |
80
|
|
|
* @return Type |
81
|
|
|
*/ |
82
|
|
|
public function getDatabaseType(): Type |
83
|
|
|
{ |
84
|
|
|
return $this->column->getType(); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Returns true if the property is compulsory (and therefore should be fetched in the constructor). |
89
|
|
|
* |
90
|
|
|
* @return bool |
91
|
|
|
*/ |
92
|
|
|
public function isCompulsory(): bool |
93
|
|
|
{ |
94
|
|
|
return $this->column->getNotnull() && !$this->isAutoincrement() && $this->column->getDefault() === null && !$this->hasUuidAnnotation(); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
private function isAutoincrement(): bool |
98
|
|
|
{ |
99
|
|
|
return $this->column->getAutoincrement() || $this->getAutoincrementAnnotation() !== null; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
private function hasUuidAnnotation(): bool |
103
|
|
|
{ |
104
|
|
|
return $this->getUuidAnnotation() !== null; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
private function getUuidAnnotation(): ?Annotation\UUID |
108
|
|
|
{ |
109
|
|
|
/** @var Annotation\UUID|null $annotation */ |
110
|
|
|
$annotation = $this->getAnnotations()->findAnnotation(Annotation\UUID::class); |
111
|
|
|
return $annotation; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
private function getAutoincrementAnnotation(): ?Annotation\Autoincrement |
115
|
|
|
{ |
116
|
|
|
/** @var Annotation\Autoincrement|null $annotation */ |
117
|
|
|
$annotation = $this->getAnnotations()->findAnnotation(Annotation\Autoincrement::class); |
118
|
|
|
return $annotation; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
private function getAnnotations(): Annotations |
122
|
|
|
{ |
123
|
|
|
if ($this->annotations === null) { |
124
|
|
|
$this->annotations = $this->annotationParser->getColumnAnnotations($this->column, $this->table); |
125
|
|
|
} |
126
|
|
|
return $this->annotations; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Returns true if the property has a default value (or if the @UUID annotation is set for the column) |
131
|
|
|
* |
132
|
|
|
* @return bool |
133
|
|
|
*/ |
134
|
|
|
public function hasDefault(): bool |
135
|
|
|
{ |
136
|
|
|
// MariaDB 10.3 issue: it returns "NULL" (the string) instead of *null* |
137
|
|
|
return ($this->column->getDefault() !== null && $this->column->getDefault() !== 'NULL') || $this->hasUuidAnnotation(); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Returns the code that assigns a value to its default value. |
142
|
|
|
* |
143
|
|
|
* @return string |
144
|
|
|
*/ |
145
|
|
|
public function assignToDefaultCode(): string |
146
|
|
|
{ |
147
|
|
|
$str = '$this->%s(%s);'; |
148
|
|
|
|
149
|
|
|
$uuidAnnotation = $this->getUuidAnnotation(); |
150
|
|
|
if ($uuidAnnotation !== null) { |
151
|
|
|
$defaultCode = $this->getUuidCode($uuidAnnotation); |
152
|
|
|
} else { |
153
|
|
|
$default = $this->column->getDefault(); |
154
|
|
|
$type = $this->column->getType(); |
155
|
|
|
|
156
|
|
|
if (in_array($type->getName(), [ |
157
|
|
|
'datetime', |
158
|
|
|
'datetime_immutable', |
159
|
|
|
'datetimetz', |
160
|
|
|
'datetimetz_immutable', |
161
|
|
|
'date', |
162
|
|
|
'date_immutable', |
163
|
|
|
'time', |
164
|
|
|
'time_immutable', |
165
|
|
|
], true)) { |
166
|
|
|
if ($default !== null && in_array(strtoupper($default), ['CURRENT_TIMESTAMP' /* MySQL */, 'NOW()' /* PostgreSQL */, 'SYSDATE' /* Oracle */ , 'CURRENT_TIMESTAMP()' /* MariaDB 10.3 */], true)) { |
167
|
|
|
$defaultCode = 'new \DateTimeImmutable()'; |
168
|
|
|
} else { |
169
|
|
|
throw new TDBMException('Unable to set default value for date in "'.$this->table->getName().'.'.$this->column->getName().'". Database passed this default value: "'.$default.'"'); |
170
|
|
|
} |
171
|
|
|
} else { |
172
|
|
|
$defaultValue = $type->convertToPHPValue($this->column->getDefault(), new MySQL57Platform()); |
173
|
|
|
$defaultCode = var_export($defaultValue, true); |
174
|
|
|
} |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
return sprintf($str, $this->getSetterName(), $defaultCode); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
private function getUuidCode(Annotation\UUID $uuidAnnotation): string |
181
|
|
|
{ |
182
|
|
|
$comment = $uuidAnnotation->value; |
183
|
|
|
switch ($comment) { |
184
|
|
|
case '': |
185
|
|
|
case 'v1': |
186
|
|
|
return 'Uuid::uuid1()->toString()'; |
187
|
|
|
case 'v4': |
188
|
|
|
return 'Uuid::uuid4()->toString()'; |
189
|
|
|
default: |
190
|
|
|
throw new TDBMException('@UUID annotation accepts either "v1" or "v4" parameter. Unexpected parameter: ' . $comment); |
191
|
|
|
} |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Returns true if the property is the primary key. |
196
|
|
|
* |
197
|
|
|
* @return bool |
198
|
|
|
*/ |
199
|
|
|
public function isPrimaryKey(): bool |
200
|
|
|
{ |
201
|
|
|
$primaryKey = $this->table->getPrimaryKey(); |
202
|
|
|
if ($primaryKey === null) { |
203
|
|
|
return false; |
204
|
|
|
} |
205
|
|
|
return in_array($this->column->getName(), $primaryKey->getUnquotedColumns()); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* Returns the PHP code for getters and setters. |
210
|
|
|
* |
211
|
|
|
* @return (MethodGenerator|null)[] |
212
|
|
|
*/ |
213
|
|
|
public function getGetterSetterCode(): array |
214
|
|
|
{ |
215
|
|
|
$normalizedType = $this->getPhpType(); |
216
|
|
|
|
217
|
|
|
$columnGetterName = $this->getGetterName(); |
218
|
|
|
$columnSetterName = $this->getSetterName(); |
219
|
|
|
$variableName = ltrim($this->getSafeVariableName(), '$'); |
220
|
|
|
|
221
|
|
|
// 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). |
222
|
|
|
$isNullable = !$this->column->getNotnull() || $this->isAutoincrement(); |
223
|
|
|
|
224
|
|
|
$resourceTypeCheck = ''; |
225
|
|
|
if ($normalizedType === 'resource') { |
226
|
|
|
$checkNullable = ''; |
227
|
|
|
if ($isNullable) { |
228
|
|
|
$checkNullable = sprintf('$%s !== null && ', $variableName); |
229
|
|
|
} |
230
|
|
|
$resourceTypeCheck .= <<<EOF |
231
|
|
|
if (%s!\is_resource($%s)) { |
232
|
|
|
throw \TheCodingMachine\TDBM\TDBMInvalidArgumentException::badType('resource', $%s, __METHOD__); |
233
|
|
|
} |
234
|
|
|
EOF; |
235
|
|
|
$resourceTypeCheck = sprintf($resourceTypeCheck, $checkNullable, $variableName, $variableName); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
$types = [ $normalizedType ]; |
239
|
|
|
if ($isNullable) { |
240
|
|
|
$types[] = 'null'; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
$paramType = null; |
244
|
|
|
if ($this->isTypeHintable()) { |
245
|
|
|
$paramType = ($isNullable ? '?' : '').$normalizedType; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
$getter = new MethodGenerator($columnGetterName); |
249
|
|
|
$getterDocBlock = new DocBlockGenerator(sprintf('The getter for the "%s" column.', $this->column->getName())); |
250
|
|
|
$getterDocBlock->setTag(new ReturnTag($types))->setWordWrap(false); |
251
|
|
|
$getter->setDocBlock($getterDocBlock); |
252
|
|
|
$getter->setReturnType($paramType); |
253
|
|
|
|
254
|
|
|
$getter->setBody(sprintf( |
255
|
|
|
'return $this->get(%s, %s);', |
256
|
|
|
var_export($this->column->getName(), true), |
257
|
|
|
var_export($this->table->getName(), true) |
258
|
|
|
)); |
259
|
|
|
|
260
|
|
|
if ($this->isGetterProtected()) { |
261
|
|
|
$getter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
if (!$this->isReadOnly()) { |
265
|
|
|
$setter = new MethodGenerator($columnSetterName); |
266
|
|
|
$setterDocBlock = new DocBlockGenerator(sprintf('The setter for the "%s" column.', $this->column->getName())); |
267
|
|
|
$setterDocBlock->setTag(new ParamTag($variableName, $types))->setWordWrap(false); |
268
|
|
|
$setter->setDocBlock($setterDocBlock); |
269
|
|
|
|
270
|
|
|
$parameter = new ParameterGenerator($variableName, $paramType); |
271
|
|
|
$setter->setParameter($parameter); |
272
|
|
|
$setter->setReturnType('void'); |
273
|
|
|
|
274
|
|
|
$setter->setBody(sprintf( |
275
|
|
|
'%s |
276
|
|
|
$this->set(%s, $%s, %s);', |
277
|
|
|
$resourceTypeCheck, |
278
|
|
|
var_export($this->column->getName(), true), |
279
|
|
|
$variableName, |
280
|
|
|
var_export($this->table->getName(), true) |
281
|
|
|
)); |
282
|
|
|
|
283
|
|
|
if ($this->isSetterProtected()) { |
284
|
|
|
$setter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED); |
285
|
|
|
} |
286
|
|
|
} else { |
287
|
|
|
$setter = null; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
return [$getter, $setter]; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Returns the part of code useful when doing json serialization. |
295
|
|
|
* |
296
|
|
|
* @return string |
297
|
|
|
*/ |
298
|
|
|
public function getJsonSerializeCode(): string |
299
|
|
|
{ |
300
|
|
|
if ($this->findAnnotation(Annotation\JsonIgnore::class)) { |
301
|
|
|
return ''; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
if (!$this->canBeSerialized()) { |
305
|
|
|
return ''; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
// Do not export the property is the getter is protected. |
309
|
|
|
if ($this->isGetterProtected()) { |
310
|
|
|
return ''; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** @var Annotation\JsonKey|null $jsonKey */ |
314
|
|
|
$jsonKey = $this->findAnnotation(Annotation\JsonKey::class); |
315
|
|
|
$index = $jsonKey ? $jsonKey->key : $this->namingStrategy->getJsonProperty($this); |
316
|
|
|
$getter = $this->getGetterName(); |
317
|
|
|
switch ($this->getPhpType()) { |
318
|
|
|
case '\\DateTimeImmutable': |
319
|
|
|
/** @var Annotation\JsonFormat|null $jsonFormat */ |
320
|
|
|
$jsonFormat = $this->findAnnotation(Annotation\JsonFormat::class); |
321
|
|
|
$format = $jsonFormat ? $jsonFormat->datetime : 'c'; |
322
|
|
|
if ($this->column->getNotnull()) { |
323
|
|
|
return "\$array['$index'] = \$this->$getter()->format('$format');"; |
324
|
|
|
} else { |
325
|
|
|
return "\$array['$index'] = (\$date = \$this->$getter()) ? \$date->format('$format') : null;"; |
326
|
|
|
} |
327
|
|
|
// no break |
328
|
|
|
case 'int': |
329
|
|
|
case 'float': |
330
|
|
|
/** @var Annotation\JsonFormat|null $jsonFormat */ |
331
|
|
|
$jsonFormat = $this->findAnnotation(Annotation\JsonFormat::class); |
332
|
|
|
if ($jsonFormat) { |
333
|
|
|
$args = [$jsonFormat->decimals, $jsonFormat->point, $jsonFormat->separator]; |
334
|
|
|
for ($i = 2; $i >= 0; --$i) { |
335
|
|
|
if ($args[$i] === null) { |
336
|
|
|
unset($args[$i]); |
337
|
|
|
} else { |
338
|
|
|
break; |
339
|
|
|
} |
340
|
|
|
} |
341
|
|
|
$args = array_map(function ($v) { |
342
|
|
|
return var_export($v, true); |
343
|
|
|
}, $args); |
344
|
|
|
$args = empty($args) ? '' : ', ' . implode(', ', $args); |
345
|
|
|
$unit = $jsonFormat->unit ? ' . ' . var_export($jsonFormat->unit, true) : ''; |
346
|
|
|
if ($this->column->getNotnull()) { |
347
|
|
|
return "\$array['$index'] = number_format(\$this->$getter()$args)$unit;"; |
348
|
|
|
} else { |
349
|
|
|
return "\$array['$index'] = \$this->$getter() !== null ? number_format(\$this->$getter()$args)$unit : null;"; |
350
|
|
|
} |
351
|
|
|
} |
352
|
|
|
// no break |
353
|
|
|
default: |
354
|
|
|
return "\$array['$index'] = \$this->$getter();"; |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Returns the column name. |
360
|
|
|
* |
361
|
|
|
* @return string |
362
|
|
|
*/ |
363
|
|
|
public function getColumnName(): string |
364
|
|
|
{ |
365
|
|
|
return $this->column->getName(); |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* The code to past in the __clone method. |
370
|
|
|
* @return null|string |
371
|
|
|
*/ |
372
|
|
|
public function getCloneRule(): ?string |
373
|
|
|
{ |
374
|
|
|
$uuidAnnotation = $this->getUuidAnnotation(); |
375
|
|
|
if ($uuidAnnotation !== null && $this->isPrimaryKey()) { |
376
|
|
|
return sprintf("\$this->%s(%s);\n", $this->getSetterName(), $this->getUuidCode($uuidAnnotation)); |
377
|
|
|
} |
378
|
|
|
return null; |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* tells is this type is suitable for Json Serialization |
383
|
|
|
* |
384
|
|
|
* @return bool |
385
|
|
|
*/ |
386
|
|
|
public function canBeSerialized(): bool |
387
|
|
|
{ |
388
|
|
|
$type = $this->column->getType(); |
389
|
|
|
|
390
|
|
|
$unserialisableTypes = [ |
391
|
|
|
Type::BLOB, |
|
|
|
|
392
|
|
|
Type::BINARY |
|
|
|
|
393
|
|
|
]; |
394
|
|
|
|
395
|
|
|
return \in_array($type->getName(), $unserialisableTypes, true) === false; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Tells if this property is a type-hintable in PHP (resource isn't for example) |
400
|
|
|
* |
401
|
|
|
* @return bool |
402
|
|
|
*/ |
403
|
|
|
public function isTypeHintable(): bool |
404
|
|
|
{ |
405
|
|
|
$type = $this->getPhpType(); |
406
|
|
|
$invalidScalarTypes = [ |
407
|
|
|
'resource' |
408
|
|
|
]; |
409
|
|
|
|
410
|
|
|
return \in_array($type, $invalidScalarTypes, true) === false; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
private function isGetterProtected(): bool |
414
|
|
|
{ |
415
|
|
|
return $this->findAnnotation(Annotation\ProtectedGetter::class) !== null; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
private function isSetterProtected(): bool |
419
|
|
|
{ |
420
|
|
|
return $this->findAnnotation(Annotation\ProtectedSetter::class) !== null; |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
public function isReadOnly(): bool |
424
|
|
|
{ |
425
|
|
|
return $this->findAnnotation(Annotation\ReadOnlyColumn::class) !== null; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
private function findAnnotation(string $type): ?object |
429
|
|
|
{ |
430
|
|
|
return $this->getAnnotations()->findAnnotation($type); |
431
|
|
|
} |
432
|
|
|
} |
433
|
|
|
|
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.