Test Failed
Push — main ( a8a1f8...d105a1 )
by Rafael
11:47
created

Schema::normalize()   F

Complexity

Conditions 24
Paths > 20000

Size

Total Lines 157
Code Lines 84

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 84
nc 41856
nop 1
dl 0
loc 157
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Copyright (C) 2022-2023  Rafael San José Tovar   <[email protected]>
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License
16
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
 */
18
19
namespace Alxarafe\Database;
20
21
use Alxarafe\Core\Singletons\Config;
22
use Alxarafe\Core\Singletons\Debug;
23
use Alxarafe\Core\Singletons\PhpFileCache;
24
use DebugBar\DebugBarException;
25
use Symfony\Component\Yaml\Yaml;
26
use function Alxarafe\Core\Helpers\count;
0 ignored issues
show
introduced by
The function Alxarafe\Core\Helpers\count was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
27
use const Alxarafe\Core\Helpers\DEFAULT_INTEGER_SIZE;
0 ignored issues
show
Bug introduced by
The constant Alxarafe\Core\Helpers\DEFAULT_INTEGER_SIZE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
28
use const Alxarafe\Core\Helpers\DEFAULT_STRING_LENGTH;
0 ignored issues
show
Bug introduced by
The constant Alxarafe\Core\Helpers\DEFAULT_STRING_LENGTH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
29
30
/**
31
 * The Schema class contains static methods that allow you to manipulate the
32
 * database. It is used to create and modify tables and indexes in the database.
33
 */
