Passed
Pull Request — master (#282)
by Wilmer
13:20
created

Schema::getSchemaForeignKeys()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Schema;
6
7
use PDO;
8
use Yiisoft\Cache\Dependency\TagDependency;
9
use Yiisoft\Db\Cache\SchemaCache;
10
use Yiisoft\Db\Constraint\Constraint;
11
use Yiisoft\Db\Exception\Exception;
12
use Yiisoft\Db\Exception\NotSupportedException;
13
14
use function array_key_exists;
15
use function gettype;
16
use function is_array;
17
use function preg_match;
18
19
abstract class Schema implements SchemaInterface
20
{
21
    public const SCHEMA = 'schema';
22
    public const PRIMARY_KEY = 'primaryKey';
23
    public const INDEXES = 'indexes';
24
    public const CHECKS = 'checks';
25
    public const FOREIGN_KEYS = 'foreignKeys';
26
    public const DEFAULT_VALUES = 'defaultValues';
27
    public const UNIQUES = 'uniques';
28
29
    public const TYPE_PK = 'pk';
30
    public const TYPE_UPK = 'upk';
31
    public const TYPE_BIGPK = 'bigpk';
32
    public const TYPE_UBIGPK = 'ubigpk';
33
    public const TYPE_CHAR = 'char';
34
    public const TYPE_STRING = 'string';
35
    public const TYPE_TEXT = 'text';
36
    public const TYPE_TINYINT = 'tinyint';
37
    public const TYPE_SMALLINT = 'smallint';
38
    public const TYPE_INTEGER = 'integer';
39
    public const TYPE_BIGINT = 'bigint';
40
    public const TYPE_FLOAT = 'float';
41
    public const TYPE_DOUBLE = 'double';
42
    public const TYPE_DECIMAL = 'decimal';
43
    public const TYPE_DATETIME = 'datetime';
44
    public const TYPE_TIMESTAMP = 'timestamp';
45
    public const TYPE_TIME = 'time';
46
    public const TYPE_DATE = 'date';
47
    public const TYPE_BINARY = 'binary';
48
    public const TYPE_BOOLEAN = 'boolean';
49
    public const TYPE_MONEY = 'money';
50
    public const TYPE_JSON = 'json';
51
52
    /**
53
     * Schema cache version, to detect incompatibilities in cached values when the data format of the cache changes.
54
     */
55
    protected const SCHEMA_CACHE_VERSION = 1;
56
57
    /**
58
     * @var string|null the default schema name used for the current session.
59
     */
60
    protected ?string $defaultSchema = null;
61
    private array $schemaNames = [];
62
    private array $tableNames = [];
63
    private array $tableMetadata = [];
64
65
    public function __construct(private SchemaCache $schemaCache)
66
    {
67
    }
68
69
    /**
70
     * Returns the cache key for the specified table name.
71
     *
72
     * @param string $name the table name.
73
     *
74
     * @return array the cache key.
75
     */
76
    abstract protected function getCacheKey(string $name): array;
77
78
    /**
79
     * Returns the cache tag name.
80
     *
81
     * This allows {@see refresh()} to invalidate all cached table schemas.
82
     *
83
     * @return string the cache tag name.
84
     */
85
    abstract protected function getCacheTag(): string;
86
87
    /**
88
     * Loads all check constraints for the given table.
89
     *
90
     * @param string $tableName table name.
91
     *
92
     * @return array check constraints for the given table.
93
     */
94
    abstract protected function loadTableChecks(string $tableName): array;
95
96
    /**
97
     * Loads all default value constraints for the given table.
98
     *
99
     * @param string $tableName table name.
100
     *
101
     * @return array default value constraints for the given table.
102
     */
103
    abstract protected function loadTableDefaultValues(string $tableName): array;
104
105
    /**
106
     * Loads all foreign keys for the given table.
107
     *
108
     * @param string $tableName table name.
109
     *
110 2532
     * @return array foreign keys for the given table.
111
     */
112 2532
    abstract protected function loadTableForeignKeys(string $tableName): array;
113 2532
114 2532
    /**
115
     * Loads all indexes for the given table.
116
     *
117
     * @param string $tableName table name.
118
     *
119
     * @return array indexes for the given table.
120
     */
121 963
    abstract protected function loadTableIndexes(string $tableName): array;
122
123 963
    /**
124 963
     * Loads a primary key for the given table.
125
     *
126
     * @param string $tableName table name.
127 963
     *
128
     * @return Constraint|null primary key for the given table, `null` if the table has no primary key.
129
     */
130 1665
    abstract protected function loadTablePrimaryKey(string $tableName): ?Constraint;
131
132 1665
    /**
133
     * Loads all unique constraints for the given table.
134
     *
135
     * @param string $tableName table name.
136
     *
137
     * @return array unique constraints for the given table.
138
     */
139
    abstract protected function loadTableUniques(string $tableName): array;
140 18
141
    /**
142 18
     * Loads the metadata for the specified table.
143
     *
144
     * @param string $name table name.
145
     *
146
     * @return TableSchema|null DBMS-dependent table metadata, `null` if the table does not exist.
147
     */
148 1322
    abstract protected function loadTableSchema(string $name): ?TableSchema;
149
150 1322
    public function getDefaultSchema(): ?string
151
    {
152
        return $this->defaultSchema;
153
    }
154
155
    public function getPdoType(mixed $data): int
156 13
    {
157
        static $typeMap = [
158 13
            // php type => PDO type
159
            'boolean' => PDO::PARAM_BOOL,
160
            'integer' => PDO::PARAM_INT,
161
            'string' => PDO::PARAM_STR,
162
            'resource' => PDO::PARAM_LOB,
163
            'NULL' => PDO::PARAM_NULL,
164 4
        ];
165
166 4
        $type = gettype($data);
167 4
168
        return $typeMap[$type] ?? PDO::PARAM_STR;
169
    }
170 4
171
    public function getSchemaCache(): SchemaCache
172
    {
173
        return $this->schemaCache;
174
    }
175
176 23
    /**
177
     * @throws NotSupportedException
178 23
     */
179 23
    public function getSchemaChecks(string $schema = '', bool $refresh = false): array
180
    {
181
        return $this->getSchemaMetadata($schema, 'checks', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...ma, 'checks', $refresh) returns an array which contains values of type Yiisoft\Db\Constraint\Co...t\Db\Schema\TableSchema which are incompatible with the return type Yiisoft\Db\Constraint\CheckConstraint[] mandated by Yiisoft\Db\Constraint\Co...face::getSchemaChecks().
Loading history...
182 23
    }
183
184
    /**
185
     * @throws NotSupportedException
186
     */
187
    public function getSchemaDefaultValues(string $schema = '', bool $refresh = false): array
188 1360
    {
189
        return $this->getSchemaMetadata($schema, 'defaultValues', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...faultValues', $refresh) returns an array which contains values of type Yiisoft\Db\Schema\TableSchema|array which are incompatible with the return type Yiisoft\Db\Constraint\DefaultValueConstraint mandated by Yiisoft\Db\Constraint\Co...etSchemaDefaultValues().
Loading history...
190 1360
    }
191
192
    /**
193
     * @throws NotSupportedException
194
     */
195
    public function getSchemaForeignKeys(string $schema = '', bool $refresh = false): array
196
    {
197
        return $this->getSchemaMetadata($schema, 'foreignKeys', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...foreignKeys', $refresh) returns an array which contains values of type Yiisoft\Db\Constraint\Co...t\Db\Schema\TableSchema which are incompatible with the return type Yiisoft\Db\Constraint\ForeignKeyConstraint[] mandated by Yiisoft\Db\Constraint\Co...:getSchemaForeignKeys().
Loading history...
198
    }
199 1360
200
    /**
201 1360
     * @throws NotSupportedException
202
     */
203
    public function getSchemaIndexes(string $schema = '', bool $refresh = false): array
204
    {
205
        return $this->getSchemaMetadata($schema, 'indexes', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...a, 'indexes', $refresh) returns an array which contains values of type Yiisoft\Db\Constraint\Co...t\Db\Schema\TableSchema which are incompatible with the return type Yiisoft\Db\Constraint\IndexConstraint[] mandated by Yiisoft\Db\Constraint\Co...ace::getSchemaIndexes().
Loading history...
206
    }
207 96
208
    public function getSchemaNames(bool $refresh = false): array
209 96
    {
210 96
        if (empty($this->schemaNames) || $refresh) {
211
            $this->schemaNames = $this->findSchemaNames();
212
        }
213 96
214 96
        return $this->schemaNames;
215 96
    }
216
217
    /**
218
     * @throws NotSupportedException
219
     */
220 103
    public function getSchemaPrimaryKeys(string $schema = '', bool $refresh = false): array
221
    {
222 103
        return $this->getSchemaMetadata($schema, 'primaryKey', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...'primaryKey', $refresh) returns an array which contains values of type Yiisoft\Db\Schema\TableSchema|array which are incompatible with the return type Yiisoft\Db\Constraint\Constraint mandated by Yiisoft\Db\Constraint\Co...:getSchemaPrimaryKeys().
Loading history...
223
    }
224 103
225
    /**
226 103
     * @throws NotSupportedException
227
     */
228 103
    public function getSchemaUniques(string $schema = '', bool $refresh = false): array
229 103
    {
230
        return $this->getSchemaMetadata($schema, 'uniques', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...a, 'uniques', $refresh) returns an array which contains values of type Yiisoft\Db\Constraint\Co...t\Db\Schema\TableSchema which are incompatible with the return type Yiisoft\Db\Constraint\Constraint[] mandated by Yiisoft\Db\Constraint\Co...ace::getSchemaUniques().
Loading history...
231 103
    }
232
233
    public function getTableChecks(string $name, bool $refresh = false): array
234
    {
235
        return $this->getTableMetadata($name, 'checks', $refresh);
236 36
    }
237
238 36
    public function getTableDefaultValues(string $name, bool $refresh = false): array
239 36
    {
240 36
        return $this->getTableMetadata($name, 'defaultValues', $refresh);
241
    }
242
243
    public function getTableForeignKeys(string $name, bool $refresh = false): array
244
    {
245
        return $this->getTableMetadata($name, 'foreignKeys', $refresh);
246
    }
247
248
    public function getTableIndexes(string $name, bool $refresh = false): array
249
    {
250 10
        return $this->getTableMetadata($name, 'indexes', $refresh);
251
    }
252 10
253
    public function getTableNames(string $schema = '', bool $refresh = false): array
254
    {
255
        if (!isset($this->tableNames[$schema]) || $refresh) {
256
            $this->tableNames[$schema] = $this->findTableNames($schema);
257
        }
258 4
259
        return $this->tableNames[$schema];
260 4
    }
261 4
262
    public function getTablePrimaryKey(string $name, bool $refresh = false): ?Constraint
263
    {
264
        return $this->getTableMetadata($name, 'primaryKey', $refresh);
265
    }
266
267
    public function getTableSchema(string $name, bool $refresh = false): ?TableSchema
268
    {
269
        return $this->getTableMetadata($name, 'schema', $refresh);
270
    }
271
272
    public function getTableSchemas(string $schema = '', bool $refresh = false): array
273
    {
274 4
        return $this->getSchemaMetadata($schema, 'schema', $refresh);
0 ignored issues
show
introduced by
The expression return $this->getSchemaM...ma, 'schema', $refresh) returns an array which contains values of type Yiisoft\Db\Constraint\Constraint|array which are incompatible with the return type Yiisoft\Db\Schema\TableSchema mandated by Yiisoft\Db\Schema\Schema...face::getTableSchemas().
Loading history...
275
    }
276 4
277 4
    public function getTableUniques(string $name, bool $refresh = false): array
278
    {
279
        return $this->getTableMetadata($name, 'uniques', $refresh);
280
    }
281
282 8
    /**
283
     * Returns a value indicating whether a SQL statement is for read purpose.
284 8
     *
285 8
     * @param string $sql the SQL statement.
286
     *
287
     * @return bool whether a SQL statement is for read purpose.
288
     */
289
    public function isReadQuery(string $sql): bool
290 28
    {
291
        $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i';
292 28
293
        return preg_match($pattern, $sql) > 0;
294 28
    }
295
296
    /**
297
     * Refreshes the schema.
298 28
     *
299 28
     * This method cleans up all cached table schemas so that they can be re-created later to reflect the database
300
     * schema change.
301 28
     */
302 26
    public function refresh(): void
303 24
    {
304 24
        if ($this->schemaCache->isEnabled()) {
305
            $this->schemaCache->invalidate($this->getCacheTag());
306
        }
307 4
308
        $this->tableNames = [];
309
        $this->tableMetadata = [];
310 28
    }
311
312
    public function refreshTableSchema(string $name): void
313
    {
314
        $rawName = $this->getRawTableName($name);
315
316 1075
        unset($this->tableMetadata[$rawName]);
317
318 1075
        $this->tableNames = [];
319 6
320
        if ($this->schemaCache->isEnabled()) {
321
            $this->schemaCache->remove($this->getCacheKey($rawName));
322 1075
        }
323 1075
    }
324
325
    /**
326
     * Returns all schema names in the database, including the default one but not system schemas.
327
     *
328
     * This method should be overridden by child classes in order to support this feature because the default
329
     * implementation simply throws an exception.
330
     *
331
     * @throws NotSupportedException if this method is not supported by the DBMS.
332
     *
333 1574
     * @return array all schema names in the database, except system schemas.
334
     */
335 1574
    protected function findSchemaNames(): array
336 4
    {
337
        throw new NotSupportedException(static::class . ' does not support fetching all schema names.');
338
    }
339 1574
340 146
    /**
341
     * Returns all table names in the database.
342
     *
343 1550
     * This method should be overridden by child classes in order to support this feature because the default
344 1467
     * implementation simply throws an exception.
345
     *
346
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
347 287
     *
348
     * @throws NotSupportedException if this method is not supported by the DBMS.
349 287
     *
350 287
     * @return array all table names in the database. The names have NO schema name prefix.
351
     */
352
    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

352
    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...
353 287
    {
354
        throw new NotSupportedException(static::class . ' does not support fetching all table names.');
355
    }
356
357
    /**
358
     * Extracts the PHP type from abstract DB type.
359 1698
     *
360
     * @param ColumnSchema $column the column schema information.
361 1698
     *
362 149
     * @return string PHP type name.
363
     */
364
    protected function getColumnPhpType(ColumnSchema $column): string
365 1683
    {
366 232
        static $typeMap = [
367 232
            // abstract type => php type
368
            self::TYPE_TINYINT => 'integer',
369 1673
            self::TYPE_SMALLINT => 'integer',
370
            self::TYPE_INTEGER => 'integer',
371
            self::TYPE_BIGINT => 'integer',
372 1683
            self::TYPE_BOOLEAN => 'boolean',
373 4
            self::TYPE_FLOAT => 'double',
374
            self::TYPE_DOUBLE => 'double',
375
            self::TYPE_BINARY => 'resource',
376 1683
            self::TYPE_JSON => 'array',
377
        ];
378
379
        if (isset($typeMap[$column->getType()])) {
380
            if ($column->getType() === 'bigint') {
381
                return PHP_INT_SIZE === 8 && !$column->isUnsigned() ? 'integer' : 'string';
382 1344
            }
383
384 1344
            if ($column->getType() === 'integer') {
385 987
                return PHP_INT_SIZE === 4 && $column->isUnsigned() ? 'string' : 'integer';
386
            }
387 357
388
            return $typeMap[$column->getType()];
389
        }
390 1344
391
        return 'string';
392
    }
393
394
    /**
395
     * Returns the metadata of the given type for all tables in the given schema.
396 1683
     *
397
     * @param string $schema the schema of the metadata. Defaults to empty string, meaning the current or default schema
398 1683
     * name.
399 1357
     * @param string $type metadata type.
400
     * @param bool $refresh whether to fetch the latest available table metadata. If this is `false`, cached data may be
401 326
     * returned if available.
402
     *
403
     * @throws NotSupportedException
404 1683
     *
405 1683
     * @return array array of metadata.
406
     */
407
    protected function getSchemaMetadata(string $schema, string $type, bool $refresh): array
408
    {
409
        $metadata = [];
410
411 5
        foreach ($this->getTableNames($schema, $refresh) as $name) {
412
            if ($schema !== '') {
413 5
                $name = $schema . '.' . $name;
414 5
            }
415
416
            $tableMetadata = $this->getTableTypeMetadata($type, $name, $refresh);
417
418
            if ($tableMetadata !== null) {
419 5
                $metadata[] = $tableMetadata;
420
            }
421
        }
422
423
        return $metadata;
424
    }
425
426
    /**
427
     * Returns the metadata of the given type for the given table.
428
     *
429
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
430
     * @param string $type metadata type.
431
     * @param bool $refresh whether to reload the table metadata even if it is found in the cache.
432
     *
433
     * @return mixed metadata.
434
     */
435
    protected function getTableMetadata(string $name, string $type, bool $refresh = false): mixed
436
    {
437
        $rawName = $this->getRawTableName($name);
438
439 1622
        if (!isset($this->tableMetadata[$rawName])) {
440
            $this->loadTableMetadataFromCache($rawName);
441 1622
        }
442 130
443
        if ($refresh || !array_key_exists($type, $this->tableMetadata[$rawName])) {
444 130
            $this->tableMetadata[$rawName][$type] = $this->loadTableTypeMetadata($type, $rawName);
445
            $this->saveTableMetadataToCache($rawName);
446
        }
447 1622
448
        return $this->tableMetadata[$rawName][$type];
449
    }
450
451
    /**
452
     * This method returns the desired metadata type for the table name.
453 50
     *
454
     * @param string $type
455 50
     * @param string $name
456
     *
457
     * @return array|Constraint|TableSchema|null
458
     */
459 50
    protected function loadTableTypeMetadata(string $type, string $name): Constraint|array|TableSchema|null
460
    {
461 50
        return match ($type) {
462 50
            self::SCHEMA => $this->loadTableSchema($name),
463 13
            self::PRIMARY_KEY => $this->loadTablePrimaryKey($name),
464
            self::UNIQUES => $this->loadTableUniques($name),
465
            self::FOREIGN_KEYS => $this->loadTableForeignKeys($name),
466
            self::INDEXES => $this->loadTableIndexes($name),
467 50
            self::DEFAULT_VALUES => $this->loadTableDefaultValues($name),
468 50
            self::CHECKS => $this->loadTableChecks($name),
469
            default => null,
470 50
        };
471
    }
472
473
    /**
474
     * This method returns the desired metadata type for table name (with refresh if needed)
475
     *
476 12
     * @param string $type
477
     * @param string $name
478 12
     * @param bool $refresh
479
     *
480 12
     * @return array|Constraint|TableSchema|null
481
     */
482
    protected function getTableTypeMetadata(
483
        string $type,
484
        string $name,
485
        bool $refresh = false
486 375
    ): Constraint|array|null|TableSchema {
487
        return match ($type) {
488 375
            self::SCHEMA => $this->getTableSchema($name, $refresh),
489 375
            self::PRIMARY_KEY => $this->getTablePrimaryKey($name, $refresh),
490
            self::UNIQUES => $this->getTableUniques($name, $refresh),
491
            self::FOREIGN_KEYS => $this->getTableForeignKeys($name, $refresh),
492 375
            self::INDEXES => $this->getTableIndexes($name, $refresh),
493
            self::DEFAULT_VALUES => $this->getTableDefaultValues($name, $refresh),
494
            self::CHECKS => $this->getTableChecks($name, $refresh),
495
            default => null,
496
        };
497
    }
498 153
499
    /**
500 153
     * Resolves the table name and schema name (if any).
501
     *
502
     * @param string $name the table name.
503
     *
504
     * @throws NotSupportedException if this method is not supported by the DBMS.
505
     *
506
     * @return TableSchema with resolved table, schema, etc. names.
507
     *
508
     * {@see \Yiisoft\Db\Schema\TableSchema}
509
     */
510
    protected function resolveTableName(string $name): TableSchema
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

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

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...
511
    {
512
        throw new NotSupportedException(static::class . ' does not support resolving table names.');
513
    }
514 20
515
    /**
516 20
     * Sets the metadata of the given type for the given table.
517
     *
518
     * @param string $name table name.
519
     * @param string $type metadata type.
520
     * @param mixed $data metadata.
521
     */
522
    protected function setTableMetadata(string $name, string $type, mixed $data): void
523
    {
524
        $this->tableMetadata[$this->getRawTableName($name)][$type] = $data;
525
    }
526
527
    /**
528
     * Tries to load and populate table metadata from cache.
529
     *
530 141
     * @param string $rawName
531
     */
532 141
    private function loadTableMetadataFromCache(string $rawName): void
533
    {
534
        if (!$this->schemaCache->isEnabled() || $this->schemaCache->isExcluded($rawName)) {
535
            $this->tableMetadata[$rawName] = [];
536
            return;
537
        }
538
539
        $metadata = $this->schemaCache->getOrSet(
540
            $this->getCacheKey($rawName),
541
            null,
542
            $this->schemaCache->getDuration(),
543
            new TagDependency($this->getCacheTag()),
544
        );
545
546 156
        if (
547
            !is_array($metadata) ||
548 156
            !isset($metadata['cacheVersion']) ||
549
            $metadata['cacheVersion'] !== static::SCHEMA_CACHE_VERSION
550
        ) {
551
            $this->tableMetadata[$rawName] = [];
552
553
            return;
554
        }
555
556
        unset($metadata['cacheVersion']);
557
        $this->tableMetadata[$rawName] = $metadata;
558
    }
559
560
    /**
561
     * Saves table metadata to cache.
562 63
     *
563
     * @param string $rawName
564 63
     */
565
    private function saveTableMetadataToCache(string $rawName): void
566
    {
567
        if ($this->schemaCache->isEnabled() === false || $this->schemaCache->isExcluded($rawName) === true) {
568
            return;
569
        }
570
571
        $metadata = $this->tableMetadata[$rawName];
572
573
        $metadata['cacheVersion'] = static::SCHEMA_CACHE_VERSION;
574
575
        $this->schemaCache->set(
576
            $this->getCacheKey($rawName),
577
            $metadata,
578 61
            $this->schemaCache->getDuration(),
579
            new TagDependency($this->getCacheTag()),
580 61
        );
581
    }
582
}
583