SchemaDB::checkTableStructure()   B
last analyzed

Complexity

Conditions 9
Paths 10

Size

Total Lines 47
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 9
eloc 32
nc 10
nop 2
dl 0
loc 47
ccs 0
cts 31
cp 0
crap 90
rs 8.0555
c 3
b 0
f 0
1
<?php
2
/**
3
 * Alxarafe. Development of PHP applications in a flash!
4
 * Copyright (C) 2018-2020 Alxarafe <[email protected]>
5
 */
6
7
namespace Alxarafe\Core\Helpers;
8
9
use Alxarafe\Core\Helpers\Utils\ArrayUtils;
10
use Alxarafe\Core\Models\TableModel;
11
use Alxarafe\Core\Providers\Database;
12
use Alxarafe\Core\Providers\FlashMessages;
13
14
/**
15
 * The SchemaDB class contains static methods that allow you to manipulate the database. It is used to create and
16
 * modify tables and indexes in the database.
17
 */
18
class SchemaDB
19
{
20
    /**
21
     * Return the tables on the database.
22
     *
23
     * @return array
24
     */
25
    public static function getTables(): array
26
    {
27
        $queries = Database::getInstance()->getSqlHelper()->getTables();
28
        $queryResult = [];
29
        foreach ($queries as $query) {
30
            $queryResult[] = Database::getInstance()->getDbEngine()->selectCoreCache('tables', $query);
31
        }
32
        return ArrayUtils::flatArray($queryResult);
33
    }
34
35
    /**
36
     * Create or update the structure of the table.
37
     * It does not destroy fields of the structure, it only creates, adds or modifies the existing ones.
38
     * NOTE: It could happen that when modifying the structure of a field, information could be lost.
39
     *
40
     * @param string $tableName
41
     *
42
     * @return bool
43
     */
44
    public static function checkTableStructure(string $tableName, $tabla): bool
45
    {
46
        Database::getInstance()->getDbEngine()->clearCoreCache($tableName . '-exists');
47
48
        // $tabla = Database::getInstance()->getDbEngine()->getDbTableStructure($tableName);
49
        if (isset($tabla['indexes'])) {
50
            foreach ($tabla['indexes'] as $key => $index) {
51
                $autoincrement = ($tabla['fields'][$index['column']]['autoincrement'] ?? 'no') === 'yes';
52
                if ($autoincrement) {
53
                    $tabla['indexes'][$key]['autoincrement'] = 'yes';
54
                    $tabla['indexes'][$key]['length'] = $tabla['fields'][$index['column']]['length'];
55
                }
56
            }
57
        }
58
59
        $tableExists = self::tableExists($tableName);
60
        if ($tableExists) {
61
            $sql = [];
62
            $sql = ArrayUtils::addToArray($sql, self::updateFields($tableName, $tabla['fields'] ?? []));
63
            if (isset($tabla['indexes'])) {
64
                $sql = ArrayUtils::addToArray($sql, self::updateIndexes($tableName, $tabla['indexes']));
65
            }
66
        } else {
67
            $sql = self::createFields($tableName, $tabla['fields']);
68
            if (!Database::getInstance()->getDbEngine()->batchExec($sql)) {
69
                $data = [
70
                    'error' => "Maybe the file 'schema" . DIRECTORY_SEPARATOR . $tableName . ".yaml' is missing.",
71
                    'tableName' => $tableName,
72
                    'sql' => $sql,
73
                    'table fields' => $tabla,
74
                ];
75
                FlashMessages::getInstance()::setError('Error executing: <pre>' . var_export($data, true) . '</pre>');
76
                return false;
77
            }
78
79
            $sql = [];
80
            foreach ($tabla['indexes'] as $name => $index) {
81
                $sql = ArrayUtils::addToArray($sql, self::createIndex($tableName, $name, $index));
82
            }
83
            $values = $tabla['values'] ?? [];
84
            $values = ArrayUtils::addToArray($values, Schema::getFromYamlFile($tableName, 'values'));
85
            if (count($values) > 0) {
86
                $sql = ArrayUtils::addToArray($sql, Schema::setValues($tableName, $tabla, $values));
87
            }
88
            $sql = ArrayUtils::addToArray($sql, self::createTableView($tableName, $tabla));
89
        }
90
        return Database::getInstance()->getDbEngine()->batchExec($sql);
91
    }
92
93
    /**
94
     * Return true if $tableName exists in database
95
     *
96
     * @param string $tableName
97
     *
98
     * @return bool
99
     */
100 3
    public static function tableExists($tableName): bool
101
    {
102 3
        $sql = Database::getInstance()->getSqlHelper()->getSqlTableExists($tableName);
103 3
        return !empty(Database::getInstance()->getDbEngine()->selectCoreCache($tableName . '-exists', $sql));
104
    }
105
106
    /**
107
     * Update fields for tablename.
108
     *
109
     * @param string $tableName
110
     * @param array  $fieldsList
111
     *
112
     * @return array
113
     */
114
    protected static function updateFields(string $tableName, array $fieldsList): array
115
    {
116
        $fields = self::modifyFields($tableName, $fieldsList);
117
        if ($fields === '') {
118
            return [];
119
        }
120
        return ['ALTER TABLE ' . self::quoteTableName($tableName, true) . $fields . ';'];
121
    }
122
123
    /**
124
     * Modify (add or change) fields for tablename.
125
     *
126
     * @param string $tableName
127
     * @param array  $fieldsList
128
     *
129
     * @return string
130
     */
131
    private static function modifyFields(string $tableName, array $fieldsList): string
132
    {
133
        $tableFields = Database::getInstance()->getSqlHelper()->getColumns($tableName);
134
        $newFields = [];
135
        $modifiedFields = [];
136
        foreach ($fieldsList as $key => $fields) {
137
            unset($fields['key'], $tableFields[$key]['key']);
138
            if (!isset($tableFields[$key])) {
139
                $newFields[$key] = $fields;
140
            } elseif (count(array_diff($fields, $tableFields[$key])) > 0) {
141
                $modifiedFields[$key] = $fields;
142
            }
143
        }
144
        $sql1 = self::assignFields($modifiedFields, 'MODIFY COLUMN');
145
        $sql2 = self::assignFields($newFields, 'ADD COLUMN');
146
147
        return ($sql1 === '') ? $sql2 : $sql1 . ($sql2 === '' ? '' : ',' . $sql2);
148
    }
149
150
    /**
151
     * Convert an array of fields into a string to be added to an SQL command, CREATE TABLE or ALTER TABLE.
152
     * You can add a prefix field operation (usually ADD or MODIFY) that will be added at begin of each field.
153
     *
154
     * @param array  $fieldsList
155
     * @param string $fieldOperation (usually ADD, MODIFY or empty string)
156
     *
157
     * @return string
158
     */
159
    protected static function assignFields(array $fieldsList, string $fieldOperation = ''): string
160
    {
161
        $fields = [];
162
        foreach ($fieldsList as $index => $col) {
163
            $field = Database::getInstance()->getSqlHelper()->getSQLField($index, $col);
164
            if ($field !== '') {
165
                $fields[] = ' ' . trim($fieldOperation . ' ' . $field);
166
            }
167
        }
168
        return implode(', ', $fields);
169
    }
170
171
    /**
172
     * Returns the name of the table in quotes.
173
     *
174
     * @param string $tableName
175
     * @param bool   $usePrefix
176
     *
177
     * @return string
178
     */
179
    public static function quoteTableName($tableName, bool $usePrefix = true): string
180
    {
181
        return Database::getInstance()->getSqlHelper()->quoteTableName($tableName, $usePrefix);
182
    }
183
184
    /**
185
     * Update indexes for tablename.
186
     *
187
     * @param string $tableName
188
     * @param array  $indexesList
189
     *
190
     * @return array
191
     */
192
    protected static function updateIndexes(string $tableName, array $indexesList): array
193
    {
194
        $sql = [];
195
196
        $tableIndexes = Database::getInstance()->getSqlHelper()->getIndexes($tableName);
197
        $quotedTableName = self::quoteTableName($tableName, true);
198
199
        // Erase the deleted or modified indexes
200
        foreach ($tableIndexes as $key => $value) {
201
            if ($key === 'PRIMARY') {
202
                // If deleted of YAML, delete it...
203
                if (!isset($indexesList[$key])) {
204
                    $sql = ArrayUtils::addToArray($sql, ["ALTER TABLE {$quotedTableName} DROP PRIMARY KEY;"]);
205
                    continue;
206
                }
207
                if ($value != $indexesList[$key]) {
208
                    $autoincrement = isset($indexesList[$key]['autoincrement']) && $indexesList[$key]['autoincrement'] === 'yes';
209
                    $sql = ArrayUtils::addToArray($sql, self::createPrimaryIndex($tableName, $indexesList[$key], $autoincrement, true));
210
                }
211
                continue;
212
            }
213
214
            if (!isset($indexesList[$key])) {
215
                if (isset($value['constraint']) && $value['constraint'] === 'yes') {
216
                    $sql = ArrayUtils::addToArray($sql, ["ALTER TABLE {$quotedTableName} DROP FOREIGN KEY {$key};"]);
217
                }
218
                $sql = ArrayUtils::addToArray($sql, ["ALTER TABLE {$quotedTableName} DROP INDEX {$key};"]);
219
            }
220
        }
221
222
        // Create the missing indexes
223
        foreach ($indexesList as $key => $value) {
224
            if ($key === 'PRIMARY') {
225
                if (isset($tableIndexes[$key])) {
226
                    continue;
227
                }
228
                $autoincrement = isset($value['autoincrement']) && $value['autoincrement'] === 'yes';
229
                $sql = ArrayUtils::addToArray($sql, self::createPrimaryIndex($tableName, $value, $autoincrement, false));
230
                continue;
231
            }
232
            $value['index'] = $key;
233
            $exists = isset($tableIndexes[$key]);
234
235
            if (isset($value['constraint']) && $value['constraint'] === 'yes') {
236
                $sql = ArrayUtils::addToArray($sql, ["ALTER TABLE {$quotedTableName} DROP CONSTRAINT {$key};"]);
237
                $sql = ArrayUtils::addToArray($sql, self::createConstraint($tableName, $value, $exists));
238
            } else {
239
                if (isset($value['unique']) && $value['unique'] === 'yes') {
240
                    $sql = ArrayUtils::addToArray($sql, self::createUniqueIndex($tableName, $value, $exists));
241
                } else {
242
                    $sql = ArrayUtils::addToArray($sql, self::createStandardIndex($tableName, $value, $exists));
243
                }
244
            }
245
        }
246
247
        return $sql;
248
    }
249
250
    /**
251
     * Creates index for primary key of tablename.
252
     *
253
     * @param string $tableName
254
     * @param array  $indexData
255
     * @param bool   $autoincrement
256
     * @param bool   $exists
257
     *
258
     * @return array
259
     */
260
    protected static function createPrimaryIndex(string $tableName, array $indexData, bool $autoincrement, bool $exists = false): array
261
    {
262
        // https://www.w3schools.com/sql/sql_primarykey.asp
263
        // ALTER TABLE Persons ADD CONSTRAINT PK_Person PRIMARY KEY (ID,LastName);
264
        // 'ADD PRIMARY KEY ('id') AUTO_INCREMENT' is specific of MySQL?
265
        // ALTER TABLE t2 ADD c INT UNSIGNED NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY (c);
266
        //
267
        // TODO: Check dependencies of MySQL
268
        $quotedTableName = self::quoteTableName($tableName, true);
269
        $columnField = self::quoteFieldName($indexData['column']);
270
        $sql = [];
271
        if ($exists) {
272
            $sql[] = "ALTER TABLE {$quotedTableName} DROP PRIMARY KEY;";
273
        }
274
        $sql[] = "ALTER TABLE {$quotedTableName} ADD PRIMARY KEY ({$columnField});";
275
        if ($autoincrement) {
276
            $type = Database::getInstance()->getSqlHelper()->toNative('integer', $indexData['length']);
0 ignored issues
show
Bug introduced by
The method toNative() does not exist on Alxarafe\Core\Database\SqlHelper. It seems like you code against a sub-type of Alxarafe\Core\Database\SqlHelper such as Alxarafe\Core\Database\SqlHelpers\SqlMySql. ( Ignorable by Annotation )

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

276
            $type = Database::getInstance()->getSqlHelper()->/** @scrutinizer ignore-call */ toNative('integer', $indexData['length']);
Loading history...
277
            $sql[] = "ALTER TABLE {$quotedTableName} MODIFY {$columnField} $type UNSIGNED AUTO_INCREMENT";
278
        }
279
        return $sql;
280
    }
281
282
    /**
283
     * Returns the name of the field in quotes.
284
     *
285
     * @param string $fieldName
286
     *
287
     * @return string
288
     */
289
    public static function quoteFieldName($fieldName): string
290
    {
291
        return Database::getInstance()->getSqlHelper()->quoteFieldName($fieldName);
292
    }
293
294
    /**
295
     * Creates a constraint for tablename.
296
     *
297
     * @param string $tableName
298
     * @param array  $indexData
299
     * @param bool   $exists
300
     *
301
     * @return array
302
     */
303
    protected static function createConstraint(string $tableName, array $indexData, bool $exists = false): array
304
    {
305
        // https://www.w3schools.com/sql/sql_foreignkey.asp
306
        // ALTER TABLE Orders ADD CONSTRAINT FK_PersonOrder FOREIGN KEY (PersonID) REFERENCES Persons(PersonID);
307
308
        $referencedTableWithoutPrefix = $indexData['referencedtable'];
309
        $referencedTable = Database::getInstance()->getConnectionData()['dbPrefix'] . $referencedTableWithoutPrefix;
310
311
        $sql = [];
312
        $quotedTableName = self::quoteTableName($tableName, true);
313
        $indexName = self::quoteFieldName($indexData['index']);
314
        $columnName = self::quoteFieldName($indexData['column']);
315
        $referencedTableName = self::quoteFieldName($referencedTable);
316
        $refencedFieldName = self::quoteFieldName($indexData['referencedfield']);
317
        if ($exists && ($indexData['deleterule'] === '' || $indexData['updaterule'] === '')) {
318
            $sql[] = "ALTER TABLE {$quotedTableName} DROP FOREIGN KEY {$indexName};";
319
        }
320
321
        // Delete (if exists) and create the index related to the constraint
322
        $sql = ArrayUtils::addToArray($sql, self::createStandardIndex($tableName, $indexData, $exists));
323
324
        $query = "ALTER TABLE {$quotedTableName} ADD CONSTRAINT {$indexName} FOREIGN KEY ({$columnName}) REFERENCES {$referencedTableName} ({$refencedFieldName})";
325
326
        if ($indexData['deleterule'] !== '') {
327
            $query .= ' ON DELETE ' . $indexData['deleterule'];
328
        }
329
330
        if ($indexData['updaterule'] !== '') {
331
            $query .= ' ON UPDATE ' . $indexData['updaterule'] . ';';
332
        }
333
334
        $sql[] = $query;
335
336
        return $sql;
337
    }
338
339
    /**
340
     * Creates a standard index for tablename.
341
     *
342
     * @param string $tableName
343
     * @param array  $indexData
344
     * @param bool   $exists
345
     *
346
     * @return array
347
     */
348
    protected static function createStandardIndex(string $tableName, array $indexData, bool $exists = false): array
349
    {
350
        // https://www.w3schools.com/sql/sql_create_index.asp
351
        // CREATE INDEX idx_pname ON Persons (LastName, FirstName);
352
        // CREATE UNIQUE INDEX idx_pname ON Persons (LastName, FirstName);
353
        $sql = [];
354
        $quotedTableName = self::quoteTableName($tableName, true);
355
        $indexName = self::quoteFieldName($indexData['index']);
356
        $indexColumn = self::quoteFieldName($indexData['column']);
357
        if ($exists) {
358
            $sql[] = "ALTER TABLE {$quotedTableName} DROP INDEX {$indexName};";
359
        }
360
        $sql[] = "CREATE INDEX {$indexName} ON {$quotedTableName} ({$indexColumn});";
361
        return $sql;
362
    }
363
364
    /**
365
     * Creates a unique index for the tablename.
366
     *
367
     * @param string $tableName
368
     * @param array  $indexData
369
     * @param bool   $exists
370
     *
371
     * @return array
372
     */
373
    protected static function createUniqueIndex(string $tableName, array $indexData, bool $exists = false): array
374
    {
375
        // https://www.w3schools.com/sql/sql_unique.asp
376
        // ALTER TABLE Persons ADD CONSTRAINT UC_Person UNIQUE (ID,LastName);
377
        $columnsArray = explode(',', $indexData['column']);
378
        foreach ($columnsArray as $key => $column) {
379
            $columnsArray[$key] = self::quoteFieldName($column);
380
        }
381
        $columns = implode(',', $columnsArray);
382
383
        $sql = [];
384
        $quotedTableName = self::quoteTableName($tableName, true);
385
        $indexName = self::quoteFieldName($indexData['index']);
386
        if ($exists) {
387
            $sql[] = "ALTER TABLE {$quotedTableName} DROP INDEX {$indexName};";
388
        }
389
        $sql[] = "ALTER TABLE {$quotedTableName} ADD CONSTRAINT {$indexName} UNIQUE ({$columns})";
390
        return $sql;
391
    }
392
393
    /**
394
     * Build the SQL statement to create the fields in the table.
395
     * It can also create the primary key if the auto_increment attribute is defined.
396
     *
397
     * @param string $tableName
398
     * @param array  $fieldsList
399
     *
400
     * @return array
401
     */
402
    protected static function createFields(string $tableName, array $fieldsList): array
403
    {
404
        // If the table does not exists
405
        $sql = 'CREATE TABLE ' . self::quoteTableName($tableName, true) . ' ('
406
            . self::assignFields($fieldsList)
407
            . ') ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;';
408
409
        return [$sql];
410
    }
411
412
    /**
413
     * Create the SQL statements for the construction of one index.
414
     * In the case of the primary index, it is not necessary if it is auto_increment.
415
     *
416
     * TODO:
417
     *
418
     * Moreover, it should not be defined if it is auto_increment because it would
419
     * generate an error when it already exists.
420
     *
421
     * @param string $tableName
422
     * @param string $indexName
423
     * @param array  $indexData
424
     *
425
     * @return array
426
     */
427
    protected static function createIndex(string $tableName, string $indexName, array $indexData): array
428
    {
429
        $tableIndexes = Database::getInstance()->getSqlHelper()->getIndexes($tableName);
430
        $tableIndex = $tableIndexes[$indexName] ?? [];
431
        $indexDiff = array_diff($indexData, $tableIndex);
432
        $existsIndex = isset($tableIndexes[$indexName]);
433
        $changedIndex = (count($indexDiff) > 0);
434
        if (!$changedIndex) {
435
            return [];
436
        }
437
438
        $indexData['index'] = $indexName;
439
440
        if ($indexName === 'PRIMARY') {
441
            $autoincrement = (($indexData['autoincrement'] ?? 'no') === 'yes');
442
            return self::createPrimaryIndex($tableName, $indexData, $autoincrement, $existsIndex);
443
        }
444
445
        $unique = isset($indexData['unique']) && ($indexData['unique'] === 'yes');
446
        //$nullable = isset($indexData['nullable']) && ($indexData['nullable'] == 'yes');
447
        $constraint = $indexData['constraint'] ?? false;
448
449
        if ($constraint) {
450
            return self::createConstraint($tableName, $indexData, $existsIndex);
451
        }
452
453
        return $unique ?
454
            self::createUniqueIndex($tableName, $indexData, $existsIndex) :
455
            self::createStandardIndex($tableName, $indexData, $existsIndex);
456
    }
457
458
    /**
459
     * Create a tableView
460
     *
461
     * @param string $tableName
462
     *
463
     * @return array
464
     */
465
    protected static function createTableView(string $tableName, array $table): array
466
    {
467
        $primaryColumn = [];
468
        $nameColumn = [];
469
        // $tabla = Database::getInstance()->getDbEngine()->getDbTableStructure($tableName);
470
        $fields = $table['fields'];
471
        $indexes = $table['indexes'];
472
473
        // Ignore indexes that aren't constraints
474
        foreach ($indexes as $indexName => $indexData) {
475
            if (isset($indexData['constraint'])) {
476
                $refTable = (new TableModel())->get($indexData['referencedtable']);
477
                $newClass = $refTable->namespace;
478
                if (!empty($newClass)) {
479
                    $class = new $newClass();
480
                    // $tableNameIndex = $refTable->tablename;
481
                    // $tableIndex[$indexName] = Database::getInstance()->getDbEngine()->getDbTableStructure($tableNameIndex);
482
                    $primaryColumn[$indexName] = $table['indexes']['PRIMARY']['column'];
483
                    $nameColumn[$indexName] = $class->getNameField();
484
                } else {
485
                    // throw new RuntimeException(
486
                    //     "Model class for table '" . $indexData['referencedtable'] . "' not loaded. Do you forgot to add 'getDependencies()' to model for '" . $tableName . "' table'."
487
                    // );
488
                }
489
            } else {
490
                unset($indexes[$indexName]);
491
            }
492
        }
493
        // If no indexes for constraints, we don't need a related view
494
        if (empty($indexes)) {
495
            return [];
496
        }
497
498
        $quotedTableName = self::quoteTableName($tableName, true);
499
        $quotedViewTableName = self::quoteTableName('view_' . $tableName, true);
500
        $sqlView = "CREATE OR REPLACE VIEW {$quotedViewTableName} AS SELECT ";
501
        $sep = '';
502
        foreach ($fields as $fieldName => $fieldData) {
503
            if (!is_null($fieldName)) {
504
                $sqlView .= "{$sep}{$quotedTableName}." . self::quoteFieldName($fieldName);
505
                $sep = ', ';
506
            }
507
        }
508
        foreach ($indexes as $indexName => $indexData) {
509
            if (!is_null($nameColumn[$indexName])) {
510
                $sqlView .= $sep . self::quoteTableName($indexData['referencedtable'], true) . '.' . self::quoteFieldName($nameColumn[$indexName])
511
                    . " AS {$indexData['referencedtable']}_{$nameColumn[$indexName]}";
512
                $sep = ', ';
513
            }
514
        }
515
        $sqlView .= " FROM {$quotedTableName}";
516
        foreach ($indexes as $indexName => $indexData) {
517
            if (!is_null($indexData['column']) && !is_null($primaryColumn[$indexName])) {
518
                $sqlView .= ' LEFT JOIN ' . self::quoteTableName($indexData['referencedtable'], true)
519
                    . " ON {$quotedTableName}." . self::quoteFieldName($indexData['column']) . ' = '
520
                    . self::quoteTableName($indexData['referencedtable'], true) . '.' . self::quoteFieldName($primaryColumn[$indexName]);
521
            }
522
        }
523
        $sqlView .= ';';
524
        return [$sqlView];
525
    }
526
}
527