34
class Schema
35
{
36
    public const TYPE_INTEGER = 'integer';
37
    public const TYPE_FLOAT = 'float';
38
    public const TYPE_DECIMAL = 'decimal';
39
    public const TYPE_STRING = 'string';
40
    public const TYPE_TEXT = 'text';
41
    public const TYPE_DATE = 'date';
42
    public const TYPE_TIME = 'time';
43
    public const TYPE_DATETIME = 'datetime';
44
    public const TYPE_BOOLEAN = 'bool';
45
46
    public const TYPES = [
47
        self::TYPE_INTEGER => ['tinyint', 'smallint', 'mediumint', 'int', 'bigint'],
48
        self::TYPE_FLOAT => ['real', 'double'],
49
        self::TYPE_DECIMAL => ['decimal', 'numeric'],
50
        self::TYPE_STRING => ['char', 'varchar'],
51
        self::TYPE_TEXT => ['tinytext', 'text', 'mediumtext', 'longtext', 'blob'],
52
        self::TYPE_DATE => ['date'],
53
        self::TYPE_TIME => ['time'],
54
        self::TYPE_DATETIME => ['datetime', 'timestamp'],
55
        self::TYPE_BOOLEAN => ['boolean'],
56
    ];
57
    public const YAML_CACHE_TABLES_FOLDER = 'models';
58
59
    /**
60
     * Carriage Return and Line Feed
61
     */
62
    const CRLF = "\r\n";
63
    const DB_INDEX_TYPE = 'bigint (20) unsigned';
64
65
    public static array $tables = [];
66
67
    /**
68
     * Contains the database structure data.
69
     * Each table is an index of the associative array.
70
     *
71
     * @var array
72
     */
73
    public static array $bbddStructure;
74
75
    /**
76
     * Return true if $tableName exists in database
77
     *
78
     * @param string $tableName
79
     *
80
     * @return bool
81
     * @throws DebugBarException
82
     */
83
    public static function tableExists($tableName): bool
84
    {
85
        $tableNameWithPrefix = Config::$dbPrefix . $tableName;
86
        $dbName = Config::$dbName;
87
        $sql = "SELECT COUNT(*) AS Total FROM information_schema.tables WHERE table_schema = '{$dbName}' AND table_name='{$tableNameWithPrefix}'";
88
89
        $data = Engine::select($sql);
90
        $result = reset($data);
91
92
        return $result['Total'] === '1';
93
    }
94
95
    private static function getFields($tableName): array
96
    {
97
        $yamlFilename = PhpFileCache::getYamlFileName(self::YAML_CACHE_TABLES_FOLDER, $tableName);
98
        if (file_exists($yamlFilename)) {
99
            return PhpFileCache::loadYamlFile(self::YAML_CACHE_TABLES_FOLDER, $tableName);
100
        }
101
102
        if (empty(self::$tables)) {
103
            self::$tables = YamlSchema::getTables();
104
        }
105
106
        $yamlSourceFilename = self::$tables[$tableName];
107
        if (!file_exists($yamlSourceFilename)) {
108
            dump('No existe el archivo ' . $yamlSourceFilename);
109
        }
110
111
        $data = Yaml::parseFile($yamlSourceFilename);
112
113
        $result = [];
114
        foreach ($data as $key => $datum) {
115
            $datum['key'] = $key;
116
            $result[$key] = Schema::normalize($datum);
117
        }
118
119
        /*
120
        Igual conviene crear una clase:
121
        - DBSchema (con los datos de la base de datos real)
122
        - DefinedSchema (con los datos definidos)
123
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
124
125
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
126
    oportunas siempre que no suponga una pérdida de datos.
127
        */
128
129
        return $result;
130
    }
131
132
    private static function getIndexes($tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName 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

132
    private static function getIndexes(/** @scrutinizer ignore-unused */ $tableName): 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...
133
    {
134
        $result = [];
135
        return $result;
136
    }
137
138
    private static function getRelated($tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName 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

138
    private static function getRelated(/** @scrutinizer ignore-unused */ $tableName): 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...
139
    {
140
        $result = [];
141
        return $result;
142
    }
143
144
    private static function getSeed($tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName 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

144
    private static function getSeed(/** @scrutinizer ignore-unused */ $tableName): 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...
145
    {
146
        $result = [];
147
        return $result;
148
    }
149
150
    public static function checkStructure(string $tableName, bool $create)
151
    {
152
        if (!empty(self::$bbddStructure[$tableName])) {
153
            return;
154
        }
155
156
        $structure = [];
157
        $structure['fields'] = self::getFields($tableName);
158
        $structure['indexes'] = self::getIndexes($tableName);
159
        $structure['related'] = self::getRelated($tableName);
160
        if ($create) {
161
            $structure['seed'] = self::getSeed($tableName);
162
        }
163
        self::$bbddStructure[$tableName] = $structure;
164
    }
165
166
    /**
167
     * Normalize an array that has the file structure defined in the model by setStructure,
168
     * so that it has fields with all the values it must have. Those that do not exist are
169
     * created with the default value, avoiding having to do the check each time, or
170
     * calculating their value based on the data provided by the other fields.
171
     *
172
     * It also ensures that the keys and default values exist as an empty array if they
173
     * did not exist.
174
     *
175
     * @param array  $structure
176
     * @param string $tableName
177
     *
178
     * @return array
179
     */
180
    public static function setNormalizedStructure(array $structure, string $tableName): array
181
    {
182
        $ret['keys'] = $structure['keys'] ?? [];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$ret was never initialized. Although not strictly required by PHP, it is generally a good practice to add $ret = array(); before regardless.
Loading history...
183
        $ret['values'] = $structure['values'] ?? [];
184
        foreach ($structure['fields'] as $key => $value) {
185
            $ret['fields'][$key] = self::normalizeField($tableName, $key, $value);
186
        }
187
        return $ret;
188
    }
189
190
    /**
191
     * Take the definition of a field, and make sure you have all the information
192
     * that is necessary for its creation or maintenance, calculating the missing
193
     * data if possible.
194
     * It can cause an exception if some vital data is missing, but this should
195
     * only occur at the design stage.
196
     *
197
     * @param string $tableName
198
     * @param string $field
199
     * @param array  $structure
200
     *
201
     * @return array
202
     */
203
    protected static function normalizeField(string $tableName, string $field, array $structure): array
204
    {
205
        if (!isset($structure['type'])) {
206
            dump("The type parameter is mandatory in {$field}. Error in table " . $tableName);
207
            dump($structure);
208
        }
209
210
        $dbType = $structure['type'];
211
212
        if ($dbType == 'boolean') {
213
            $dbType = 'tinyint';
214
            $structure['min'] = 0;
215
            $structure['max'] = 1;
216
        }
217
218
        if ($dbType == 'int' || $dbType == 'tinyint' || $dbType == 'number') {
219
            $type = 'number';
220
        } else {
221
            if ($dbType == 'float') {
222
                $type = 'float';
223
            } else {
224
                if ($dbType == 'double') {
225
                    $type = 'double';
226
                } else {
227
                    if ($dbType == 'char' || $dbType == 'varchar' || $dbType == 'text') {
228
                        $type = 'text';
229
                    } else {
230
                        if ($dbType == 'date') {
231
                            $type = 'date';
232
                        } else {
233
                            if ($dbType == 'datetime' || $dbType == 'timestamp') {
234
                                $type = 'datetime-local';
235
                            } else {
236
                                echo "<p>Check Schema.normalizeField if you think that {$dbType} might be necessary.</p>";
237
                                die("Type {$dbType} is not valid for field {$field} of table {$tableName}");
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return array. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
238
                            }
239
                        }
240
                    }
241
                }
242
            }
243
        }
244
245
        $min = (isset($structure['min'])) ? $structure['min'] : 0;
246
        $max = (isset($structure['max'])) ? $structure['max'] : 0;
247
        $default = (isset($structure['default'])) ? $structure['default'] : null;
248
        $label = (isset($structure['label'])) ? $structure['label'] : $field;
249
        $unsigned = (!isset($structure['unsigned']) || $structure['unsigned'] == true);
250
        $null = ((isset($structure['null'])) && $structure['null'] == true);
251
252
        $ret = [];
253
        if ($type == 'text') {
254
            if ($max == 0) {
255
                $max = DEFAULT_STRING_LENGTH;
256
            }
257
            $dbType = "$dbType($max)";
258
            $ret['pattern'] = '.{' . $min . ',' . $max . '}';
259
        } else {
260
            if ($type == 'number') {
261
                if ($default === true) {
262
                    $default = '1';
263
                }
264
                if ($max == 0) {
265
                    $tmpLength = DEFAULT_INTEGER_SIZE;
266
                    $max = pow(10, $tmpLength) - 1;
267
                } else {
268
                    $tmpLength = strlen($max);
269
                }
270
271
                if ($min == 0) {
272
                    $min = $unsigned ? 0 : -$max;
273
                } else {
274
                    if ($tmpLength < strlen($min)) {
275
                        $tmpLength = strlen($min);
276
                    }
277
                }
278
279
                if (isset($structure['decimals'])) {
280
                    $decimales = $structure['decimals'];
281
                    $precision = pow(10, -$decimales);
282
                    $tmpLength += $decimales;
283
                    $dbType = "decimal($tmpLength,$decimales)" . ($unsigned ? ' unsigned' : '');
284
                    $ret['min'] = $min == 0 ? 0 : ($min < 0 ? $min - 1 + $precision : $min + 1 - $precision);
285
                    $ret['max'] = $max > 0 ? $max + 1 - $precision : $max - 1 + $precision;
286
                } else {
287
                    $precision = null;
288
                    $dbType = "integer($tmpLength)" . ($unsigned ? ' unsigned' : '');
289
                    $ret['min'] = $min;
290
                    $ret['max'] = $max;
291
                }
292
            }
293
        }
294
295
        $ret['type'] = $type;
296
        $ret['dbtype'] = $dbType;
297
        $ret['default'] = $default;
298
        $ret['null'] = $null;
299
        $ret['label'] = $label;
300
        if (isset($precision)) {
301
            $ret['step'] = $precision;
302
        }
303
        if (isset($structure['key'])) {
304
            $ret['key'] = $structure['key'];
305
        }
306
        if (isset($structure['placeholder'])) {
307
            $ret['placeholder'] = $structure['placeholder'];
308
        }
309
        if (isset($structure['extra'])) {
310
            $ret['extra'] = $structure['extra'];
311
        }
312
        if (isset($structure['help'])) {
313
            $ret['help'] = $structure['help'];
314
        }
315
        if (isset($structure['unique']) && $structure['unique']) {
316
            $ret['unique'] = $structure['unique'];
317
        }
318
319
        if (isset($structure['relations'][$field]['table'])) {
320
            $ret['relation'] = [
321
                'table' => $structure['relations'][$field]['table'],
322
                'id' => isset($structure['relations'][$field]['id']) ? $structure['relations'][$field]['id'] : 'id',
323
                'name' => isset($structure['relations'][$field]['name']) ? $structure['relations'][$field]['name'] : 'name',
324
            ];
325
        }
326
327
        return $ret;
328
    }
329
330
    private static function getTypeOf(string $type): string
331
    {
332
        foreach (self::TYPES as $index => $types) {
333
            if (in_array(strtolower($type), $types)) {
334
                return $index;
335
            }
336
        }
337
        Debug::addMessage('messages', $type . ' not found in DBSchema::getTypeOf()');
338
        return 'text';
339
    }
340
341
    private static function splitType(string $originalType): array
342
    {
343
        $replacesSources = [
344
            'character varying',
345
            // 'timestamp without time zone',
346
            'double precision',
347
        ];
348
        $replacesDestination = [
349
            'varchar',
350
            // 'timestamp',
351
            'double',
352
        ];
353
        $modifiedType = (str_replace($replacesSources, $replacesDestination, $originalType));
354
355
        if ($originalType !== $modifiedType) {
356
            Debug::addMessage('messages', "XML: Uso de '{$originalType}' en lugar de '{$modifiedType}'.");
357
        }
358
        $explode = explode(' ', strtolower($modifiedType));
359
360
        $pos = strpos($explode[0], '(');
361
        if ($pos > 0) {
362
            $begin = $pos + 1;
363
            $end = strpos($explode[0], ')');
364
            $type = substr($explode[0], 0, $pos);
365
            $length = substr($explode[0], $begin, $end - $begin);
366
        } else {
367
            $type = $explode[0];
368
            $length = null;
369
        }
370
371
        $pos = array_search('unsigned', $explode, true);
372
        $unsigned = $pos ? 'yes' : 'no';
373
374
        $pos = array_search('zerofill', $explode, true);
375
        $zerofill = $pos ? 'yes' : 'no';
376
377
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
378
    }
379
380
    /**
381
     * Toma los datos del fichero de definición de una tabla y genera el definitivo.
382
     *
383
     * @author  Rafael San José Tovar <[email protected]>
384
     * @version 2022.1224
385
     *
386
     * @param array $structure
387
     *
388
     * @return array
389
     */
390
    protected static function normalize(array $structure): array
391
    {
392
        $column = [];
393
        $key = (string) $structure['key'];
394
        $type = (string) $structure['type'];
395
        $column['key'] = $key;
396
397
        /**
398
         * Entrada:
399
         * - type es el tipo lógico del campo y tiene que estar definido como índice en
400
         *   TYPES, o ser uno de los predefinidos como 'autoincrement', 'relationship', etc.
401
         *
402
         * Salida:
403
         * - type queda intacto.
404
         * - dbtype es como queda definido en la tabla, por ejemplo, varchar(20)
405
         * - realtype es el tipo resultado, por ejemplo varchar (sin el tamaño)
406
         * - generictype es uno de los índices de TYPE. P.E. autoincrement se cambiará por integer
407
         *
408
         */
409
410
        $column['type'] = $type;
411
        switch ($type) {
412
            case 'autoincrement':
413
            case 'relationship':
414
                $colType = self::DB_INDEX_TYPE;
415
                break;
416
            case 'boolean':
417
                $colType = 'tinyint(1) unsigned';
418
                break;
419
            default:
420
                $colType = $type;
421
        }
422
423
        $typeArray = static::splitType($colType);
424
        /**
425
         * ^ array:4 [▼
426
         *        "type" => "bigint"
427
         *        "length" => null
428
         *        "unsigned" => "yes"
429
         *        "zerofill" => "no"
430
         * ]
431
         */
432
        $type = $typeArray['type'];
433
        $length = $typeArray['length'] ?? $structure['length'];
434
        $unsigned = $typeArray['unsigned'] === 'yes';
435
        $zerofill = $typeArray['zerofill'] === 'yes';
0 ignored issues
show
Unused Code introduced by
The assignment to $zerofill is dead and can be removed.
Loading history...
436
        $genericType = static::getTypeOf($type);
437
438
        $column['dbtype'] = $colType;
439
        $column['realtype'] = $type;
440
        $column['generictype'] = $genericType;
441
442
        $column['null'] = 'YES';
443
        if ($structure['null'] && mb_strtolower($structure['null']) == 'no') {
444
            $column['null'] = 'NO';
445
        }
446
447
        if (empty($structure['default'])) {
448
            $column['default'] = null;
449
        } else {
450
            $column['default'] = (string) $structure['default'];
451
        }
452
453
        /**
454
         * Pueden existir otras definiciones de limitaciones físicas como min y max
455
         * De existir, tienen que ser contempladas en el método test y tener mayor peso que
456
         * la limitación en plantilla.
457
         */
458
        foreach (['min', 'max'] as $field) {
459
            if (isset($structure[$field])) {
460
                $column[$field] = (string) $structure[$field];
461
            }
462
        }
463
464
        if (isset($structure['comment'])) {
465
            $column['comentario'] = (string) $structure['comment'];
466
        }
467
468
        if (isset($structure['default'])) {
469
            $column['default'] = trim($structure['default'], " \"'`");
470
        }
471
472
        switch ($genericType) {
473
            case 'text':
474
                $column['dbtype'] = 'varchar(' . $length . ')';
475
                $column['maxlength'] = $length;
476
                break;
477
            case 'integer':
478
                /**
479
                 * Lo primero es ver la capacidad física máxima según el tipo de dato.
480
                 */
481
                $bytes = 4;
482
                switch ($type) {
483
                    case 'tinyint':
484
                        $bytes = 1;
485
                        break;
486
                    case 'smallint':
487
                        $bytes = 2;
488
                        break;
489
                    case 'mediumint':
490
                        $bytes = 3;
491
                        break;
492
                    case 'int':
493
                        $bytes = 4;
494
                        break;
495
                    case 'bigint':
496
                        $bytes = 8;
497
                        break;
498
                }
499
                $bits = 8 * (int) $bytes;
500
                $physicalMaxLength = 2 ** $bits;
501
502
                /**
503
                 * $minDataLength y $maxDataLength contendrán el mínimo y máximo valor que puede contener el campo.
504
                 */
505
                $minDataLength = $unsigned ? 0 : -$physicalMaxLength / 2;
506
                $maxDataLength = ($unsigned ? $physicalMaxLength : $physicalMaxLength / 2) - 1;
507
508
                /**
509
                 * De momento, se asignan los límites máximos por el tipo de dato.
510
                 * En $min y $max, iremos arrastrando los límites conforme se vayan comprobando.
511
                 * $min nunca podrá ser menor que $minDataLength.
512
                 * $max nunca podrá ser mayor que $maxDataLength.
513
                 */
514
                $min = $minDataLength;
515
                $max = $maxDataLength;
516
517
                /**
518
                 * Se puede hacer una limitación física Se puede haber definido en el xml un min y un max.
519
                 * A todos los efectos, lo definido en el XML como min o max se toma como limitación
520
                 * física del campo.
521
                 */
522
                if (isset($structure['min'])) {
523
                    $minXmlLength = $structure['min'];
524
                    if ($minXmlLength > $minDataLength) {
525
                        $min = $minXmlLength;
526
                    } else {
527
                        Debug::addMessage('messages', "({$key}): Se ha especificado un min {$minXmlLength} en el XML, pero por el tipo de datos, el mínimo es {$minDataLength}.");
528
                    }
529
                }
530
                if (isset($structure['max'])) {
531
                    $maxXmlLength = $structure['max'];
532
                    if ($maxXmlLength < $maxDataLength) {
533
                        $max = $maxXmlLength;
534
                    } else {
535
                        Debug::addMessage('messages', "({$key}): Se ha especificado un min {$maxXmlLength} en el XML, pero por el tipo de datos, el máximo es {$maxDataLength}.");
536
                    }
537
                }
538
539
                $column['min'] = $min;
540
                $column['max'] = $max;
541
                break;
542
            default:
543
                // ???
544
        }
545
546
        return $column;
547
    }
548
549
    /**
550
     * Create a table in the database.
551
     * Build the default fields, indexes and values defined in the model.
552
     *
553
     * @param string $tableName
554
     *
555
     * @return bool
556
     * @throws DebugBarException
557
     */
558
    public static function createTable(string $tableName): bool
559
    {
560
        $tabla = self::$bbddStructure[$tableName];
561
        $sql = self::createFields($tableName, $tabla['fields']);
562
563
        foreach ($tabla['keys'] as $name => $index) {
564
            $sql .= self::createIndex($tableName, $name, $index);
565
        }
566
        if (isset($tabla['values'])) {
567
            $sql .= self::setValues($tableName, $tabla['values']);
568
        }
569
570
        return Engine::exec($sql);
571
    }
572
573
    /**
574
     * Build the SQL statement to create the fields in the table.
575
     * It can also create the primary key if the auto_increment attribute is defined.
576
     *
577
     * @param string $tablename
578
     * @param array  $fieldList
579
     *
580
     * @return string
581
     */
582
    protected static function createFields(string $tablename, array $fieldList): string
583
    {
584
        $tablenameWithPrefix = Config::$dbPrefix . $tablename;
585
586
        $sql = "CREATE TABLE $tablenameWithPrefix ( ";
587
        foreach ($fieldList as $index => $col) {
588
            if (!isset($col['dbtype'])) {
589
                die('Tipo no especificado en createTable ' . $index);
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
590
            }
591
592
            $sql .= '`' . $index . '` ' . $col['dbtype'];
593
            $nulo = isset($col['null']) && $col['null'];
594
595
            $sql .= ($nulo ? '' : ' NOT') . ' NULL';
596
597
            if (isset($col['extra']) && (strtolower($col['extra']) == 'auto_increment')) {
598
                $sql .= ' PRIMARY KEY AUTO_INCREMENT';
599
            }
600
601
            $tmpDefecto = $col['default'] ?? null;
602
            $defecto = '';
603
            if (isset($tmpDefecto)) {
604
                if ($tmpDefecto == 'CURRENT_TIMESTAMP') {
605
                    $defecto = "$tmpDefecto";
606
                } else {
607
                    $defecto = "'$tmpDefecto'";
608
                }
609
            } else {
610
                if ($nulo) {
611
                    $defecto = 'NULL';
612
                }
613
            }
614
615
            if ($defecto != '') {
616
                $sql .= ' DEFAULT ' . $defecto;
617
            }
618
619
            $sql .= ', ';
620
        }
621
        $sql = substr($sql, 0, -2); // Quitamos la coma y el espacio del final
622
        $sql .= ') ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;' . self::CRLF;
623
624
        return $sql;
625
    }
626
627
    /**
628
     * Create the SQL statements for the construction of one index.
629
     * In the case of the primary index, it is not necessary if it is auto_increment.
630
     *
631
     * TODO:
632
     *
633
     * Moreover, it should not be defined if it is auto_increment because it would
634
     * generate an error when it already exists.
635
     *
636
     * @param string $tableName
637
     * @param string $indexname
638
     * @param array  $indexData
639
     *
640
     * @return string
641
     */
642
    protected static function createIndex($tableName, $indexname, $indexData)
643
    {
644
        $sql = "ALTER TABLE $tableName ADD CONSTRAINT $indexname ";
645
646
        $command = '';
647
        // https://www.w3schools.com/sql/sql_primarykey.asp
648
        // ALTER TABLE Persons ADD CONSTRAINT PK_Person PRIMARY KEY (ID,LastName);
649
        if (isset($indexData['PRIMARY'])) {
650
            $command = 'PRIMARY KEY ';
651
            $fields = $indexData['PRIMARY'];
652
        }
653
654
        // https://www.w3schools.com/sql/sql_create_index.asp
655
        // CREATE INDEX idx_pname ON Persons (LastName, FirstName);
656
        if (isset($indexData['INDEX'])) {
657
            $command = 'INDEX ';
658
            $fields = $indexData['INDEX'];
659
        }
660
661
        // https://www.w3schools.com/sql/sql_unique.asp
662
        // ALTER TABLE Persons ADD CONSTRAINT UC_Person UNIQUE (ID,LastName);
663
        if (isset($indexData['UNIQUE'])) {
664
            $command = 'UNIQUE INDEX ';
665
            $fields = $indexData['UNIQUE'];
666
        }
667
668
        if ($command == '') {
669
            // https://www.w3schools.com/sql/sql_foreignkey.asp
670
            // ALTER TABLE Orders ADD CONSTRAINT FK_PersonOrder FOREIGN KEY (PersonID) REFERENCES Persons(PersonID);
671
            if (isset($indexData['FOREIGN'])) {
672
                $command = 'FOREIGN KEY ';
673
                $foreignField = $indexData['FOREIGN'];
674
                if (isset($indexData['REFERENCES'])) {
675
                    $references = $indexData['REFERENCES'];
676
                    if (!is_array($references)) {
677
                        die('Esperaba un array en REFERENCES: ' . $tableName . '/' . $indexname);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
678
                    }
679
                    if (count($references) != 1) {
680
                        die('Esperaba un array de 1 elemento en REFERENCES: ' . $tableName . '/' . $indexname);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
681
                    }
682
                    $refTable = key($references);
683
                    $fields = '(' . implode(',', $references) . ')';
684
                } else {
685
                    die('FOREIGN necesita REFERENCES en ' . $tableName . '/' . $indexname);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
686
                }
687
688
                $sql .= $command . ' ' . $foreignField . ' REFERENCES ' . $refTable . $fields;
689
690
                if (isset($indexData['ON']) && is_array($indexData['ON'])) {
691
                    foreach ($indexData['ON'] as $key => $value) {
692
                        $sql .= ' ON ' . $key . ' ' . $value . ', ';
693
                    }
694
                    $sql = substr($sql, 0, -2); // Quitamos el ', ' de detrás
695
                }
696
            }
697
        } else {
698
            if (is_array($fields)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fields does not seem to be defined for all execution paths leading up to this point.
Loading history...
699
                $fields = '(' . implode(',', $fields) . ')';
700
            } else {
701
                $fields = "($fields)";
702
            }
703
704
            if ($command == 'INDEX ') {
705
                $sql = "CREATE INDEX {$indexname} ON {$tableName}" . $fields;
706
            } else {
707
                $sql .= $command . ' ' . $fields;
708
            }
709
        }
710
711
        return $sql . ';' . self::CRLF;
712
    }
713
714
    /**
715
     * Create the SQL statements to fill the table with default data.
716
     *
717
     * @param string $tableName
718
     * @param array  $values
719
     *
720
     * @return string
721
     */
722
    protected static function setValues(string $tableName, array $values): string
723
    {
724
        $sql = "INSERT INTO $tableName ";
725
        $header = true;
726
        foreach ($values as $value) {
727
            $fields = "(";
728
            $datos = "(";
729
            foreach ($value as $fname => $fvalue) {
730
                $fields .= $fname . ", ";
731
                $datos .= "'$fvalue'" . ", ";
732
            }
733
            $fields = substr($fields, 0, -2) . ") ";
734
            $datos = substr($datos, 0, -2) . "), ";
735
736
            if ($header) {
737
                $sql .= $fields . " VALUES ";
738
                $header = false;
739
            }
740
741
            $sql .= $datos;
742
        }
743
744
        return substr($sql, 0, -2) . self::CRLF;
745
    }
746
}
747