1 | <?php |
||||
2 | |||||
3 | /** |
||||
4 | * This file is part of Cycle ORM package. |
||||
5 | * |
||||
6 | * For the full copyright and license information, please view the LICENSE |
||||
7 | * file that was distributed with this source code. |
||||
8 | */ |
||||
9 | |||||
10 | declare(strict_types=1); |
||||
11 | |||||
12 | namespace Cycle\Database\Schema; |
||||
13 | |||||
14 | use Cycle\Database\Driver\Jsoner; |
||||
15 | use Cycle\Database\Schema\Attribute\ColumnAttribute; |
||||
16 | use Cycle\Database\Schema\Traits\ColumnAttributesTrait; |
||||
17 | use Cycle\Database\ColumnInterface; |
||||
18 | use Cycle\Database\Driver\DriverInterface; |
||||
19 | use Cycle\Database\Exception\DefaultValueException; |
||||
20 | use Cycle\Database\Exception\SchemaException; |
||||
21 | use Cycle\Database\Injection\Fragment; |
||||
22 | use Cycle\Database\Injection\FragmentInterface; |
||||
23 | use Cycle\Database\Query\QueryParameters; |
||||
24 | use Cycle\Database\Schema\Traits\ElementTrait; |
||||
25 | |||||
26 | /** |
||||
27 | * Abstract column schema with read (see ColumnInterface) and write abilities. Must be implemented |
||||
28 | * by driver to support DBMS specific syntax and creation rules. |
||||
29 | * |
||||
30 | * Shortcuts for various column types: |
||||
31 | * |
||||
32 | * @method $this|AbstractColumn primary() |
||||
33 | * @method $this|AbstractColumn smallPrimary() |
||||
34 | * @method $this|AbstractColumn bigPrimary() |
||||
35 | * @method $this|AbstractColumn boolean() |
||||
36 | * @method $this|AbstractColumn integer() |
||||
37 | * @method $this|AbstractColumn tinyInteger() |
||||
38 | * @method $this|AbstractColumn smallInteger() |
||||
39 | * @method $this|AbstractColumn bigInteger() |
||||
40 | * @method $this|AbstractColumn text() |
||||
41 | * @method $this|AbstractColumn tinyText() |
||||
42 | * @method $this|AbstractColumn mediumText() |
||||
43 | * @method $this|AbstractColumn longText() |
||||
44 | * @method $this|AbstractColumn double() |
||||
45 | * @method $this|AbstractColumn float() |
||||
46 | * @method $this|AbstractColumn date() |
||||
47 | * @method $this|AbstractColumn time() |
||||
48 | * @method $this|AbstractColumn timestamp() |
||||
49 | * @method $this|AbstractColumn binary() |
||||
50 | * @method $this|AbstractColumn tinyBinary() |
||||
51 | * @method $this|AbstractColumn longBinary() |
||||
52 | * @method $this|AbstractColumn json() |
||||
53 | * @method $this|AbstractColumn uuid() |
||||
54 | */ |
||||
55 | abstract class AbstractColumn implements ColumnInterface, ElementInterface |
||||
56 | { |
||||
57 | use ColumnAttributesTrait; |
||||
58 | use ElementTrait; |
||||
59 | |||||
60 | /** |
||||
61 | * Default timestamp expression (driver specific). |
||||
62 | */ |
||||
63 | public const DATETIME_NOW = 'CURRENT_TIMESTAMP'; |
||||
64 | |||||
65 | /** |
||||
66 | * Value to be excluded from comparison. |
||||
67 | */ |
||||
68 | public const EXCLUDE_FROM_COMPARE = ['timezone', 'userType', 'attributes']; |
||||
69 | |||||
70 | /** |
||||
71 | * Normalization for time and dates. |
||||
72 | */ |
||||
73 | public const DATE_FORMAT = 'Y-m-d'; |
||||
74 | |||||
75 | public const TIME_FORMAT = 'H:i:s'; |
||||
76 | public const DATETIME_PRECISION = 6; |
||||
77 | |||||
78 | /** |
||||
79 | * Mapping between abstract type and internal database type with it's options. Multiple abstract |
||||
80 | * types can map into one database type, this implementation allows us to equalize two columns |
||||
81 | * if they have different abstract types but same database one. Must be declared by DBMS |
||||
82 | * specific implementation. |
||||
83 | * |
||||
84 | * Example: |
||||
85 | * integer => array('type' => 'int', 'size' => 1), |
||||
86 | * boolean => array('type' => 'tinyint', 'size' => 1) |
||||
87 | * |
||||
88 | * @internal |
||||
89 | */ |
||||
90 | protected array $mapping = [ |
||||
91 | //Primary sequences |
||||
92 | 'primary' => null, |
||||
93 | 'smallPrimary' => null, |
||||
94 | 'bigPrimary' => null, |
||||
95 | |||||
96 | //Enum type (mapped via method) |
||||
97 | 'enum' => null, |
||||
98 | |||||
99 | //Logical types |
||||
100 | 'boolean' => null, |
||||
101 | |||||
102 | //Integer types (size can always be changed with size method), longInteger has method alias |
||||
103 | //bigInteger |
||||
104 | 'integer' => null, |
||||
105 | 'tinyInteger' => null, |
||||
106 | 'smallInteger' => null, |
||||
107 | 'bigInteger' => null, |
||||
108 | |||||
109 | //String with specified length (mapped via method) |
||||
110 | 'string' => null, |
||||
111 | |||||
112 | //Generic types |
||||
113 | 'text' => null, |
||||
114 | 'tinyText' => null, |
||||
115 | 'longText' => null, |
||||
116 | |||||
117 | //Real types |
||||
118 | 'double' => null, |
||||
119 | 'float' => null, |
||||
120 | |||||
121 | //Decimal type (mapped via method) |
||||
122 | 'decimal' => null, |
||||
123 | |||||
124 | //Date and Time types |
||||
125 | 'datetime' => null, |
||||
126 | 'date' => null, |
||||
127 | 'time' => null, |
||||
128 | 'timestamp' => null, |
||||
129 | |||||
130 | //Binary types |
||||
131 | 'binary' => null, |
||||
132 | 'tinyBinary' => null, |
||||
133 | 'longBinary' => null, |
||||
134 | |||||
135 | //Additional types |
||||
136 | 'json' => null, |
||||
137 | ]; |
||||
138 | |||||
139 | /** |
||||
140 | * Reverse mapping is responsible for generating abstract type based on database type and it's |
||||
141 | * options. Multiple database types can be mapped into one abstract type. |
||||
142 | * |
||||
143 | * @internal |
||||
144 | */ |
||||
145 | protected array $reverseMapping = [ |
||||
146 | 'primary' => [], |
||||
147 | 'smallPrimary' => [], |
||||
148 | 'bigPrimary' => [], |
||||
149 | 'enum' => [], |
||||
150 | 'boolean' => [], |
||||
151 | 'integer' => [], |
||||
152 | 'tinyInteger' => [], |
||||
153 | 'smallInteger' => [], |
||||
154 | 'bigInteger' => [], |
||||
155 | 'string' => [], |
||||
156 | 'text' => [], |
||||
157 | 'tinyText' => [], |
||||
158 | 'longText' => [], |
||||
159 | 'double' => [], |
||||
160 | 'float' => [], |
||||
161 | 'decimal' => [], |
||||
162 | 'datetime' => [], |
||||
163 | 'date' => [], |
||||
164 | 'time' => [], |
||||
165 | 'timestamp' => [], |
||||
166 | 'binary' => [], |
||||
167 | 'tinyBinary' => [], |
||||
168 | 'longBinary' => [], |
||||
169 | 'json' => [], |
||||
170 | ]; |
||||
171 | |||||
172 | /** |
||||
173 | * User defined type. Only until actual mapping. |
||||
174 | */ |
||||
175 | protected ?string $userType = null; |
||||
176 | |||||
177 | /** |
||||
178 | * DBMS specific column type. |
||||
179 | */ |
||||
180 | protected string $type = ''; |
||||
181 | |||||
182 | protected ?\DateTimeZone $timezone = null; |
||||
183 | |||||
184 | /** |
||||
185 | * Indicates that column can contain null values. |
||||
186 | */ |
||||
187 | #[ColumnAttribute] |
||||
188 | protected bool $nullable = true; |
||||
189 | |||||
190 | /** |
||||
191 | * Default column value, may not be applied to some datatypes (for example to primary keys), |
||||
192 | * should follow type size and other options. |
||||
193 | */ |
||||
194 | #[ColumnAttribute] |
||||
195 | protected mixed $defaultValue = null; |
||||
196 | |||||
197 | /** |
||||
198 | * Column type size, can have different meanings for different datatypes. |
||||
199 | */ |
||||
200 | #[ColumnAttribute] |
||||
201 | protected int $size = 0; |
||||
202 | |||||
203 | /** |
||||
204 | * Precision of column, applied only for "decimal" type. |
||||
205 | */ |
||||
206 | #[ColumnAttribute(['decimal'])] |
||||
207 | protected int $precision = 0; |
||||
208 | |||||
209 | /** |
||||
210 | * Scale of column, applied only for "decimal" type. |
||||
211 | */ |
||||
212 | #[ColumnAttribute(['decimal'])] |
||||
213 | protected int $scale = 0; |
||||
214 | |||||
215 | /** |
||||
216 | * List of allowed enum values. |
||||
217 | */ |
||||
218 | protected array $enumValues = []; |
||||
219 | |||||
220 | /** |
||||
221 | * Abstract type aliases (for consistency). |
||||
222 | */ |
||||
223 | protected array $aliases = [ |
||||
224 | 'int' => 'integer', |
||||
225 | 'smallint' => 'smallInteger', |
||||
226 | 'bigint' => 'bigInteger', |
||||
227 | 'incremental' => 'primary', |
||||
228 | 'smallIncremental' => 'smallPrimary', |
||||
229 | 'bigIncremental' => 'bigPrimary', |
||||
230 | 'bool' => 'boolean', |
||||
231 | 'blob' => 'binary', |
||||
232 | 1950 | ]; |
|||
233 | |||||
234 | /** |
||||
235 | * Association list between abstract types and native PHP types. Every non listed type will be |
||||
236 | * converted into string. |
||||
237 | 1950 | * |
|||
238 | 1950 | * @internal |
|||
239 | */ |
||||
240 | private array $phpMapping = [ |
||||
241 | self::INT => ['primary', 'smallPrimary', 'bigPrimary', 'integer', 'tinyInteger', 'smallInteger', 'bigInteger'], |
||||
242 | self::BOOL => ['boolean'], |
||||
243 | self::FLOAT => ['double', 'float', 'decimal'], |
||||
244 | ]; |
||||
245 | 1750 | ||||
246 | /** |
||||
247 | 1750 | * @psalm-param non-empty-string $table |
|||
248 | * @psalm-param non-empty-string $name |
||||
249 | */ |
||||
250 | 856 | public function __construct( |
|||
251 | protected string $table, |
||||
252 | 856 | protected string $name, |
|||
253 | ?\DateTimeZone $timezone = null, |
||||
254 | ) { |
||||
255 | $this->timezone = $timezone ?? new \DateTimeZone(\date_default_timezone_get()); |
||||
256 | } |
||||
257 | |||||
258 | 24 | public function getSize(): int |
|||
259 | { |
||||
260 | 24 | return $this->size; |
|||
261 | 24 | } |
|||
262 | |||||
263 | 24 | public function getPrecision(): int |
|||
264 | 24 | { |
|||
265 | 24 | return $this->precision; |
|||
266 | } |
||||
267 | |||||
268 | public function getScale(): int |
||||
269 | 24 | { |
|||
270 | 8 | return $this->scale; |
|||
271 | } |
||||
272 | |||||
273 | 24 | public function isNullable(): bool |
|||
274 | 24 | { |
|||
275 | return $this->nullable; |
||||
276 | } |
||||
277 | 24 | ||||
278 | 8 | public function hasDefaultValue(): bool |
|||
279 | { |
||||
280 | return $this->defaultValue !== null; |
||||
281 | 24 | } |
|||
282 | |||||
283 | /** |
||||
284 | * @throws DefaultValueException |
||||
285 | 24 | */ |
|||
286 | 16 | public function getDefaultValue(): mixed |
|||
287 | 16 | { |
|||
288 | if (!$this->hasDefaultValue()) { |
||||
289 | return null; |
||||
290 | 24 | } |
|||
291 | |||||
292 | if ($this->defaultValue instanceof FragmentInterface) { |
||||
293 | 8 | //Defined as SQL piece |
|||
294 | return $this->defaultValue; |
||||
295 | 8 | } |
|||
296 | |||||
297 | if (\in_array($this->getAbstractType(), ['time', 'date', 'datetime', 'timestamp'])) { |
||||
298 | 852 | return $this->formatDatetime($this->getAbstractType(), $this->defaultValue); |
|||
299 | } |
||||
300 | 852 | ||||
301 | return match ($this->getType()) { |
||||
302 | 'int' => (int) $this->defaultValue, |
||||
303 | 852 | 'float' => (float) $this->defaultValue, |
|||
304 | 'bool' => \is_string($this->defaultValue) && \strtolower($this->defaultValue) === 'false' |
||||
305 | 852 | ? false : (bool) $this->defaultValue, |
|||
306 | default => (string) $this->defaultValue, |
||||
307 | }; |
||||
308 | 8 | } |
|||
309 | |||||
310 | 8 | /** |
|||
311 | * Get every associated column constraint names. |
||||
312 | */ |
||||
313 | 1798 | public function getConstraints(): array |
|||
314 | { |
||||
315 | 1798 | return []; |
|||
316 | } |
||||
317 | |||||
318 | /** |
||||
319 | * Get allowed enum values. |
||||
320 | */ |
||||
321 | 1798 | public function getEnumValues(): array |
|||
322 | { |
||||
323 | 1798 | return $this->enumValues; |
|||
324 | 1514 | } |
|||
325 | |||||
326 | public function getInternalType(): string |
||||
327 | 1074 | { |
|||
328 | return $this->type; |
||||
329 | 368 | } |
|||
330 | |||||
331 | /** |
||||
332 | 850 | * @psalm-return non-empty-string |
|||
333 | 682 | */ |
|||
334 | public function getType(): string |
||||
335 | { |
||||
336 | 698 | $schemaType = $this->getAbstractType(); |
|||
337 | 170 | foreach ($this->phpMapping as $phpType => $candidates) { |
|||
338 | 293 | if (\in_array($schemaType, $candidates, true)) { |
|||
339 | 205 | return $phpType; |
|||
340 | 205 | } |
|||
341 | 698 | } |
|||
342 | |||||
343 | return self::STRING; |
||||
344 | } |
||||
345 | |||||
346 | /** |
||||
347 | * Returns type defined by the user, only until schema sync. Attention, this value is only preserved during the |
||||
348 | 60 | * declaration process. Value will become null after the schema fetched from database. |
|||
349 | * |
||||
350 | 60 | * @internal |
|||
351 | */ |
||||
352 | public function getDeclaredType(): ?string |
||||
353 | { |
||||
354 | return $this->userType; |
||||
355 | } |
||||
356 | 852 | ||||
357 | /** |
||||
358 | 852 | * DBMS specific reverse mapping must map database specific type into limited set of abstract |
|||
359 | * types. |
||||
360 | */ |
||||
361 | 856 | public function getAbstractType(): string |
|||
362 | { |
||||
363 | 856 | foreach ($this->reverseMapping as $type => $candidates) { |
|||
364 | foreach ($candidates as $candidate) { |
||||
365 | if (\is_string($candidate)) { |
||||
366 | if (\strtolower($candidate) === \strtolower($this->type)) { |
||||
367 | return $type; |
||||
368 | } |
||||
369 | 842 | ||||
370 | continue; |
||||
371 | 842 | } |
|||
372 | 842 | ||||
373 | 842 | if (\strtolower($candidate['type']) !== \strtolower($this->type)) { |
|||
374 | 618 | continue; |
|||
375 | } |
||||
376 | |||||
377 | foreach ($candidate as $option => $required) { |
||||
378 | 754 | if ($option === 'type') { |
|||
379 | continue; |
||||
380 | } |
||||
381 | |||||
382 | if ($this->{$option} !== $required) { |
||||
383 | continue 2; |
||||
384 | } |
||||
385 | } |
||||
386 | |||||
387 | return $type; |
||||
388 | } |
||||
389 | } |
||||
390 | |||||
391 | return 'unknown'; |
||||
392 | } |
||||
393 | |||||
394 | /** |
||||
395 | * Give column new abstract type. DBMS specific implementation must map provided type into one |
||||
396 | 1922 | * of internal database values. |
|||
397 | * |
||||
398 | 1922 | * Attention, changing type of existed columns in some databases has a lot of restrictions like |
|||
399 | 1922 | * cross type conversions and etc. Try do not change column type without a reason. |
|||
400 | 1922 | * |
|||
401 | 1890 | * @psalm-param non-empty-string $abstract Abstract or virtual type declared in mapping. |
|||
402 | 1862 | * |
|||
403 | * @todo Support native database types (simply bypass abstractType)! |
||||
404 | */ |
||||
405 | 1838 | public function type(string $abstract): self |
|||
406 | { |
||||
407 | if (isset($this->aliases[$abstract])) { |
||||
408 | 1414 | //Make recursive |
|||
409 | 1360 | $abstract = $this->aliases[$abstract]; |
|||
410 | } |
||||
411 | |||||
412 | 1034 | if (!isset($this->mapping[$abstract])) { |
|||
413 | 1034 | $this->type = $abstract; |
|||
414 | 1034 | $this->userType = $abstract; |
|||
415 | |||||
416 | return $this; |
||||
417 | 1034 | } |
|||
418 | 876 | ||||
419 | // Originally specified type. |
||||
420 | $this->userType = $abstract; |
||||
421 | |||||
422 | 688 | // Resetting all values to default state. |
|||
423 | $this->size = $this->precision = $this->scale = 0; |
||||
424 | $this->enumValues = []; |
||||
425 | |||||
426 | 4 | // Abstract type points to DBMS specific type |
|||
427 | if (\is_string($this->mapping[$abstract])) { |
||||
428 | $this->type = $this->mapping[$abstract]; |
||||
429 | |||||
430 | return $this; |
||||
431 | } |
||||
432 | |||||
433 | // Configuring column properties based on abstractType preferences |
||||
434 | foreach ($this->mapping[$abstract] as $property => $value) { |
||||
435 | $this->{$property} = $value; |
||||
436 | } |
||||
437 | |||||
438 | return $this; |
||||
439 | } |
||||
440 | |||||
441 | /** |
||||
442 | 1932 | * Set column nullable/not nullable. |
|||
443 | */ |
||||
444 | 1932 | public function nullable(bool $nullable = true): self |
|||
445 | { |
||||
446 | $this->nullable = $nullable; |
||||
447 | |||||
448 | return $this; |
||||
449 | 1932 | } |
|||
450 | |||||
451 | /** |
||||
452 | 1932 | * Change column default value (can be forbidden for some column types). |
|||
453 | * Use {@see AbstractColumn::DATETIME_NOW} to use driver specific NOW() function. |
||||
454 | * Column with JSON type can be set to default value of array type. |
||||
455 | 1932 | */ |
|||
456 | 1932 | public function defaultValue(mixed $value): self |
|||
457 | { |
||||
458 | $this->defaultValue = match (true) { |
||||
459 | 1932 | $value === self::DATETIME_NOW => static::DATETIME_NOW, |
|||
460 | 1656 | static::isJson($this) !== false && \is_array($value) => Jsoner::toJson($value), |
|||
461 | default => $value, |
||||
462 | 1656 | }; |
|||
463 | |||||
464 | return $this; |
||||
465 | } |
||||
466 | 1442 | ||||
467 | 1442 | /** |
|||
468 | * Set column as enum type and specify set of allowed values. Most of drivers will emulate enums |
||||
469 | * using column constraints. |
||||
470 | 1442 | * |
|||
471 | * Examples: |
||||
472 | * $table->status->enum(['active', 'disabled']); |
||||
473 | * $table->status->enum('active', 'disabled'); |
||||
474 | * |
||||
475 | * @param array|string $values Enum values (array or comma separated). String values only. |
||||
476 | 698 | */ |
|||
477 | public function enum(string|array $values): self |
||||
478 | 698 | { |
|||
479 | $this->type('enum'); |
||||
480 | 698 | $this->enumValues = \array_map( |
|||
481 | 'strval', |
||||
482 | \is_array($values) ? $values : \func_get_args(), |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
483 | ); |
||||
484 | |||||
485 | return $this; |
||||
486 | } |
||||
487 | 954 | ||||
488 | /** |
||||
489 | * Set column type as string with limited size. Maximum allowed size is 255 bytes, use "text" |
||||
490 | 954 | * abstract types for longer strings. |
|||
491 | 578 | * |
|||
492 | * Strings are perfect type to store email addresses as it big enough to store valid address |
||||
493 | * and |
||||
494 | 954 | * can be covered with unique index. |
|||
495 | * |
||||
496 | 954 | * @link http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address |
|||
497 | * |
||||
498 | * @param int $size Max string length. |
||||
499 | * |
||||
500 | * @throws SchemaException |
||||
501 | */ |
||||
502 | public function string(int $size = 255): self |
||||
503 | { |
||||
504 | $this->type('string'); |
||||
505 | |||||
506 | $size < 0 && throw new SchemaException('Invalid string length value'); |
||||
507 | |||||
508 | $this->size = $size; |
||||
509 | 304 | ||||
510 | return $this; |
||||
511 | 304 | } |
|||
512 | 304 | ||||
513 | 304 | public function datetime(int $size = 0, mixed ...$attributes): self |
|||
514 | 304 | { |
|||
515 | $this->type('datetime'); |
||||
516 | $this->fillAttributes($attributes); |
||||
517 | 304 | ||||
518 | ($size < 0 || $size > static::DATETIME_PRECISION) && throw new SchemaException( |
||||
519 | \sprintf('Invalid %s precision value.', $this->getAbstractType()), |
||||
520 | ); |
||||
521 | $this->size = $size; |
||||
522 | |||||
523 | return $this; |
||||
524 | } |
||||
525 | |||||
526 | /** |
||||
527 | * Set column type as decimal with specific precision and scale. |
||||
528 | * |
||||
529 | * @throws SchemaException |
||||
530 | */ |
||||
531 | public function decimal(int $precision, int $scale = 0): self |
||||
532 | { |
||||
533 | $this->type('decimal'); |
||||
534 | 1008 | ||||
535 | empty($precision) && throw new SchemaException('Invalid precision value'); |
||||
536 | 1008 | ||||
537 | $this->precision = $precision; |
||||
538 | 1008 | $this->scale = $scale; |
|||
539 | |||||
540 | 1008 | return $this; |
|||
541 | } |
||||
542 | 1008 | ||||
543 | public function sqlStatement(DriverInterface $driver): string |
||||
544 | { |
||||
545 | $statement = [$driver->identifier($this->name), $this->type]; |
||||
0 ignored issues
–
show
The method
identifier() does not exist on Cycle\Database\Driver\DriverInterface . It seems like you code against a sub-type of Cycle\Database\Driver\DriverInterface such as Cycle\Database\Driver\Driver .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
546 | |||||
547 | if (static::isEnum($this)) { |
||||
548 | //Enum specific column options |
||||
549 | if (!empty($enumDefinition = $this->quoteEnum($driver))) { |
||||
550 | 64 | $statement[] = $enumDefinition; |
|||
551 | } |
||||
552 | 64 | } elseif (!empty($this->precision)) { |
|||
553 | $statement[] = "({$this->precision}, {$this->scale})"; |
||||
554 | 64 | } elseif (!empty($this->size)) { |
|||
555 | $statement[] = "({$this->size})"; |
||||
556 | 56 | } |
|||
557 | 56 | ||||
558 | $statement[] = $this->nullable ? 'NULL' : 'NOT NULL'; |
||||
559 | 56 | ||||
560 | if ($this->defaultValue !== null) { |
||||
561 | $statement[] = "DEFAULT {$this->quoteDefault($driver)}"; |
||||
562 | 1464 | } |
|||
563 | |||||
564 | 1464 | return \implode(' ', $statement); |
|||
565 | } |
||||
566 | 1464 | ||||
567 | public function compare(self $initial): bool |
||||
568 | 458 | { |
|||
569 | 458 | $normalized = clone $initial; |
|||
570 | |||||
571 | 1440 | // soft compare, todo: improve |
|||
572 | 42 | if ($this == $normalized) { |
|||
573 | 1410 | return true; |
|||
574 | 878 | } |
|||
575 | |||||
576 | $columnVars = \get_object_vars($this); |
||||
577 | 1464 | $dbColumnVars = \get_object_vars($normalized); |
|||
578 | |||||
579 | 1464 | $difference = []; |
|||
580 | 634 | foreach ($columnVars as $name => $value) { |
|||
581 | if (\in_array($name, static::EXCLUDE_FROM_COMPARE, true)) { |
||||
582 | continue; |
||||
583 | 1464 | } |
|||
584 | |||||
585 | if ($name === 'type') { |
||||
586 | 1894 | // user defined type |
|||
587 | if (!isset($this->mapping[$this->type]) && $this->type === $this->userType) { |
||||
588 | 1894 | continue; |
|||
589 | } |
||||
590 | } |
||||
591 | 1894 | ||||
592 | 1838 | if ($name === 'defaultValue') { |
|||
593 | $defaultValue = $this->getDefaultValue() instanceof FragmentInterface |
||||
594 | ? $this->getDefaultValue()->__toString() |
||||
0 ignored issues
–
show
The method
__toString() does not exist on Cycle\Database\Injection\FragmentInterface . It seems like you code against a sub-type of said class. However, the method does not exist in Cycle\Database\Query\QueryInterface or Spiral\Database\Injection\FragmentInterface or Spiral\Database\Query\QueryInterface or Cycle\Database\Query\ReturningInterface . Are you sure you never get one of those?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
595 | 1598 | : $this->getDefaultValue(); |
|||
596 | 1598 | $initialDefaultValue = $initial->getDefaultValue() instanceof FragmentInterface |
|||
597 | ? $initial->getDefaultValue()->__toString() |
||||
598 | 1598 | : $initial->getDefaultValue(); |
|||
599 | 1598 | ||||
600 | 1598 | //Default values has to compared using type-casted value |
|||
601 | 1598 | if ($defaultValue != $initialDefaultValue) { |
|||
602 | $difference[] = $name; |
||||
603 | } elseif ( |
||||
604 | 1598 | $defaultValue !== $initialDefaultValue |
|||
605 | && (!\is_object($this->getDefaultValue()) && !\is_object($initial->getDefaultValue())) |
||||
606 | 1598 | ) { |
|||
607 | 380 | $difference[] = $name; |
|||
608 | } |
||||
609 | 1554 | ||||
610 | 1554 | continue; |
|||
611 | } |
||||
612 | 16 | ||||
613 | if ($value !== $dbColumnVars[$name]) { |
||||
614 | $difference[] = $name; |
||||
615 | 1598 | } |
|||
616 | } |
||||
617 | |||||
618 | 1598 | return empty($difference); |
|||
619 | 154 | } |
|||
620 | |||||
621 | public function isReadonlySchema(): bool |
||||
622 | { |
||||
623 | 1598 | return $this->getAttributes()['readonlySchema'] ?? false; |
|||
624 | } |
||||
625 | |||||
626 | /** |
||||
627 | * Get column comment. |
||||
628 | * An empty string will be returned if the feature is not supported by the driver. |
||||
629 | 152 | */ |
|||
630 | public function getComment(): string |
||||
631 | 152 | { |
|||
632 | 152 | return ''; |
|||
633 | 152 | } |
|||
634 | |||||
635 | /** |
||||
636 | 152 | * Shortcut for AbstractColumn->type() method. |
|||
637 | * |
||||
638 | * @psalm-param non-empty-string $name |
||||
639 | */ |
||||
640 | public function __call(string $name, array $arguments = []): self |
||||
641 | { |
||||
642 | 846 | if (isset($this->aliases[$name]) || isset($this->mapping[$name])) { |
|||
643 | $this->type($name); |
||||
644 | 846 | } |
|||
645 | 846 | ||||
646 | // The type must be set before the attributes are filled. |
||||
647 | !empty($this->type) or throw new SchemaException('Undefined abstract/virtual type'); |
||||
648 | |||||
649 | 846 | if (\count($arguments) === 1 && \key($arguments) === 0) { |
|||
650 | 578 | if (\array_key_exists($name, $this->getAttributesMap())) { |
|||
651 | 578 | $this->fillAttributes([$name => $arguments[0]]); |
|||
652 | 578 | return $this; |
|||
653 | } |
||||
654 | } |
||||
655 | |||||
656 | $this->fillAttributes($arguments); |
||||
657 | 798 | ||||
658 | 410 | return $this; |
|||
659 | 293 | } |
|||
660 | 83 | ||||
661 | 798 | public function __toString(): string |
|||
662 | { |
||||
663 | return $this->table . '.' . $this->getName(); |
||||
664 | } |
||||
665 | |||||
666 | /** |
||||
667 | * Simplified way to dump information. |
||||
668 | */ |
||||
669 | public function __debugInfo(): array |
||||
670 | { |
||||
671 | $column = [ |
||||
672 | 682 | 'name' => $this->name, |
|||
673 | 'type' => [ |
||||
674 | 'database' => $this->type, |
||||
675 | 'schema' => $this->getAbstractType(), |
||||
676 | 682 | 'php' => $this->getType(), |
|||
677 | ], |
||||
678 | 578 | ]; |
|||
679 | |||||
680 | if (!empty($this->size)) { |
||||
681 | 634 | $column['size'] = $this->size; |
|||
682 | 8 | } |
|||
683 | |||||
684 | 634 | if ($this->nullable) { |
|||
685 | $column['nullable'] = true; |
||||
686 | 32 | } |
|||
687 | 32 | ||||
688 | if ($this->defaultValue !== null) { |
||||
689 | 632 | $column['defaultValue'] = $this->getDefaultValue(); |
|||
690 | } |
||||
691 | |||||
692 | if (static::isEnum($this)) { |
||||
693 | 634 | $column['enumValues'] = $this->enumValues; |
|||
694 | 32 | } |
|||
695 | 301 | ||||
696 | if ($this->getAbstractType() === 'decimal') { |
||||
697 | 634 | $column['precision'] = $this->precision; |
|||
698 | $column['scale'] = $this->scale; |
||||
699 | } |
||||
700 | |||||
701 | if ($this->attributes !== []) { |
||||
702 | $column['attributes'] = $this->attributes; |
||||
703 | } |
||||
704 | |||||
705 | return $column; |
||||
706 | } |
||||
707 | |||||
708 | protected static function isEnum(self $column): bool |
||||
709 | { |
||||
710 | return $column->getAbstractType() === 'enum'; |
||||
711 | } |
||||
712 | |||||
713 | /** |
||||
714 | * Checks if the column is JSON or no. |
||||
715 | * |
||||
716 | * Returns null if it's impossible to explicitly define the JSON type. |
||||
717 | */ |
||||
718 | protected static function isJson(self $column): ?bool |
||||
719 | { |
||||
720 | return $column->getAbstractType() === 'json'; |
||||
721 | } |
||||
722 | |||||
723 | /** |
||||
724 | * Get database specific enum type definition options. |
||||
725 | */ |
||||
726 | protected function quoteEnum(DriverInterface $driver): string |
||||
727 | { |
||||
728 | $enumValues = []; |
||||
729 | foreach ($this->enumValues as $value) { |
||||
730 | $enumValues[] = $driver->quote($value); |
||||
731 | } |
||||
732 | |||||
733 | return !empty($enumValues) ? '(' . \implode(', ', $enumValues) . ')' : ''; |
||||
734 | } |
||||
735 | |||||
736 | /** |
||||
737 | * Must return driver specific default value. |
||||
738 | */ |
||||
739 | protected function quoteDefault(DriverInterface $driver): string |
||||
740 | { |
||||
741 | $defaultValue = $this->getDefaultValue(); |
||||
742 | if ($defaultValue === null) { |
||||
743 | return 'NULL'; |
||||
744 | } |
||||
745 | |||||
746 | if ($defaultValue instanceof FragmentInterface) { |
||||
747 | return $driver->getQueryCompiler()->compile( |
||||
748 | new QueryParameters(), |
||||
749 | '', |
||||
750 | $defaultValue, |
||||
751 | ); |
||||
752 | } |
||||
753 | |||||
754 | return match ($this->getType()) { |
||||
755 | 'bool' => $defaultValue ? 'TRUE' : 'FALSE', |
||||
756 | 'float' => \sprintf('%F', $defaultValue), |
||||
757 | 'int' => (string) $defaultValue, |
||||
758 | default => $driver->quote($defaultValue), |
||||
759 | }; |
||||
760 | } |
||||
761 | |||||
762 | /** |
||||
763 | * Ensure that datetime fields are correctly formatted. |
||||
764 | * |
||||
765 | * @psalm-param non-empty-string $type |
||||
766 | * |
||||
767 | * @throws DefaultValueException |
||||
768 | */ |
||||
769 | protected function formatDatetime( |
||||
770 | string $type, |
||||
771 | string|int|\DateTimeInterface $value, |
||||
772 | ): \DateTimeInterface|FragmentInterface|string { |
||||
773 | if ($value === static::DATETIME_NOW) { |
||||
774 | //Dynamic default value |
||||
775 | return new Fragment($value); |
||||
0 ignored issues
–
show
It seems like
$value can also be of type DateTimeInterface ; however, parameter $fragment of Cycle\Database\Injection\Fragment::__construct() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
776 | } |
||||
777 | |||||
778 | if ($value instanceof \DateTimeInterface) { |
||||
779 | $datetime = clone $value; |
||||
780 | } else { |
||||
781 | if (\is_numeric($value)) { |
||||
782 | //Presumably timestamp |
||||
783 | $datetime = new \DateTimeImmutable('now', $this->timezone); |
||||
784 | $datetime = $datetime->setTimestamp($value); |
||||
0 ignored issues
–
show
It seems like
$value can also be of type string ; however, parameter $timestamp of DateTimeImmutable::setTimestamp() does only seem to accept integer , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
785 | } else { |
||||
786 | $datetime = new \DateTimeImmutable($value, $this->timezone); |
||||
787 | } |
||||
788 | } |
||||
789 | |||||
790 | return match ($type) { |
||||
791 | 'datetime', 'timestamp' => $datetime, |
||||
792 | 'time' => $datetime->format(static::TIME_FORMAT), |
||||
793 | 'date' => $datetime->format(static::DATE_FORMAT), |
||||
794 | default => $value, |
||||
795 | }; |
||||
796 | } |
||||
797 | } |
||||
798 |