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\DriverInterface; |
||||
15 | use Cycle\Database\Driver\HandlerInterface; |
||||
16 | use Cycle\Database\Exception\DriverException; |
||||
17 | use Cycle\Database\Exception\HandlerException; |
||||
18 | use Cycle\Database\Exception\SchemaException; |
||||
19 | use Cycle\Database\TableInterface; |
||||
20 | |||||
21 | /** |
||||
22 | * AbstractTable class used to describe and manage state of specified table. It provides ability to |
||||
23 | * get table introspection, update table schema and automatically generate set of diff operations. |
||||
24 | * |
||||
25 | * Most of table operation like column, index or foreign key creation/altering will be applied when |
||||
26 | * save() method will be called. |
||||
27 | * |
||||
28 | * Column configuration shortcuts: |
||||
29 | * |
||||
30 | * @method AbstractColumn primary($column) |
||||
31 | * @method AbstractColumn bigPrimary($column) |
||||
32 | * @method AbstractColumn enum($column, array $values) |
||||
33 | * @method AbstractColumn string($column, $length = 255) |
||||
34 | * @method AbstractColumn decimal($column, $precision, $scale) |
||||
35 | * @method AbstractColumn boolean($column) |
||||
36 | * @method AbstractColumn integer($column) |
||||
37 | * @method AbstractColumn tinyInteger($column) |
||||
38 | * @method AbstractColumn smallInteger($column) |
||||
39 | * @method AbstractColumn bigInteger($column) |
||||
40 | * @method AbstractColumn text($column) |
||||
41 | * @method AbstractColumn tinyText($column) |
||||
42 | * @method AbstractColumn mediumText($column) |
||||
43 | * @method AbstractColumn longText($column) |
||||
44 | * @method AbstractColumn json($column) |
||||
45 | * @method AbstractColumn double($column) |
||||
46 | * @method AbstractColumn float($column) |
||||
47 | * @method AbstractColumn datetime($column, $size = 0) |
||||
48 | * @method AbstractColumn date($column) |
||||
49 | * @method AbstractColumn time($column) |
||||
50 | * @method AbstractColumn timestamp($column) |
||||
51 | * @method AbstractColumn binary($column) |
||||
52 | * @method AbstractColumn tinyBinary($column) |
||||
53 | * @method AbstractColumn longBinary($column) |
||||
54 | * @method AbstractColumn uuid($column) |
||||
55 | */ |
||||
56 | abstract class AbstractTable implements TableInterface, ElementInterface |
||||
57 | { |
||||
58 | /** |
||||
59 | * Table states. |
||||
60 | */ |
||||
61 | public const STATUS_NEW = 0; |
||||
62 | |||||
63 | public const STATUS_EXISTS = 1; |
||||
64 | public const STATUS_DECLARED_DROPPED = 2; |
||||
65 | |||||
66 | /** |
||||
67 | * Initial table state. |
||||
68 | * |
||||
69 | * @internal |
||||
70 | */ |
||||
71 | protected State $initial; |
||||
72 | |||||
73 | /** |
||||
74 | * Currently defined table state. |
||||
75 | * |
||||
76 | * @internal |
||||
77 | */ |
||||
78 | protected State $current; |
||||
79 | |||||
80 | /** |
||||
81 | * Indication that table is exists and current schema is fetched from database. |
||||
82 | */ |
||||
83 | private int $status = self::STATUS_NEW; |
||||
84 | |||||
85 | /** |
||||
86 | * @param DriverInterface $driver Parent driver. |
||||
87 | * |
||||
88 | 1974 | * @psalm-param non-empty-string $name Table name, must include table prefix. |
|||
89 | * |
||||
90 | * @param string $prefix Database specific table prefix. Required for table renames. |
||||
91 | */ |
||||
92 | public function __construct( |
||||
93 | protected DriverInterface $driver, |
||||
94 | 1974 | string $name, |
|||
95 | 1974 | private string $prefix, |
|||
96 | 1974 | ) { |
|||
97 | //Initializing states |
||||
98 | 1974 | $prefixedName = $this->prefixTableName($name); |
|||
99 | 1928 | $this->initial = new State($prefixedName); |
|||
100 | $this->current = new State($prefixedName); |
||||
101 | |||||
102 | 1974 | if ($this->driver->getSchemaHandler()->hasTable($this->getFullName())) { |
|||
103 | $this->status = self::STATUS_EXISTS; |
||||
104 | 1928 | } |
|||
105 | |||||
106 | if ($this->exists()) { |
||||
107 | 1974 | //Initiating table schema |
|||
108 | 1974 | $this->initSchema($this->initial); |
|||
109 | } |
||||
110 | |||||
111 | $this->setState($this->initial); |
||||
112 | } |
||||
113 | |||||
114 | /** |
||||
115 | 14 | * Sanitize column expression for index name |
|||
116 | * |
||||
117 | 14 | * @psalm-param non-empty-string $column |
|||
118 | * |
||||
119 | * @psalm-return non-empty-string |
||||
120 | */ |
||||
121 | public static function sanitizeColumnExpression(string $column): string |
||||
122 | { |
||||
123 | return \preg_replace(['/\(/', '/\)/', '/ /'], '__', \strtolower($column)); |
||||
124 | } |
||||
125 | |||||
126 | /** |
||||
127 | * Get instance of associated driver. |
||||
128 | */ |
||||
129 | public function getDriver(): DriverInterface |
||||
130 | { |
||||
131 | return $this->driver; |
||||
132 | 1948 | } |
|||
133 | |||||
134 | /** |
||||
135 | 1948 | * Return database specific table prefix. |
|||
136 | 1948 | */ |
|||
137 | public function getPrefix(): string |
||||
138 | { |
||||
139 | return $this->prefix; |
||||
140 | } |
||||
141 | |||||
142 | public function getComparator(): ComparatorInterface |
||||
143 | { |
||||
144 | return new Comparator($this->initial, $this->current); |
||||
145 | } |
||||
146 | |||||
147 | public function exists(): bool |
||||
148 | 1950 | { |
|||
149 | // Declared as dropped != actually dropped |
||||
150 | 1950 | return $this->status === self::STATUS_EXISTS || $this->status === self::STATUS_DECLARED_DROPPED; |
|||
151 | 1950 | } |
|||
152 | 1950 | ||||
153 | /** |
||||
154 | 8 | * Table status (see codes above). |
|||
155 | */ |
||||
156 | public function getStatus(): int |
||||
157 | 8 | { |
|||
158 | 8 | return $this->status; |
|||
159 | 8 | } |
|||
160 | 8 | ||||
161 | 8 | /** |
|||
162 | 8 | * Sets table name. Use this function in combination with save to rename table. |
|||
163 | 8 | * |
|||
164 | * @psalm-param non-empty-string $name |
||||
165 | * |
||||
166 | * @psalm-return non-empty-string Prefixed table name. |
||||
167 | */ |
||||
168 | public function setName(string $name): string |
||||
169 | { |
||||
170 | 112 | $this->current->setName($this->prefixTableName($name)); |
|||
171 | |||||
172 | 112 | return $this->getFullName(); |
|||
173 | } |
||||
174 | |||||
175 | /** |
||||
176 | * @psalm-return non-empty-string |
||||
177 | */ |
||||
178 | 240 | public function getName(): string |
|||
179 | { |
||||
180 | 240 | return $this->getFullName(); |
|||
181 | } |
||||
182 | |||||
183 | 1950 | /** |
|||
184 | * @psalm-return non-empty-string |
||||
185 | 1950 | */ |
|||
186 | public function getFullName(): string |
||||
187 | { |
||||
188 | 1974 | return $this->current->getName(); |
|||
189 | } |
||||
190 | |||||
191 | 1974 | /** |
|||
192 | * Table name before rename. |
||||
193 | * |
||||
194 | * @psalm-return non-empty-string |
||||
195 | */ |
||||
196 | public function getInitialName(): string |
||||
197 | 112 | { |
|||
198 | return $this->initial->getName(); |
||||
199 | 112 | } |
|||
200 | |||||
201 | /** |
||||
202 | * Declare table as dropped, you have to sync table using "save" method in order to apply this |
||||
203 | * change. |
||||
204 | * |
||||
205 | * Attention, method will flush declared FKs to ensure that table express no dependecies. |
||||
206 | */ |
||||
207 | public function declareDropped(): void |
||||
208 | 132 | { |
|||
209 | $this->status === self::STATUS_NEW and throw new SchemaException('Unable to drop non existed table'); |
||||
210 | 132 | ||||
211 | //Declaring as dropped |
||||
212 | 132 | $this->status = self::STATUS_DECLARED_DROPPED; |
|||
213 | } |
||||
214 | |||||
215 | /** |
||||
216 | * Set table primary keys. Operation can only be applied for newly created tables. Now every |
||||
217 | * database might support compound indexes. |
||||
218 | 18 | */ |
|||
219 | public function setPrimaryKeys(array $columns): self |
||||
220 | 18 | { |
|||
221 | //Originally i were forcing an exception when primary key were changed, now we should |
||||
222 | //force it when table will be synced |
||||
223 | |||||
224 | //Updating primary keys in current state |
||||
225 | $this->current->setPrimaryKeys($columns); |
||||
226 | 1974 | ||||
227 | return $this; |
||||
228 | 1974 | } |
|||
229 | |||||
230 | public function getPrimaryKeys(): array |
||||
231 | { |
||||
232 | return $this->current->getPrimaryKeys(); |
||||
233 | } |
||||
234 | |||||
235 | public function hasColumn(string $name): bool |
||||
236 | 1930 | { |
|||
237 | return $this->current->hasColumn($name); |
||||
238 | 1930 | } |
|||
239 | |||||
240 | /** |
||||
241 | * @return AbstractColumn[] |
||||
242 | */ |
||||
243 | public function getColumns(): array |
||||
244 | { |
||||
245 | return $this->current->getColumns(); |
||||
246 | } |
||||
247 | 1938 | ||||
248 | public function hasIndex(array $columns = []): bool |
||||
249 | 1938 | { |
|||
250 | return $this->current->hasIndex($columns); |
||||
251 | } |
||||
252 | 1930 | ||||
253 | 1930 | /** |
|||
254 | * @return AbstractIndex[] |
||||
255 | */ |
||||
256 | public function getIndexes(): array |
||||
257 | { |
||||
258 | return $this->current->getIndexes(); |
||||
259 | 16 | } |
|||
260 | |||||
261 | public function hasForeignKey(array $columns): bool |
||||
262 | { |
||||
263 | return $this->current->hasForeignKey($columns); |
||||
264 | } |
||||
265 | 16 | ||||
266 | /** |
||||
267 | 16 | * @return AbstractForeignKey[] |
|||
268 | */ |
||||
269 | public function getForeignKeys(): array |
||||
270 | 1952 | { |
|||
271 | return $this->current->getForeignKeys(); |
||||
272 | 1952 | } |
|||
273 | |||||
274 | public function getDependencies(): array |
||||
275 | 568 | { |
|||
276 | $tables = []; |
||||
277 | 568 | foreach ($this->current->getForeignKeys() as $foreignKey) { |
|||
278 | $tables[] = $foreignKey->getForeignTable(); |
||||
279 | } |
||||
280 | |||||
281 | return $tables; |
||||
282 | } |
||||
283 | 1956 | ||||
284 | /** |
||||
285 | 1956 | * Get/create instance of AbstractColumn associated with current table. |
|||
286 | * |
||||
287 | * Attention, renamed column will be available by it's old name until being synced! |
||||
288 | 458 | * |
|||
289 | * @psalm-param non-empty-string $name |
||||
290 | 458 | * |
|||
291 | * Examples: |
||||
292 | * $table->column('name')->string(); |
||||
293 | */ |
||||
294 | public function column(string $name): AbstractColumn |
||||
0 ignored issues
–
show
|
|||||
295 | { |
||||
296 | 1952 | if ($this->current->hasColumn($name)) { |
|||
297 | //Column already exists |
||||
298 | 1952 | return $this->current->findColumn($name); |
|||
0 ignored issues
–
show
|
|||||
299 | } |
||||
300 | |||||
301 | 224 | if ($this->initial->hasColumn($name)) { |
|||
302 | //Fetch from initial state (this code is required to ensure column states after schema |
||||
303 | 224 | //flushing) |
|||
304 | $column = clone $this->initial->findColumn($name); |
||||
305 | } else { |
||||
306 | $column = $this->createColumn($name); |
||||
307 | } |
||||
308 | |||||
309 | 1954 | $this->current->registerColumn($column); |
|||
310 | |||||
311 | 1954 | return $column; |
|||
312 | } |
||||
313 | |||||
314 | 120 | /** |
|||
315 | * Get/create instance of AbstractIndex associated with current table based on list of forming |
||||
316 | 120 | * column names. |
|||
317 | 120 | * |
|||
318 | 104 | * Example: |
|||
319 | * $table->index(['key']); |
||||
320 | * $table->index(['key', 'key2']); |
||||
321 | 120 | * |
|||
322 | * @param array $columns List of index columns |
||||
323 | * |
||||
324 | * @throws SchemaException |
||||
325 | * @throws DriverException |
||||
326 | */ |
||||
327 | public function index(array $columns): AbstractIndex |
||||
328 | { |
||||
329 | $original = $columns; |
||||
330 | $normalized = []; |
||||
331 | $sort = []; |
||||
332 | |||||
333 | foreach ($columns as $expression) { |
||||
334 | 1950 | [$column, $order] = AbstractIndex::parseColumn($expression); |
|||
335 | |||||
336 | 1950 | // If expression like 'column DESC' was passed, we cast it to 'column' => 'DESC' |
|||
337 | if ($order !== null) { |
||||
338 | 988 | $this->isIndexColumnSortingSupported() or throw new DriverException(\sprintf( |
|||
339 | 'Failed to create index with `%s` on `%s`, column sorting is not supported', |
||||
340 | $expression, |
||||
341 | 1948 | $this->getFullName(), |
|||
342 | )); |
||||
343 | |||||
344 | $sort[$column] = $order; |
||||
345 | } |
||||
346 | 1948 | ||||
347 | $normalized[] = $column; |
||||
348 | } |
||||
349 | 1948 | $columns = $normalized; |
|||
350 | |||||
351 | 1948 | foreach ($columns as $column) { |
|||
352 | $this->hasColumn($column) or throw new SchemaException( |
||||
353 | "Undefined column '{$column}' in '{$this->getFullName()}'", |
||||
354 | ); |
||||
355 | } |
||||
356 | |||||
357 | if ($this->hasIndex($original)) { |
||||
358 | return $this->current->findIndex($original); |
||||
0 ignored issues
–
show
|
|||||
359 | } |
||||
360 | |||||
361 | if ($this->initial->hasIndex($original)) { |
||||
362 | //Let's ensure that index name is always stays synced (not regenerated) |
||||
363 | $name = $this->initial->findIndex($original)->getName(); |
||||
364 | } else { |
||||
365 | $name = $this->createIdentifier('index', $original); |
||||
366 | } |
||||
367 | 458 | ||||
368 | $index = $this->createIndex($name)->columns($columns)->sort($sort); |
||||
369 | 458 | ||||
370 | 458 | //Adding to current schema |
|||
371 | 458 | $this->current->registerIndex($index); |
|||
372 | |||||
373 | 458 | return $index; |
|||
374 | 458 | } |
|||
375 | |||||
376 | /** |
||||
377 | 458 | * Get/create instance of AbstractReference associated with current table based on local column |
|||
378 | 16 | * name. |
|||
379 | * |
||||
380 | * @throws SchemaException |
||||
381 | */ |
||||
382 | public function foreignKey(array $columns, bool $indexCreate = true): AbstractForeignKey |
||||
383 | { |
||||
384 | 16 | foreach ($columns as $column) { |
|||
385 | $this->hasColumn($column) or throw new SchemaException( |
||||
386 | "Undefined column '{$column}' in '{$this->getFullName()}'", |
||||
387 | 458 | ); |
|||
388 | } |
||||
389 | 458 | ||||
390 | if ($this->hasForeignKey($columns)) { |
||||
391 | 458 | return $this->current->findForeignKey($columns); |
|||
0 ignored issues
–
show
|
|||||
392 | 458 | } |
|||
393 | |||||
394 | if ($this->initial->hasForeignKey($columns)) { |
||||
395 | //Let's ensure that FK name is always stays synced (not regenerated) |
||||
396 | $name = $this->initial->findForeignKey($columns)->getName(); |
||||
397 | 458 | } else { |
|||
398 | 24 | $name = $this->createIdentifier('foreign', $columns); |
|||
399 | } |
||||
400 | |||||
401 | 458 | $foreign = $this->createForeign($name)->columns($columns); |
|||
402 | |||||
403 | //Adding to current schema |
||||
404 | $this->current->registerForeignKey($foreign); |
||||
405 | 458 | ||||
406 | //Let's ensure index existence to performance and compatibility reasons |
||||
407 | $indexCreate ? $this->index($columns) : $foreign->setIndex(false); |
||||
408 | 458 | ||||
409 | return $foreign; |
||||
410 | } |
||||
411 | 458 | ||||
412 | /** |
||||
413 | 458 | * Rename column (only if column exists). |
|||
414 | * |
||||
415 | * @psalm-param non-empty-string $column |
||||
416 | * @psalm-param non-empty-string $name New column name. |
||||
417 | * |
||||
418 | * @throws SchemaException |
||||
419 | */ |
||||
420 | public function renameColumn(string $column, string $name): self |
||||
421 | { |
||||
422 | 224 | $this->hasColumn($column) or throw new SchemaException( |
|||
423 | "Undefined column '{$column}' in '{$this->getFullName()}'", |
||||
424 | 224 | ); |
|||
425 | 224 | ||||
426 | //Rename operation is simple about declaring new name |
||||
427 | $this->column($column)->setName($name); |
||||
428 | |||||
429 | return $this; |
||||
430 | 224 | } |
|||
431 | 64 | ||||
432 | /** |
||||
433 | * Rename index (only if index exists). |
||||
434 | 224 | * |
|||
435 | * @param array $columns Index forming columns. |
||||
436 | * |
||||
437 | * @psalm-param non-empty-string $name New index name. |
||||
438 | 224 | * |
|||
439 | * @throws SchemaException |
||||
440 | */ |
||||
441 | 224 | public function renameIndex(array $columns, string $name): self |
|||
442 | { |
||||
443 | $this->hasIndex($columns) or throw new SchemaException( |
||||
444 | 224 | "Undefined index ['" . \implode("', '", $columns) . "'] in '{$this->getFullName()}'", |
|||
445 | ); |
||||
446 | |||||
447 | 224 | //Declaring new index name |
|||
448 | $this->index($columns)->setName($name); |
||||
449 | 224 | ||||
450 | return $this; |
||||
451 | } |
||||
452 | |||||
453 | /** |
||||
454 | * Drop column by it's name. |
||||
455 | * |
||||
456 | * @psalm-param non-empty-string $column |
||||
457 | * |
||||
458 | * @throws SchemaException |
||||
459 | */ |
||||
460 | 64 | public function dropColumn(string $column): self |
|||
461 | { |
||||
462 | 64 | $schema = $this->current->findColumn($column); |
|||
463 | $schema === null and throw new SchemaException("Undefined column '{$column}' in '{$this->getFullName()}'"); |
||||
464 | |||||
465 | //Dropping column from current schema |
||||
466 | $this->current->forgetColumn($schema); |
||||
0 ignored issues
–
show
It seems like
$schema can also be of type null ; however, parameter $column of Cycle\Database\Schema\State::forgetColumn() does only seem to accept Cycle\Database\Schema\AbstractColumn , 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
![]() |
|||||
467 | 64 | ||||
468 | return $this; |
||||
469 | 64 | } |
|||
470 | |||||
471 | /** |
||||
472 | * Drop index by it's forming columns. |
||||
473 | * |
||||
474 | * @throws SchemaException |
||||
475 | */ |
||||
476 | public function dropIndex(array $columns): self |
||||
477 | { |
||||
478 | $schema = $this->current->findIndex($columns); |
||||
479 | $schema === null and throw new SchemaException( |
||||
480 | 8 | "Undefined index ['" . \implode("', '", $columns) . "'] in '{$this->getFullName()}'", |
|||
481 | ); |
||||
482 | 8 | ||||
483 | //Dropping index from current schema |
||||
484 | $this->current->forgetIndex($schema); |
||||
0 ignored issues
–
show
It seems like
$schema can also be of type null ; however, parameter $index of Cycle\Database\Schema\State::forgetIndex() does only seem to accept Cycle\Database\Schema\AbstractIndex , 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
![]() |
|||||
485 | |||||
486 | return $this; |
||||
487 | 8 | } |
|||
488 | |||||
489 | 8 | /** |
|||
490 | * Drop foreign key by it's name. |
||||
491 | * |
||||
492 | * @throws SchemaException |
||||
493 | */ |
||||
494 | public function dropForeignKey(array $columns): self |
||||
495 | { |
||||
496 | $schema = $this->current->findForeignKey($columns); |
||||
497 | if ($schema === null) { |
||||
498 | $names = \implode("','", $columns); |
||||
499 | 32 | throw new SchemaException("Undefined FK on '{$names}' in '{$this->getFullName()}'"); |
|||
500 | } |
||||
501 | 32 | ||||
502 | 32 | //Dropping foreign from current schema |
|||
503 | $this->current->forgetForeignKey($schema); |
||||
504 | |||||
505 | 32 | return $this; |
|||
506 | } |
||||
507 | 32 | ||||
508 | /** |
||||
509 | * Get current table state (detached). |
||||
510 | */ |
||||
511 | public function getState(): State |
||||
512 | { |
||||
513 | $state = clone $this->current; |
||||
514 | $state->remountElements(); |
||||
515 | 102 | ||||
516 | return $state; |
||||
517 | 102 | } |
|||
518 | 102 | ||||
519 | /** |
||||
520 | * Reset table state to new form. |
||||
521 | * |
||||
522 | * @param State $state Use null to flush table schema. |
||||
523 | 102 | */ |
|||
524 | public function setState(?State $state = null): self |
||||
525 | 102 | { |
|||
526 | $this->current = new State($this->initial->getName()); |
||||
527 | |||||
528 | if ($state !== null) { |
||||
529 | $this->current->setName($state->getName()); |
||||
530 | $this->current->syncState($state); |
||||
531 | } |
||||
532 | |||||
533 | 216 | return $this; |
|||
534 | } |
||||
535 | 216 | ||||
536 | 216 | /** |
|||
537 | * Reset table state to it initial form. |
||||
538 | */ |
||||
539 | public function resetState(): self |
||||
540 | { |
||||
541 | $this->setState($this->initial); |
||||
542 | 216 | ||||
543 | return $this; |
||||
544 | 216 | } |
|||
545 | |||||
546 | /** |
||||
547 | * Save table schema including every column, index, foreign key creation/altering. If table |
||||
548 | * does not exist it must be created. If table declared as dropped it will be removed from |
||||
549 | * the database. |
||||
550 | 852 | * |
|||
551 | * @param int $operation Operation to be performed while table being saved. In some cases |
||||
552 | 852 | * (when multiple tables are being updated) it is reasonable to drop |
|||
553 | 852 | * foreign keys and indexes prior to dropping related columns. See sync |
|||
554 | * bus class to get more details. |
||||
555 | 852 | * @param bool $reset When true schema will be marked as synced. |
|||
556 | * |
||||
557 | * @throws HandlerException |
||||
558 | * @throws SchemaException |
||||
559 | */ |
||||
560 | public function save(int $operation = HandlerInterface::DO_ALL, bool $reset = true): void |
||||
561 | { |
||||
562 | // We need an instance of Handler of dbal operations |
||||
563 | 1974 | $handler = $this->driver->getSchemaHandler(); |
|||
564 | |||||
565 | 1974 | if ($this->status === self::STATUS_DECLARED_DROPPED && $operation & HandlerInterface::DO_DROP) { |
|||
566 | //We don't need reflector for this operation |
||||
567 | 1974 | $handler->dropTable($this); |
|||
568 | 1974 | ||||
569 | 1974 | //Flushing status |
|||
570 | $this->status = self::STATUS_NEW; |
||||
571 | |||||
572 | 1974 | return; |
|||
573 | } |
||||
574 | |||||
575 | // Ensure that columns references to valid indexes and et |
||||
576 | $prepared = $this->normalizeSchema(($operation & HandlerInterface::CREATE_FOREIGN_KEYS) !== 0); |
||||
577 | |||||
578 | 116 | if ($this->status === self::STATUS_NEW) { |
|||
579 | //Executing table creation |
||||
580 | 116 | $handler->createTable($prepared); |
|||
581 | } else { |
||||
582 | 116 | //Executing table syncing |
|||
583 | if ($this->hasChanges()) { |
||||
584 | $handler->syncTable($prepared, $operation); |
||||
585 | } |
||||
586 | } |
||||
587 | |||||
588 | // Syncing our schemas |
||||
589 | if ($reset) { |
||||
590 | $this->status = self::STATUS_EXISTS; |
||||
591 | $this->initial->syncState($prepared->current); |
||||
592 | } |
||||
593 | } |
||||
594 | |||||
595 | /** |
||||
596 | * Shortcut for column() method. |
||||
597 | * |
||||
598 | * @psalm-param non-empty-string $column |
||||
599 | 1950 | */ |
|||
600 | public function __get(string $column): AbstractColumn |
||||
601 | { |
||||
602 | 1950 | return $this->column($column); |
|||
603 | } |
||||
604 | 1950 | ||||
605 | /** |
||||
606 | 1930 | * Column creation/altering shortcut, call chain is identical to: |
|||
607 | * AbstractTable->column($name)->$type($arguments). |
||||
608 | * |
||||
609 | 1930 | * Example: |
|||
610 | * $table->string("name"); |
||||
611 | 1930 | * $table->text("some_column"); |
|||
612 | * |
||||
613 | * @psalm-param non-empty-string $type |
||||
614 | * |
||||
615 | 1950 | * @param array $arguments Type specific parameters. |
|||
616 | */ |
||||
617 | 1950 | public function __call(string $type, array $arguments): AbstractColumn |
|||
618 | { |
||||
619 | 1948 | return \call_user_func_array( |
|||
620 | [$this->column($arguments[0]), $type], |
||||
621 | \array_slice($arguments, 1), |
||||
622 | 1836 | ); |
|||
623 | 646 | } |
|||
624 | |||||
625 | public function __toString(): string |
||||
626 | { |
||||
627 | return $this->getFullName(); |
||||
628 | 1944 | } |
|||
629 | 1944 | ||||
630 | 1944 | /** |
|||
631 | * Cloning schemas as well. |
||||
632 | 1944 | */ |
|||
633 | public function __clone() |
||||
634 | { |
||||
635 | $this->initial = clone $this->initial; |
||||
636 | $this->current = clone $this->current; |
||||
637 | } |
||||
638 | |||||
639 | public function __debugInfo(): array |
||||
640 | { |
||||
641 | 458 | return [ |
|||
642 | 'status' => $this->status, |
||||
643 | 458 | 'full_name' => $this->getFullName(), |
|||
644 | 'name' => $this->getName(), |
||||
645 | 'primaryKeys' => $this->getPrimaryKeys(), |
||||
646 | 'columns' => \array_values($this->getColumns()), |
||||
647 | 'indexes' => \array_values($this->getIndexes()), |
||||
648 | 'foreignKeys' => \array_values($this->getForeignKeys()), |
||||
649 | 1836 | ]; |
|||
650 | } |
||||
651 | 1836 | ||||
652 | /** |
||||
653 | * Check if table schema has been modified since synchronization. |
||||
654 | */ |
||||
655 | protected function hasChanges(): bool |
||||
656 | { |
||||
657 | return $this->getComparator()->hasChanges() || $this->status === self::STATUS_DECLARED_DROPPED; |
||||
658 | } |
||||
659 | |||||
660 | /** |
||||
661 | 1974 | * Add prefix to a given table name |
|||
662 | * |
||||
663 | 1974 | * @psalm-param non-empty-string $column |
|||
664 | * |
||||
665 | * @psalm-return non-empty-string |
||||
666 | */ |
||||
667 | protected function prefixTableName(string $name): string |
||||
668 | { |
||||
669 | return $this->prefix . $name; |
||||
670 | 1950 | } |
|||
671 | |||||
672 | /** |
||||
673 | 1950 | * Ensure that no wrong indexes left in table. This method will create AbstractTable |
|||
674 | * copy in order to prevent cross modifications. |
||||
675 | */ |
||||
676 | 1950 | protected function normalizeSchema(bool $withForeignKeys = true): self |
|||
677 | 16 | { |
|||
678 | 8 | // To make sure that no pre-sync modifications will be reflected on current table |
|||
679 | $target = clone $this; |
||||
680 | |||||
681 | // declare all FKs dropped on tables scheduled for removal |
||||
682 | if ($this->status === self::STATUS_DECLARED_DROPPED) { |
||||
683 | foreach ($target->getForeignKeys() as $fk) { |
||||
684 | $target->current->forgetForeignKey($fk); |
||||
685 | } |
||||
686 | 1950 | } |
|||
687 | 36 | ||||
688 | 12 | /* |
|||
689 | * In cases where columns are removed we have to automatically remove related indexes and |
||||
690 | * foreign keys. |
||||
691 | */ |
||||
692 | foreach ($this->getComparator()->droppedColumns() as $column) { |
||||
693 | 36 | foreach ($target->getIndexes() as $index) { |
|||
694 | 12 | if (\in_array($column->getName(), $index->getColumns(), true)) { |
|||
695 | $target->current->forgetIndex($index); |
||||
696 | } |
||||
697 | } |
||||
698 | |||||
699 | foreach ($target->getForeignKeys() as $foreign) { |
||||
700 | if ($column->getName() === $foreign->getColumns()) { |
||||
701 | 1950 | $target->current->forgetForeignKey($foreign); |
|||
702 | } |
||||
703 | } |
||||
704 | } |
||||
705 | |||||
706 | 150 | //We also have to adjusts indexes and foreign keys |
|||
707 | foreach ($this->getComparator()->alteredColumns() as $pair) { |
||||
708 | 150 | /** |
|||
709 | 24 | * @var AbstractColumn $initial |
|||
710 | 16 | * @var AbstractColumn $name |
|||
711 | */ |
||||
712 | [$name, $initial] = $pair; |
||||
713 | 16 | ||||
714 | 16 | foreach ($target->getIndexes() as $index) { |
|||
715 | 16 | if (\in_array($initial->getName(), $index->getColumns(), true)) { |
|||
716 | $columns = $index->getColumns(); |
||||
717 | |||||
718 | 16 | //Replacing column name |
|||
719 | foreach ($columns as &$column) { |
||||
720 | 16 | if ($column === $initial->getName()) { |
|||
721 | $column = $name->getName(); |
||||
722 | 16 | } |
|||
723 | 16 | ||||
724 | unset($column); |
||||
725 | 16 | } |
|||
726 | unset($column); |
||||
727 | |||||
728 | 16 | $targetIndex = $target->initial->findIndex($index->getColumns()); |
|||
729 | if ($targetIndex !== null) { |
||||
730 | //Target index got renamed or removed. |
||||
731 | $targetIndex->columns($columns); |
||||
732 | 150 | } |
|||
733 | 8 | ||||
734 | 8 | $index->columns($columns); |
|||
735 | 8 | } |
|||
736 | 8 | } |
|||
737 | |||||
738 | foreach ($target->getForeignKeys() as $foreign) { |
||||
739 | $foreign->columns( |
||||
740 | \array_map( |
||||
741 | static fn($column) => $column === $initial->getName() ? $name->getName() : $column, |
||||
742 | 1950 | $foreign->getColumns(), |
|||
743 | 1834 | ), |
|||
744 | ); |
||||
745 | 96 | } |
|||
746 | } |
||||
747 | |||||
748 | if (!$withForeignKeys) { |
||||
749 | 1950 | foreach ($this->getComparator()->addedForeignKeys() as $foreign) { |
|||
750 | //Excluding from creation |
||||
751 | $target->current->forgetForeignKey($foreign); |
||||
752 | } |
||||
753 | } |
||||
754 | |||||
755 | 1928 | return $target; |
|||
756 | } |
||||
757 | 1928 | ||||
758 | 1928 | /** |
|||
759 | * Populate table schema with values from database. |
||||
760 | */ |
||||
761 | 1928 | protected function initSchema(State $state): void |
|||
762 | 458 | { |
|||
763 | foreach ($this->fetchColumns() as $column) { |
||||
764 | $state->registerColumn($column); |
||||
765 | 1928 | } |
|||
766 | 224 | ||||
767 | foreach ($this->fetchIndexes() as $index) { |
||||
768 | $state->registerIndex($index); |
||||
769 | 1928 | } |
|||
770 | |||||
771 | 1928 | foreach ($this->fetchReferences() as $foreign) { |
|||
772 | $state->registerForeignKey($foreign); |
||||
773 | 12 | } |
|||
774 | |||||
775 | 12 | $state->setPrimaryKeys($this->fetchPrimaryKeys()); |
|||
776 | //DBMS specific initialization can be placed here |
||||
777 | } |
||||
778 | |||||
779 | protected function isIndexColumnSortingSupported(): bool |
||||
780 | { |
||||
781 | return true; |
||||
782 | } |
||||
783 | |||||
784 | /** |
||||
785 | * Fetch index declarations from database. |
||||
786 | * |
||||
787 | * @return AbstractColumn[] |
||||
788 | */ |
||||
789 | abstract protected function fetchColumns(): array; |
||||
790 | |||||
791 | /** |
||||
792 | * Fetch index declarations from database. |
||||
793 | * |
||||
794 | * @return AbstractIndex[] |
||||
795 | */ |
||||
796 | abstract protected function fetchIndexes(): array; |
||||
797 | |||||
798 | /** |
||||
799 | * Fetch references declaration from database. |
||||
800 | * |
||||
801 | * @return AbstractForeignKey[] |
||||
802 | */ |
||||
803 | abstract protected function fetchReferences(): array; |
||||
804 | |||||
805 | /** |
||||
806 | * Fetch names of primary keys from table. |
||||
807 | */ |
||||
808 | abstract protected function fetchPrimaryKeys(): array; |
||||
809 | |||||
810 | /** |
||||
811 | * Create column with a given name. |
||||
812 | * |
||||
813 | * @psalm-param non-empty-string $name |
||||
814 | * |
||||
815 | */ |
||||
816 | abstract protected function createColumn(string $name): AbstractColumn; |
||||
817 | |||||
818 | /** |
||||
819 | * Create index for a given set of columns. |
||||
820 | * |
||||
821 | * @psalm-param non-empty-string $name |
||||
822 | */ |
||||
823 | abstract protected function createIndex(string $name): AbstractIndex; |
||||
824 | |||||
825 | /** |
||||
826 | * Create reference on a given column set. |
||||
827 | * |
||||
828 | * @psalm-param non-empty-string $name |
||||
829 | */ |
||||
830 | abstract protected function createForeign(string $name): AbstractForeignKey; |
||||
831 | |||||
832 | 458 | /** |
|||
833 | * Generate unique name for indexes and foreign keys. |
||||
834 | * |
||||
835 | 458 | * @psalm-param non-empty-string $type |
|||
836 | 458 | */ |
|||
837 | 458 | protected function createIdentifier(string $type, array $columns): string |
|||
838 | { |
||||
839 | // Sanitize columns in case they have expressions |
||||
840 | 458 | $sanitized = []; |
|||
841 | 458 | foreach ($columns as $column) { |
|||
842 | 458 | $sanitized[] = self::sanitizeColumnExpression($column); |
|||
843 | 458 | } |
|||
844 | |||||
845 | 458 | $name = $this->getFullName() |
|||
846 | . '_' . $type |
||||
847 | 24 | . '_' . \implode('_', $sanitized) |
|||
848 | . '_' . \uniqid(); |
||||
849 | |||||
850 | 458 | if (\strlen($name) > 64) { |
|||
851 | //Many DBMS has limitations on identifier length |
||||
852 | $name = \md5($name); |
||||
853 | } |
||||
854 | |||||
855 | return $name; |
||||
856 | } |
||||
857 | } |
||||
858 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths