Passed
Pull Request — master (#808)
by Sergei
15:21 queued 13:01
created

AbstractSchema::getColumnFactory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema;
6
7
use Psr\SimpleCache\InvalidArgumentException;
8
use Throwable;
9
use Yiisoft\Db\Cache\SchemaCache;
10
use Yiisoft\Db\Command\DataType;
11
use Yiisoft\Db\Connection\ConnectionInterface;
12
use Yiisoft\Db\Constraint\Constraint;
13
use Yiisoft\Db\Constraint\IndexConstraint;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
16
use Yiisoft\Db\Schema\Column\ColumnFactory;
17
use Yiisoft\Db\Schema\Column\ColumnFactoryInterface;
18
19
use function array_change_key_case;
20
use function array_map;
21
use function gettype;
22
use function is_array;
23
use function preg_match;
24
use function preg_replace;
25
use function str_contains;
26
use function str_replace;
27
28
/**
29
 * Provides a set of methods for working with database schemas such as creating, modifying, and inspecting tables,
30
 * columns, and other database objects.
31
 *
32
 * It's a powerful and flexible tool that allows you to perform a wide range of database operations in a
33
 * database-agnostic way.
34
 */
35
abstract class AbstractSchema implements SchemaInterface
36
{
37
    /**
38
     * Schema cache version, to detect incompatibilities in cached values when the data format of the cache changes.
39
     */
40
    protected const SCHEMA_CACHE_VERSION = 1;
41
    protected const CACHE_VERSION = 'cacheVersion';
42
    /**
43
     * @var string|null $defaultSchema The default schema name used for the current session.
44
     */
45
    protected string|null $defaultSchema = null;
46
    protected array $viewNames = [];
47
    private array $schemaNames = [];
48
    /** @psalm-var string[]|array */
49
    private array $tableNames = [];
50
    private array $tableMetadata = [];
51
52
    public function __construct(
53
        protected ConnectionInterface $db,
54
        private SchemaCache $schemaCache,
55
        private ColumnFactoryInterface|null $columnFactory = null,
56
    ) {
57
    }
58
59
    /**
60
     * @param string $name The table name.
61
     *
62
     * @return array The cache key for the specified table name.
63
     */
64
    abstract protected function getCacheKey(string $name): array;
65
66
    /**
67
     * @return string The cache tag name.
68
     *
69
     * This allows {@see refresh()} to invalidate all cached table schemas.
70
     */
71
    abstract protected function getCacheTag(): string;
72
73
    /**
74
     * Loads all check constraints for the given table.
75
     *
76
     * @param string $tableName The table name.
77
     *
78
     * @return array The check constraints for the given table.
79
     */
80
    abstract protected function loadTableChecks(string $tableName): array;
81
82
    /**
83
     * Loads all default value constraints for the given table.
84
     *
85
     * @param string $tableName The table name.
86
     *
87
     * @return array The default value constraints for the given table.
88
     */
89
    abstract protected function loadTableDefaultValues(string $tableName): array;
90
91
    /**
92
     * Loads all foreign keys for the given table.
93
     *
94
     * @param string $tableName The table name.
95
     *
96
     * @return array The foreign keys for the given table.
97
     */
98
    abstract protected function loadTableForeignKeys(string $tableName): array;
99
100
    /**
101
     * Loads all indexes for the given table.
102
     *
103
     * @param string $tableName The table name.
104
     *
105
     * @return IndexConstraint[] The indexes for the given table.
106
     */
107
    abstract protected function loadTableIndexes(string $tableName): array;
108
109
    /**
110
     * Loads a primary key for the given table.
111
     *
112
     * @param string $tableName The table name.
113
     *
114
     * @return Constraint|null The primary key for the given table. `null` if the table has no primary key.
115
     */
116
    abstract protected function loadTablePrimaryKey(string $tableName): Constraint|null;
117
118
    /**
119
     * Loads all unique constraints for the given table.
120
     *
121
     * @param string $tableName The table name.
122
     *
123
     * @return array The unique constraints for the given table.
124
     */
125
    abstract protected function loadTableUniques(string $tableName): array;
126
127
    /**
128
     * Loads the metadata for the specified table.
129
     *
130
     * @param string $name The table name.
131
     *
132
     * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table doesn't exist.
133
     */
134
    abstract protected function loadTableSchema(string $name): TableSchemaInterface|null;
135
136
    public function getColumnFactory(): ColumnFactoryInterface
137
    {
138
        return $this->columnFactory ??= new ColumnFactory();
139
    }
140
141
    public function getDefaultSchema(): string|null
142
    {
143
        return $this->defaultSchema;
144
    }
145
146
    public function getDataType(mixed $data): int
147
    {
148
        return match (gettype($data)) {
149
            // php type => SQL data type
150
            SchemaInterface::PHP_TYPE_BOOLEAN => DataType::BOOLEAN,
151
            SchemaInterface::PHP_TYPE_INTEGER => DataType::INTEGER,
152
            SchemaInterface::PHP_TYPE_RESOURCE => DataType::LOB,
153
            SchemaInterface::PHP_TYPE_NULL => DataType::NULL,
154
            default => DataType::STRING,
155
        };
156
    }
157
158
    public function getRawTableName(string $name): string
159
    {
160
        if (str_contains($name, '{{')) {
161
            $name = preg_replace('/{{(.*?)}}/', '\1', $name);
162
163
            return str_replace('%', $this->db->getTablePrefix(), $name);
164
        }
165
166
        return $name;
167
    }
168
169
    /**
170
     * @throws InvalidArgumentException
171
     * @throws NotSupportedException
172
     */
173
    public function getSchemaChecks(string $schema = '', bool $refresh = false): array
174
    {
175
        return $this->getSchemaMetadata($schema, SchemaInterface::CHECKS, $refresh);
176
    }
177
178
    /**
179
     * @throws InvalidArgumentException
180
     * @throws NotSupportedException
181
     */
182
    public function getSchemaDefaultValues(string $schema = '', bool $refresh = false): array
183
    {
184
        return $this->getSchemaMetadata($schema, SchemaInterface::DEFAULT_VALUES, $refresh);
185
    }
186
187
    /**
188
     * @throws InvalidArgumentException
189
     * @throws NotSupportedException
190
     */
191
    public function getSchemaForeignKeys(string $schema = '', bool $refresh = false): array
192
    {
193
        return $this->getSchemaMetadata($schema, SchemaInterface::FOREIGN_KEYS, $refresh);
194
    }
195
196
    /**
197
     * @throws InvalidArgumentException
198
     * @throws NotSupportedException
199
     */
200
    public function getSchemaIndexes(string $schema = '', bool $refresh = false): array
201
    {
202
        return $this->getSchemaMetadata($schema, SchemaInterface::INDEXES, $refresh);
203
    }
204
205
    /**
206
     * @throws NotSupportedException If this method isn't supported by the underlying DBMS.
207
     */
208
    public function getSchemaNames(bool $refresh = false): array
209
    {
210
        if (empty($this->schemaNames) || $refresh) {
211
            $this->schemaNames = $this->findSchemaNames();
212
        }
213
214
        return $this->schemaNames;
215
    }
216
217
    /**
218
     * @throws InvalidArgumentException
219
     * @throws NotSupportedException
220
     */
221
    public function getSchemaPrimaryKeys(string $schema = '', bool $refresh = false): array
222
    {
223
        /** @psalm-var list<Constraint> */
224
        return $this->getSchemaMetadata($schema, SchemaInterface::PRIMARY_KEY, $refresh);
225
    }
226
227
    /**
228
     * @throws NotSupportedException
229
     * @throws InvalidArgumentException
230
     */
231
    public function getSchemaUniques(string $schema = '', bool $refresh = false): array
232
    {
233
        return $this->getSchemaMetadata($schema, SchemaInterface::UNIQUES, $refresh);
234
    }
235
236
    /**
237
     * @throws InvalidArgumentException
238
     */
239
    public function getTableChecks(string $name, bool $refresh = false): array
240
    {
241
        /** @psalm-var mixed $tableChecks */
242
        $tableChecks = $this->getTableMetadata($name, SchemaInterface::CHECKS, $refresh);
243
        return is_array($tableChecks) ? $tableChecks : [];
244
    }
245
246
    /**
247
     * @throws InvalidArgumentException
248
     */
249
    public function getTableDefaultValues(string $name, bool $refresh = false): array
250
    {
251
        /** @psalm-var mixed $tableDefaultValues */
252
        $tableDefaultValues = $this->getTableMetadata($name, SchemaInterface::DEFAULT_VALUES, $refresh);
253
        return is_array($tableDefaultValues) ? $tableDefaultValues : [];
254
    }
255
256
    /**
257
     * @throws InvalidArgumentException
258
     */
259
    public function getTableForeignKeys(string $name, bool $refresh = false): array
260
    {
261
        /** @psalm-var mixed $tableForeignKeys */
262
        $tableForeignKeys = $this->getTableMetadata($name, SchemaInterface::FOREIGN_KEYS, $refresh);
263
        return is_array($tableForeignKeys) ? $tableForeignKeys : [];
264
    }
265
266
    /**
267
     * @throws InvalidArgumentException
268
     */
269
    public function getTableIndexes(string $name, bool $refresh = false): array
270
    {
271
        /** @var IndexConstraint[] */
272
        return $this->getTableMetadata($name, SchemaInterface::INDEXES, $refresh);
273
    }
274
275
    /**
276
     * @throws NotSupportedException If this method isn't supported by the underlying DBMS.
277
     */
278
    public function getTableNames(string $schema = '', bool $refresh = false): array
279
    {
280
        if (!isset($this->tableNames[$schema]) || $refresh) {
281
            $this->tableNames[$schema] = $this->findTableNames($schema);
282
        }
283
284
        return is_array($this->tableNames[$schema]) ? $this->tableNames[$schema] : [];
285
    }
286
287
    /**
288
     * @throws InvalidArgumentException
289
     */
290
    public function getTablePrimaryKey(string $name, bool $refresh = false): Constraint|null
291
    {
292
        /** @psalm-var mixed $tablePrimaryKey */
293
        $tablePrimaryKey = $this->getTableMetadata($name, SchemaInterface::PRIMARY_KEY, $refresh);
294
        return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null;
295
    }
296
297
    /**
298
     * @throws InvalidArgumentException
299
     */
300
    public function getTableSchema(string $name, bool $refresh = false): TableSchemaInterface|null
301
    {
302
        /** @psalm-var mixed $tableSchema */
303
        $tableSchema = $this->getTableMetadata($name, SchemaInterface::SCHEMA, $refresh);
304
        return $tableSchema instanceof TableSchemaInterface ? $tableSchema : null;
305
    }
306
307
    /**
308
     * @throws NotSupportedException
309
     * @throws InvalidArgumentException
310
     */
311
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
312
    {
313
        /** @psalm-var list<TableSchemaInterface> */
314
        return $this->getSchemaMetadata($schema, SchemaInterface::SCHEMA, $refresh);
315
    }
316
317
    /**
318
     * @throws InvalidArgumentException
319
     *
320
     * @return array The metadata for table unique constraints.
321
     */
322
    public function getTableUniques(string $name, bool $refresh = false): array
323
    {
324
        /** @psalm-var mixed $tableUniques */
325
        $tableUniques = $this->getTableMetadata($name, SchemaInterface::UNIQUES, $refresh);
326
        return is_array($tableUniques) ? $tableUniques : [];
327
    }
328
329
    public function isReadQuery(string $sql): bool
330
    {
331
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
332
333
        return preg_match($pattern, $sql) > 0;
334
    }
335
336
    /**
337
     * @throws InvalidArgumentException
338
     */
339
    public function refresh(): void
340
    {
341
        if ($this->schemaCache->isEnabled()) {
342
            $this->schemaCache->invalidate($this->getCacheTag());
343
        }
344
345
        $this->tableNames = [];
346
        $this->tableMetadata = [];
347
    }
348
349
    /**
350
     * @throws InvalidArgumentException
351
     */
352
    public function refreshTableSchema(string $name): void
353
    {
354
        $rawName = $this->getRawTableName($name);
355
356
        unset($this->tableMetadata[$rawName]);
357
358
        $this->tableNames = [];
359
360
        if ($this->schemaCache->isEnabled()) {
361
            $this->schemaCache->remove($this->getCacheKey($rawName));
362
        }
363
    }
364
365
    public function enableCache(bool $value): void
366
    {
367
        $this->schemaCache->setEnabled($value);
368
    }
369
370
    /**
371
     * Returns all schema names in the database, including the default one but not system schemas.
372
     *
373
     * This method should be overridden by child classes to support this feature because the default
374
     * implementation simply throws an exception.
375
     *
376
     * @throws NotSupportedException If the DBMS doesn't support this method.
377
     *
378
     * @return array All schemas name in the database, except system schemas.
379
     */
380
    protected function findSchemaNames(): array
381
    {
382
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
383
    }
384
385
    /**
386
     * Returns all table names in the database.
387
     *
388
     * This method should be overridden by child classes to support this feature because the default
389
     * implementation simply throws an exception.
390
     *
391
     * @param string $schema The schema of the tables. Defaults to empty string, meaning the current or default schema.
392
     *
393
     * @throws NotSupportedException If the DBMS doesn't support this method.
394
     *
395
     * @return array All tables name in the database. The names have NO schema name prefix.
396
     */
397
    protected function findTableNames(string $schema): array
0 ignored issues
show
Unused Code introduced by
The parameter $schema is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

397
    protected function findTableNames(/** @scrutinizer ignore-unused */ string $schema): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
398
    {
399
        throw new NotSupportedException(static::class . ' does not support fetching all table names.');
400
    }
401
402
    /**
403
     * Returns the metadata of the given type for all tables in the given schema.
404
     *
405
     * @param string $schema The schema of the metadata. Defaults to empty string, meaning the current or default schema
406
     * name.
407
     * @param string $type The metadata type.
408
     * @param bool $refresh Whether to fetch the latest available table metadata. If this is `false`, cached data may be
409
     * returned if available.
410
     *
411
     * @throws InvalidArgumentException
412
     * @throws NotSupportedException
413
     *
414
     * @return array The metadata of the given type for all tables in the given schema.
415
     *
416
     * @psalm-return list<Constraint|TableSchemaInterface|array>
417
     */
418
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
419
    {
420
        $metadata = [];
421
        /** @psalm-var string[] $tableNames */
422
        $tableNames = $this->getTableNames($schema, $refresh);
423
424
        foreach ($tableNames as $name) {
425
            $name = $this->db->getQuoter()->quoteSimpleTableName($name);
426
427
            if ($schema !== '') {
428
                $name = $schema . '.' . $name;
429
            }
430
431
            $tableMetadata = $this->getTableTypeMetadata($type, $name, $refresh);
432
433
            if ($tableMetadata !== null) {
434
                $metadata[] = $tableMetadata;
435
            }
436
        }
437
438
        return $metadata;
439
    }
440
441
    /**
442
     * Returns the metadata of the given type for the given table.
443
     *
444
     * @param string $name The table name. The table name may contain a schema name if any.
445
     * Don't quote the table name.
446
     * @param string $type The metadata type.
447
     * @param bool $refresh whether to reload the table metadata even if it's found in the cache.
448
     *
449
     * @throws InvalidArgumentException
450
     *
451
     * @return mixed The metadata of the given type for the given table.
452
     */
453
    protected function getTableMetadata(string $name, string $type, bool $refresh = false): mixed
454
    {
455
        $rawName = $this->getRawTableName($name);
456
457
        if (!isset($this->tableMetadata[$rawName])) {
458
            $this->loadTableMetadataFromCache($rawName);
459
        }
460
461
        if ($refresh || !isset($this->tableMetadata[$rawName][$type])) {
462
            /** @psalm-suppress MixedArrayAssignment */
463
            $this->tableMetadata[$rawName][$type] = $this->loadTableTypeMetadata($type, $rawName);
464
            $this->saveTableMetadataToCache($rawName);
465
        }
466
467
        /** @psalm-suppress MixedArrayAccess */
468
        return $this->tableMetadata[$rawName][$type];
469
    }
470
471
    /**
472
     * This method returns the desired metadata type for the table name.
473
     */
474
    protected function loadTableTypeMetadata(string $type, string $name): Constraint|array|TableSchemaInterface|null
475
    {
476
        return match ($type) {
477
            SchemaInterface::SCHEMA => $this->loadTableSchema($name),
478
            SchemaInterface::PRIMARY_KEY => $this->loadTablePrimaryKey($name),
479
            SchemaInterface::UNIQUES => $this->loadTableUniques($name),
480
            SchemaInterface::FOREIGN_KEYS => $this->loadTableForeignKeys($name),
481
            SchemaInterface::INDEXES => $this->loadTableIndexes($name),
482
            SchemaInterface::DEFAULT_VALUES => $this->loadTableDefaultValues($name),
483
            SchemaInterface::CHECKS => $this->loadTableChecks($name),
484
            default => null,
485
        };
486
    }
487
488
    /**
489
     * This method returns the desired metadata type for table name (with refresh if needed).
490
     *
491
     * @throws InvalidArgumentException
492
     */
493
    protected function getTableTypeMetadata(
494
        string $type,
495
        string $name,
496
        bool $refresh = false
497
    ): Constraint|array|null|TableSchemaInterface {
498
        return match ($type) {
499
            SchemaInterface::SCHEMA => $this->getTableSchema($name, $refresh),
500
            SchemaInterface::PRIMARY_KEY => $this->getTablePrimaryKey($name, $refresh),
501
            SchemaInterface::UNIQUES => $this->getTableUniques($name, $refresh),
502
            SchemaInterface::FOREIGN_KEYS => $this->getTableForeignKeys($name, $refresh),
503
            SchemaInterface::INDEXES => $this->getTableIndexes($name, $refresh),
504
            SchemaInterface::DEFAULT_VALUES => $this->getTableDefaultValues($name, $refresh),
505
            SchemaInterface::CHECKS => $this->getTableChecks($name, $refresh),
506
            default => null,
507
        };
508
    }
509
510
    /**
511
     * Change row's array key case to lower.
512
     *
513
     * @param array $row Thew row's array or an array of row arrays.
514
     * @param bool $multiple Whether many rows or a single row passed.
515
     *
516
     * @return array The normalized row or rows.
517
     */
518
    protected function normalizeRowKeyCase(array $row, bool $multiple): array
519
    {
520
        if ($multiple) {
521
            return array_map(static fn (array $row) => array_change_key_case($row), $row);
522
        }
523
524
        return array_change_key_case($row);
525
    }
526
527
    /**
528
     * Resolves the table name and schema name (if any).
529
     *
530
     * @param string $name The table name.
531
     *
532
     * @throws NotSupportedException If the DBMS doesn't support this method.
533
     *
534
     * @return TableSchemaInterface The with resolved table, schema, etc. names.
535
     *
536
     * @see TableSchemaInterface
537
     */
538
    protected function resolveTableName(string $name): TableSchemaInterface
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

538
    protected function resolveTableName(/** @scrutinizer ignore-unused */ string $name): TableSchemaInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
539
    {
540
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
541
    }
542
543
    /**
544
     * Sets the metadata of the given type for the given table.
545
     *
546
     * @param string $name The table name.
547
     * @param string $type The metadata type.
548
     * @param mixed $data The metadata to set.
549
     */
550
    protected function setTableMetadata(string $name, string $type, mixed $data): void
551
    {
552
        /** @psalm-suppress MixedArrayAssignment  */
553
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
554
    }
555
556
    /**
557
     * Tries to load and populate table metadata from cache.
558
     *
559
     * @throws InvalidArgumentException
560
     */
561
    private function loadTableMetadataFromCache(string $rawName): void
562
    {
563
        if (!$this->schemaCache->isEnabled() || $this->schemaCache->isExcluded($rawName)) {
564
            $this->tableMetadata[$rawName] = [];
565
            return;
566
        }
567
568
        $metadata = $this->schemaCache->get($this->getCacheKey($rawName));
569
570
        if (
571
            !is_array($metadata) ||
572
            !isset($metadata[self::CACHE_VERSION]) ||
573
            $metadata[self::CACHE_VERSION] !== static::SCHEMA_CACHE_VERSION
574
        ) {
575
            $this->tableMetadata[$rawName] = [];
576
            return;
577
        }
578
579
        unset($metadata[self::CACHE_VERSION]);
580
        $this->tableMetadata[$rawName] = $metadata;
581
    }
582
583
    /**
584
     * Saves table metadata to cache.
585
     *
586
     * @throws InvalidArgumentException
587
     */
588
    private function saveTableMetadataToCache(string $rawName): void
589
    {
590
        if ($this->schemaCache->isEnabled() === false || $this->schemaCache->isExcluded($rawName) === true) {
591
            return;
592
        }
593
594
        /** @psalm-var array<string, array<TableSchemaInterface|int>> $metadata */
595
        $metadata = $this->tableMetadata[$rawName];
596
        /** @psalm-var int */
597
        $metadata[self::CACHE_VERSION] = static::SCHEMA_CACHE_VERSION;
598
599
        $this->schemaCache->set($this->getCacheKey($rawName), $metadata, $this->getCacheTag());
600
    }
601
602
    /**
603
     * Find the view names for the database.
604
     *
605
     * @param string $schema The schema of the views.
606
     * Defaults to empty string, meaning the current or default schema.
607
     *
608
     * @return array The names of all views in the database.
609
     */
610
    protected function findViewNames(string $schema = ''): array
0 ignored issues
show
Unused Code introduced by
The parameter $schema is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

610
    protected function findViewNames(/** @scrutinizer ignore-unused */ string $schema = ''): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
611
    {
612
        return [];
613
    }
614
615
    /**
616
     * @throws Throwable
617
     *
618
     * @return array The view names for the database.
619
     */
620
    public function getViewNames(string $schema = '', bool $refresh = false): array
621
    {
622
        if (!isset($this->viewNames[$schema]) || $refresh) {
623
            $this->viewNames[$schema] = $this->findViewNames($schema);
624
        }
625
626
        return (array) $this->viewNames[$schema];
627
    }
628
}
629