Passed
Push — main ( bc0c36...bd30b4 )
by Rafael
05:32
created

Schema::compare_columns()   F

Complexity

Conditions 26
Paths 317

Size

Total Lines 92
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 26
eloc 48
nc 317
nop 3
dl 0
loc 92
rs 1.8708
c 1
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
A Schema::createTable() 0 14 3
A Schema::updateTable() 0 29 3
A Schema::updateField() 0 9 1

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\FlashMessages;
24
use Alxarafe\Core\Singletons\Translator;
25
use DebugBar\DebugBarException;
26
use Symfony\Component\Yaml\Yaml;
27
28
/**
29
 * Class Schema
30
 *
31
 * La clase abstracta Schema, define un esquema de base de datos teórico al que
32
 * se traduce la base de datos real y viceversa, de manera que el código sea
33
 * en la medida de lo posible, no dependiente de la base de datos real.
34
 *
35
 * TODO: ¿La información cacheada se procesa en YamlSchema o no merece la pena?
36
 *
37
 * @author  Rafael San José Tovar <[email protected]>
38
 * @version 2023.0101
39
 *
40
 * @package Alxarafe\Database
41
 */
42
class Schema
43
{
44
    /**
45
     * Tipo entero. Número sin decimales.
46
     */
47
    public const TYPE_INTEGER = 'integer';
48
49
    /**
50
     * Tipo real o coma flotante. Número con decimales. Puede dar problema con redondeos.
51
     */
52
    public const TYPE_FLOAT = 'float';
53
54
    /**
55
     * Tipo numérico de coma fija. Número con N decimales y precisión absoluta.
56
     * Es igual que un integer, pero se asume que un número determinado de dígitos son decimales.
57
     */
58
    public const TYPE_DECIMAL = 'decimal';
59
60
    /**
61
     * Tipo cadena de texto
62
     */
63
    public const TYPE_STRING = 'string';
64
65
    /**
66
     * Tipo bloque de texto
67
     */
68
    public const TYPE_TEXT = 'text';
69
70
    /**
71
     * Tipo fecha
72
     */
73
    public const TYPE_DATE = 'date';
74
75
    /**
76
     * Tipo hora
77
     */
78
    public const TYPE_TIME = 'time';
79
80
    /**
81
     * Tipo fecha + hora.
82
     * TODO: Hay que revisar el tema de la zona horaria.
83
     *       De lógica, siempre se debe de almacenar como UTC y convertir al guardar y leer.
84
     */
85
    public const TYPE_DATETIME = 'datetime';
86
87
    /**
88
     * Tipo lógico: TRUE o FALSE.
89
     */
90
    public const TYPE_BOOLEAN = 'bool';
91
92
    /**
93
     * Retorno de carro y salto de línea
94
     */
95
    const CRLF = "\r\n";
96
97
    /**
98
     * Contiene la definición ampliada de la estructura de la base de datos.
99
     *
100
     * @var array
101
     */
102
    public static array $bbddStructure;
103
104
    public static function checkDatabaseStructure()
105
    {
106
        foreach (YamlSchema::getTables() as $key => $table) {
107
            if (!file_exists($table)) {
108
                Debug::message('No existe la tabla ' . $table);
109
            }
110
            dump("Verificando la tabla $key, definida en $table.");
111
            if (!static::checkStructure($key, $table)) {
112
                FlashMessages::setError('Error al comprobar la estructura de la tabla ' . $table);
113
            }
114
        }
115
    }
116
117
    /**
118
     * Return true if $tableName exists in database
119
     *
120
     * @param string $tableName
121
     *
122
     * @return bool
123
     * @throws DebugBarException
124
     */
125
    public static function tableExists($tableName): bool
126
    {
127
        $tableNameWithPrefix = Config::$dbPrefix . $tableName;
128
        $dbName = Config::$dbName;
129
        $sql = "SELECT COUNT(*) AS Total FROM information_schema.tables WHERE table_schema = '{$dbName}' AND table_name='{$tableNameWithPrefix}'";
130
131
        $data = Engine::select($sql);
132
        $result = reset($data);
133
134
        return $result['Total'] === '1';
135
    }
136
137
    private static function getFieldsAndIndexes($tableName, $path): 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

137
    private static function getFieldsAndIndexes(/** @scrutinizer ignore-unused */ $tableName, $path): 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...
Unused Code introduced by
The method getFieldsAndIndexes() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
138
    {
139
        $data = Yaml::parseFile($path);
140
141
        dump([$path => $data]);
142
143
        $result = [];
144
        foreach ($data['fields'] ?? [] as $key => $datum) {
145
            $datum['key'] = $key;
146
            $result['fields'][$key]['db'] = DB::normalizeFromYaml($datum);
147
            $result['fields'][$key]['info'] = Schema::normalize($datum);
0 ignored issues
show
Bug introduced by
The method normalize() does not exist on Alxarafe\Database\Schema. ( Ignorable by Annotation )

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

147
            /** @scrutinizer ignore-call */ 
148
            $result['fields'][$key]['info'] = Schema::normalize($datum);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
148
            if ($result['fields'][$key]['type'] === 'autoincrement') {
149
                // TODO: Ver cómo tendría que ser la primary key
150
                $result['indexes']['primary'] = $key;
151
            }
152
        }
153
        foreach ($data['indexes'] ?? [] as $key => $datum) {
154
            $datum['key'] = $key;
155
            $result['indexes'][$key] = $datum;
156
        }
157
158
        /*
159
        Igual conviene crear una clase:
160
        - DBSchema (con los datos de la base de datos real)
161
        - DefinedSchema (con los datos definidos)
162
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
163
164
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
165
    oportunas siempre que no suponga una pérdida de datos.
166
        */
167
168
        return $result;
169
    }
170
171
    private static function getFields($tableName): array
0 ignored issues
show
Unused Code introduced by
The method getFields() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
172
    {
173
        $yamlSourceFilename = self::$tables[$tableName];
0 ignored issues
show
Bug Best Practice introduced by
The property tables does not exist on Alxarafe\Database\Schema. Did you maybe forget to declare it?
Loading history...
174
        if (!file_exists($yamlSourceFilename)) {
175
            dump('No existe el archivo ' . $yamlSourceFilename);
176
        }
177
178
        $data = Yaml::parseFile($yamlSourceFilename);
179
180
        $result = [];
181
        foreach ($data as $key => $datum) {
182
            $datum['key'] = $key;
183
            $result[$key] = Schema::normalize($datum);
184
        }
185
186
        /*
187
        Igual conviene crear una clase:
188
        - DBSchema (con los datos de la base de datos real)
189
        - DefinedSchema (con los datos definidos)
190
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
191
192
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
193
    oportunas siempre que no suponga una pérdida de datos.
194
        */
195
196
        return $result;
197
    }
198
199
    private static function getIndexes($tableName): array
0 ignored issues
show
Unused Code introduced by
The method getIndexes() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
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

199
    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...
200
    {
201
        $result = [];
202
        return $result;
203
    }
204
205
    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

205
    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...
Unused Code introduced by
The method getRelated() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
206
    {
207
        $result = [];
208
        return $result;
209
    }
210
211
    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

211
    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...
Unused Code introduced by
The method getSeed() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
212
    {
213
        $result = [];
214
        return $result;
215
    }
216
217
    private static function checkTable(string $tableName, string $path, bool $create = true): array
0 ignored issues
show
Unused Code introduced by
The parameter $create 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

217
    private static function checkTable(string $tableName, string $path, /** @scrutinizer ignore-unused */ bool $create = true): 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...
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

217
    private static function checkTable(/** @scrutinizer ignore-unused */ string $tableName, string $path, bool $create = true): 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...
218
    {
219
        $yaml = Yaml::parseFile($path);
220
        $fields = $yaml['fields'] ?? [];
221
222
        $data = [];
223
        foreach ($fields as $key => $field) {
224
            $field['key'] = $key;
225
            $schema = DB::yamlFieldToSchema($field);
226
            $data[$key]['db'] = DB::yamlFieldToDb($schema);
227
            $data[$key]['schema'] = $schema;
228
        }
229
230
        $indexes = $yaml['indexes'] ?? [];
231
232
        return [
233
            'fields' => $data,
234
            'indexes' => $indexes,
235
        ];
236
    }
237
238
    /**
239
     * Comprueba la estructura de la tabla y la crea si no existe y así se solicita.
240
     * Si los datos de la estructura no están en la caché, los regenera y almacena.
241
     * Al regenerar los datos para la caché, también realiza una verificación de
242
     * la estructura por si hay cambios que aplicar en la misma.
243
     *
244
     * TODO: Es mejor que haya un checkStructure que genere TODAS las tablas e índices
245
     * Ese checkstructure se debe de generar tras limpiar caché.
246
     * La caché deberá de limpiarse cada vez que se active o desactive un módulo.
247
     * El último paso de la generación de tablas, sería comprobar las dependencias
248
     * de tablas para saber cuántas tablas usan una constraint de cada tabla para poder
249
     * realizar cambios en la base de datos y tener una visión más nítida de la misma en
250
     * cualquier momento, si bien, esa estructura no será clara hasta que no se hayan leído
251
     * todas, y si hay un cambio entre medias, pues igual la única solución viable es
252
     * determinarlo por la propia base de datos.
253
     *
254
     * @author  Rafael San José Tovar <[email protected]>
255
     * @version 2023.0105
256
     *
257
     * @param string $tableName
258
     * @param bool   $create
259
     *
260
     * @return bool
261
     */
262
    private static function checkStructure(string $tableName, string $path, bool $create = true): bool
263
    {
264
        // Si el dato ya ha sido cargado, retornamos porque no hay nada que hacer.
265
        if (!empty(self::$bbddStructure[$tableName])) {
266
            return true;
267
        }
268
269
        // Si no está, pero está cacheado, se recupera de la caché y se retorna.
270
        self::$bbddStructure[$tableName] = YamlSchema::loadCacheYamlFile(YamlSchema::YAML_CACHE_TABLES_DIR, $tableName);
271
        if (!empty(self::$bbddStructure[$tableName])) {
272
            return true;
273
        }
274
275
        // Si no está cacheado, entonces hay que comprobar si hay cambios en la estructura y regenerarla.
276
        self::$bbddStructure[$tableName] = self::checkTable($tableName, $path, $create);
277
278
        dump(self::$bbddStructure);
279
280
        if (DB::tableExists($tableName)) {
281
            dump('La tabla ' . $tableName . ' existe');
282
            if (!self::updateTable($tableName)) {
283
                dump(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
284
                FlashMessages::setError(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
285
            }
286
        } else {
287
            dump('La tabla ' . $tableName . ' NO existe');
288
            if (!self::createTable($tableName)) {
289
                dump(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
290
                FlashMessages::setError(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
291
            }
292
        }
293
294
        return true;
295
        die('Por aquí vamos ahora...');
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...
Unused Code introduced by
ExitNode is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
296
297
        if (!YamlSchema::saveCacheYamlFile(YamlSchema::YAML_CACHE_TABLES_DIR, $tableName, self::$bbddStructure[$tableName])) {
298
            Debug::message('No se ha podido guardar la información de caché para la tabla ' . $tableName);
299
            return false;
300
        }
301
        return true;
302
    }
303
304
    /**
305
     * Obtiene el tipo genérico del tipo de dato que se le ha pasado.
306
     *
307
     * @author  Rafael San José Tovar <[email protected]>
308
     * @version 2023.0101
309
     *
310
     * @param string $type
311
     *
312
     * @return string
313
     */
314
    public static function getTypeOf(string $type): string
315
    {
316
        foreach (DB::getDataTypes() as $index => $types) {
317
            if (in_array(strtolower($type), $types)) {
318
                return $index;
319
            }
320
        }
321
        Debug::message($type . ' not found in DBSchema::getTypeOf()');
322
        return 'text';
323
    }
324
325
    private static function splitType(string $originalType): array
0 ignored issues
show
Unused Code introduced by
The method splitType() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
326
    {
327
        $replacesSources = [
328
            'character varying',
329
            // 'timestamp without time zone',
330
            'double precision',
331
        ];
332
        $replacesDestination = [
333
            'varchar',
334
            // 'timestamp',
335
            'double',
336
        ];
337
        $modifiedType = (str_replace($replacesSources, $replacesDestination, $originalType));
338
339
        if ($originalType !== $modifiedType) {
340
            Debug::message("XML: Uso de '{$originalType}' en lugar de '{$modifiedType}'.");
341
        }
342
        $explode = explode(' ', strtolower($modifiedType));
343
344
        $pos = strpos($explode[0], '(');
345
        if ($pos > 0) {
346
            $begin = $pos + 1;
347
            $end = strpos($explode[0], ')');
348
            $type = substr($explode[0], 0, $pos);
349
            $length = substr($explode[0], $begin, $end - $begin);
350
        } else {
351
            $type = $explode[0];
352
            $length = null;
353
        }
354
355
        $pos = array_search('unsigned', $explode, true);
356
        $unsigned = $pos ? 'yes' : 'no';
357
358
        $pos = array_search('zerofill', $explode, true);
359
        $zerofill = $pos ? 'yes' : 'no';
360
361
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
362
    }
363
364
    /**
365
     * Create a table in the database.
366
     * Build the default fields, indexes and values defined in the model.
367
     *
368
     * @param string $tableName
369
     *
370
     * @return bool
371
     * @throws DebugBarException
372
     */
373
374
    private static function createTable(string $tableName): bool
375
    {
376
        $tabla = self::$bbddStructure[$tableName];
377
        $sql = self::createFields($tableName, $tabla['fields']);
378
379
        foreach ($tabla['keys'] as $name => $index) {
380
            $sql .= self::createIndex($tableName, $name, $index);
381
        }
382
        // TODO: values no existe, hay que cargar los datos de seeds.
383
        if (isset($tabla['values'])) {
384
            $sql .= self::setValues($tableName, $tabla['values']);
385
        }
386
387
        return Engine::exec($sql);
388
    }
389
390
    private static function updateField(string $tableName, string $fieldName, array $structure): string
0 ignored issues
show
Unused Code introduced by
The method updateField() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
391
    {
392
        dump([
393
            'tablename' => $tableName,
394
            'fieldname' => $fieldName,
395
            'new structure' => self::$bbddStructure[$tableName]['fields'][$fieldName],
396
            'structure' => $structure,
397
        ]);
398
        return '';
399
    }
400
401
    private static function updateTable(string $tableName): bool
402
    {
403
        $yamlStructure = self::$bbddStructure[$tableName];
404
        $dbStructure = DB::getColumns($tableName);
405
406
        foreach ($yamlStructure['fields'] as $field => $newStructure) {
407
            $oldDb = $dbStructure[$field];
408
            $newDb = $newStructure['db'];
409
410
            $dif = array_diff($oldDb, $newDb);
411
            $data = [
412
                'field' => $field,
413
                'dbStructure' => $dbStructure[$field],
414
                'fields of ' . $tableName => $newStructure['db'],
415
                'oldDb' => $oldDb,
416
                'newDb' => $newDb,
417
            ];
418
            if (count($dif) > 0) {
419
                $data['diferencias 1'] = $dif;
420
                $data['diferencias 2'] = array_diff($newDb, $oldDb);
421
                $data['sql'] = DB::modify($tableName, $oldDb, $newDb);
422
            }
423
424
            dump($data);
425
        }
426
427
//        die('Here');
428
429
        return Engine::exec(DB::modify($tableName, $oldDb, $newDb));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $newDb seems to be defined by a foreach iteration on line 406. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
Comprehensibility Best Practice introduced by
The variable $oldDb seems to be defined by a foreach iteration on line 406. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
430
    }
431
432
    /**
433
     * Build the SQL statement to create the fields in the table.
434
     * It can also create the primary key if the auto_increment attribute is defined.
435
     *
436
     * @param string $tablename
437
     * @param array  $fieldList
438
     *
439
     * @return string
440
     */
441
    protected static function createFields(string $tablename, array $fieldList): string
442
    {
443
        $tablenameWithPrefix = Config::$dbPrefix . $tablename;
444
445
        $sql = "CREATE TABLE $tablenameWithPrefix ( ";
446
        foreach ($fieldList as $index => $column) {
447
            $col = $column['schema'];
448
            if (!isset($col['dbtype'])) {
449
                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...
450
            }
451
452
            $sql .= '`' . $index . '` ' . $col['dbtype'];
453
            $nulo = isset($col['null']) && $col['null'];
454
455
            $sql .= ($nulo ? '' : ' NOT') . ' NULL';
456
457
            if (isset($col['extra']) && (strtolower($col['extra']) == 'auto_increment')) {
458
                $sql .= ' PRIMARY KEY AUTO_INCREMENT';
459
            }
460
461
            $tmpDefecto = $col['default'] ?? null;
462
            $defecto = '';
463
            if (isset($tmpDefecto)) {
464
                if ($tmpDefecto == 'CURRENT_TIMESTAMP') {
465
                    $defecto = "$tmpDefecto";
466
                } else {
467
                    $defecto = "'$tmpDefecto'";
468
                }
469
            } else {
470
                if ($nulo) {
471
                    $defecto = 'NULL';
472
                }
473
            }
474
475
            if ($defecto != '') {
476
                $sql .= ' DEFAULT ' . $defecto;
477
            }
478
479
            $sql .= ', ';
480
        }
481
        $sql = substr($sql, 0, -2); // Quitamos la coma y el espacio del final
482
        $sql .= ') ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;' . self::CRLF;
483
484
        return $sql;
485
    }
486
487
    /**
488
     * Create the SQL statements for the construction of one index.
489
     * In the case of the primary index, it is not necessary if it is auto_increment.
490
     *
491
     * TODO:
492
     *
493
     * Moreover, it should not be defined if it is auto_increment because it would
494
     * generate an error when it already exists.
495
     *
496
     * @param string $tableName
497
     * @param string $indexname
498
     * @param array  $indexData
499
     *
500
     * @return string
501
     */
502
    protected static function createIndex($tableName, $indexname, $indexData)
503
    {
504
        $sql = "ALTER TABLE $tableName ADD CONSTRAINT $indexname ";
505
506
        $command = '';
507
        // https://www.w3schools.com/sql/sql_primarykey.asp
508
        // ALTER TABLE Persons ADD CONSTRAINT PK_Person PRIMARY KEY (ID,LastName);
509
        if (isset($indexData['PRIMARY'])) {
510
            $command = 'PRIMARY KEY ';
511
            $fields = $indexData['PRIMARY'];
512
        }
513
514
        // https://www.w3schools.com/sql/sql_create_index.asp
515
        // CREATE INDEX idx_pname ON Persons (LastName, FirstName);
516
        if (isset($indexData['INDEX'])) {
517
            $command = 'INDEX ';
518
            $fields = $indexData['INDEX'];
519
        }
520
521
        // https://www.w3schools.com/sql/sql_unique.asp
522
        // ALTER TABLE Persons ADD CONSTRAINT UC_Person UNIQUE (ID,LastName);
523
        if (isset($indexData['UNIQUE'])) {
524
            $command = 'UNIQUE INDEX ';
525
            $fields = $indexData['UNIQUE'];
526
        }
527
528
        if ($command == '') {
529
            // https://www.w3schools.com/sql/sql_foreignkey.asp
530
            // ALTER TABLE Orders ADD CONSTRAINT FK_PersonOrder FOREIGN KEY (PersonID) REFERENCES Persons(PersonID);
531
            if (isset($indexData['FOREIGN'])) {
532
                $command = 'FOREIGN KEY ';
533
                $foreignField = $indexData['FOREIGN'];
534
                if (isset($indexData['REFERENCES'])) {
535
                    $references = $indexData['REFERENCES'];
536
                    if (!is_array($references)) {
537
                        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...
538
                    }
539
                    if (count($references) != 1) {
540
                        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...
541
                    }
542
                    $refTable = key($references);
543
                    $fields = '(' . implode(',', $references) . ')';
544
                } else {
545
                    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...
546
                }
547
548
                $sql .= $command . ' ' . $foreignField . ' REFERENCES ' . $refTable . $fields;
549
550
                if (isset($indexData['ON']) && is_array($indexData['ON'])) {
551
                    foreach ($indexData['ON'] as $key => $value) {
552
                        $sql .= ' ON ' . $key . ' ' . $value . ', ';
553
                    }
554
                    $sql = substr($sql, 0, -2); // Quitamos el ', ' de detrás
555
                }
556
            }
557
        } else {
558
            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...
559
                $fields = '(' . implode(',', $fields) . ')';
560
            } else {
561
                $fields = "($fields)";
562
            }
563
564
            if ($command == 'INDEX ') {
565
                $sql = "CREATE INDEX {$indexname} ON {$tableName}" . $fields;
566
            } else {
567
                $sql .= $command . ' ' . $fields;
568
            }
569
        }
570
571
        return $sql . ';' . self::CRLF;
572
    }
573
574
    /**
575
     * Create the SQL statements to fill the table with default data.
576
     *
577
     * @param string $tableName
578
     * @param array  $values
579
     *
580
     * @return string
581
     */
582
    protected static function setValues(string $tableName, array $values): string
583
    {
584
        $sql = "INSERT INTO $tableName ";
585
        $header = true;
586
        foreach ($values as $value) {
587
            $fields = "(";
588
            $datos = "(";
589
            foreach ($value as $fname => $fvalue) {
590
                $fields .= $fname . ", ";
591
                $datos .= "'$fvalue'" . ", ";
592
            }
593
            $fields = substr($fields, 0, -2) . ") ";
594
            $datos = substr($datos, 0, -2) . "), ";
595
596
            if ($header) {
597
                $sql .= $fields . " VALUES ";
598
                $header = false;
599
            }
600
601
            $sql .= $datos;
602
        }
603
604
        return substr($sql, 0, -2) . self::CRLF;
605
    }
606
}
607