Passed
Push — main ( 3163e6...611078 )
by Rafael
05:37
created

Schema::setValues()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 16
c 1
b 0
f 0
nc 5
nop 2
dl 0
loc 25
ccs 0
cts 17
cp 0
crap 20
rs 9.7333
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\Helpers\Dispatcher;
22
use Alxarafe\Core\Singletons\Debug;
23
use Alxarafe\Core\Singletons\FlashMessages;
24
use Alxarafe\Core\Singletons\Translator;
25
use Alxarafe\Core\Utils\MathUtils;
26
use DebugBar\DebugBarException;
27
use Symfony\Component\Yaml\Yaml;
28
29
/**
30
 * Class Schema
31
 *
32
 * La clase abstracta Schema, define un esquema de base de datos teórico al que
33
 * se traduce la base de datos real y viceversa, de manera que el código sea
34
 * en la medida de lo posible, no dependiente de la base de datos real.
35
 *
36
 * TODO: ¿La información cacheada se procesa en YamlSchema o no merece la pena?
37
 *
38
 * @author  Rafael San José Tovar <[email protected]>
39
 * @version 2023.0101
40
 *
41
 * @package Alxarafe\Database
42
 */
43
class Schema
44
{
45
    /**
46
     * Tipo entero. Número sin decimales.
47
     */
48
    public const TYPE_INTEGER = 'integer';
49
50
    /**
51
     * Tipo real o coma flotante. Número con decimales. Puede dar problema con redondeos.
52
     */
53
    public const TYPE_FLOAT = 'float';
54
55
    /**
56
     * Tipo numérico de coma fija. Número con N decimales y precisión absoluta.
57
     * Es igual que un integer, pero se asume que un número determinado de dígitos son decimales.
58
     */
59
    public const TYPE_DECIMAL = 'decimal';
60
61
    /**
62
     * Tipo cadena de texto
63
     */
64
    public const TYPE_STRING = 'string';
65
66
    /**
67
     * Tipo bloque de texto
68
     */
69
    public const TYPE_TEXT = 'text';
70
71
    /**
72
     * Tipo fecha
73
     */
74
    public const TYPE_DATE = 'date';
75
76
    /**
77
     * Tipo hora
78
     */
79
    public const TYPE_TIME = 'time';
80
81
    /**
82
     * Tipo fecha + hora.
83
     * TODO: Hay que revisar el tema de la zona horaria.
84
     *       De lógica, siempre se debe de almacenar como UTC y convertir al guardar y leer.
85
     */
86
    public const TYPE_DATETIME = 'datetime';
87
88
    /**
89
     * Tipo lógico: TRUE o FALSE.
90
     */
91
    public const TYPE_BOOLEAN = 'bool';
92
93
    /**
94
     * Nombre del campo que se usa por defecto como clave primaria
95
     */
96
    public const DEFAULT_PRIMARY_KEY_FIELD = 'id';
97
98
    /**
99
     * Nombre del campo que se usa por defecto como nombre
100
     */
101
    public const DEFAULT_NAME_FIELD = 'name';
102
103
    public const DEFAULT_UPDATE_RULE = 'restrict';
104
    public const DEFAULT_DELETE_RULE = 'restrict';
105
106
    /**
107
     * Longitud de un string si no se ha detallado ninguna
108
     */
109
    public const DEFAULT_STRING_LENGTH = 50;
110
111
    /**
112
     * Bytes que usará un integer si no se ha detallado tamaño
113
     */
114
    public const DEFAULT_INTEGER_SIZE = 4;
115
116
    /**
117
     * Si un integer usa signo por defecto o no. True si no utiliza signo por defecto.
118
     */
119
    public const DEFAULT_INTEGER_UNSIGNED = true;
120
121
    /**
122
     * Retorno de carro y salto de línea
123
     */
124
    const CRLF = "\r\n";
125
126
    /**
127
     * Contiene la definición ampliada de la estructura de la base de datos.
128
     *
129
     * @var array
130
     */
131
    public static array $bbddStructure;
132
133
    /**
134
     * Realiza una comprobación integral de la base de datos, verificando que la configuración
135
     * indicada en los archivos yaml de configuración de tablas, se corresponde con lo
136
     * creado en la base de datos.
137
     * Adecúa la base de datos a la información facilitada por los archivos yaml.
138
     *
139
     * @author Rafael San José Tovar <[email protected]>
140
     *
141
     * @throws DebugBarException
142
     */
143
    public static function checkDatabaseStructure()
144
    {
145
        // TODO: Eliminar cuando ya cree y actualice correctamente las tablas
146
        // DB::$engine->exec('DROP TABLE IF EXISTS `tc_users`;');
147
        // DB::$engine->exec('DROP TABLE IF EXISTS `tc_menus`;');
148
        // DB::$engine->exec('DROP TABLE IF EXISTS `tc_portfolio_assets`;');
149
150
        foreach (YamlSchema::getTables() as $key => $table) {
151
            if (!file_exists($table)) {
152
                Debug::message('No existe la tabla ' . $table);
153
            }
154
            Debug::message("Verificando la tabla $key, definida en $table.");
155
            if (!static::checkStructure($key, $table)) {
156
                FlashMessages::setError('Error al comprobar la estructura de la tabla ' . $table);
157
            }
158
        }
159
    }
160
161
    private static function getGenericType(array $data): array
162
    {
163
        $result = [];
164
        $type = $data['type'];
165
166
        switch ($type) {
167
            case 'autoincrement':
168
                $result['nullable'] = 'no';
169
            // No se hace break intencionadamente
170
            case 'relationship':
171
                $type = Schema::TYPE_INTEGER;
172
                $result['size'] = 8;
173
                break;
174
        }
175
176
        // Si es un tipo genérico, se retorna automáticamente.
177
        if (isset(DB::$helper::$types[$type])) {
178
            $result['generictype'] = $type;
179
            return $result;
180
        }
181
182
        foreach (DB::$helper::$types as $key => $types) {
183
            if (in_array($type, $types)) {
184
                $result['generictype'] = $key;
185
                return $result;
186
            }
187
        }
188
189
        Debug::message("No se ha encontrado genérico para {$type}. Se asume 'string'.");
190
        $result['generictype'] = 'string';
191
        return $result;
192
    }
193
194
    private static function yamlFieldAnyToSchema(string $genericType): array
195
    {
196
        $types = DB::$helper::getDataTypes();
197
        $type = $types[$genericType];
198
        $result = [];
199
        $result['generictype'] = $genericType;
200
        $result['dbtype'] = reset($type);
201
        return $result;
202
    }
203
204
    /**
205
     * Cumplimenta los datos faltantes del yaml de definición al de caché para
206
     * tipos enteros.
207
     * Posibles valores que se pueden recibir en $data:
208
     * - min, es el valor mínimo aceptado por el entero.
209
     * - max, es el valor máximo aceptado por el entero.
210
     * - size, es el número de bytes que ocupa el entero.
211
     * - unsigned, indica si necesita signo o no.
212
     * La respuesta puede modificar algunos de esos valores.
213
     *
214
     * @author Rafael San José Tovar <[email protected]>
215
     *
216
     * @param array $data
217
     *
218
     * @return array
219
     */
220
    private static function yamlFieldIntegerToSchema(array $data): array
221
    {
222
        $min = $data['min'] ?? null;
223
        $max = $data['max'] ?? null;
224
225
        // Si hay mínimo y máximo, se ajusta el resto de parámetros a esos datos.
226
        if ($min !== null && $max !== null) {
227
            $unsigned = $min >= 0;
228
            $size = MathUtils::howManyBytes($max, $min, $unsigned);
229
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
230
            return [
231
                'dbtype' => $extra['dbtype'],
232
                'min' => $min,
233
                'max' => $max,
234
                'size' => $extra['size'],
235
                'unsigned' => $extra['unsigned'],
236
            ];
237
        }
238
239
        // Si tenemos máximo, pero no tenemos mínimo, se ajusta al máximo y se toma signo por defecto
240
        if ($max !== null) {
241
            $unsigned = $data['unsigned'] ?? self::DEFAULT_INTEGER_UNSIGNED;
242
            $size = MathUtils::howManyBytes($max);
243
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
244
            return [
245
                'dbtype' => $extra['dbtype'],
246
                'min' => $extra['min'],
247
                'max' => $max,
248
                'size' => $extra['size'],
249
                'unsigned' => $extra['unsigned'],
250
            ];
251
        }
252
253
        // Si lo que no tenemos es máximo, ajustamos el tamaño al mínimo y se ajusta el signo al mínimo
254
        if ($min !== null) {
255
            $unsigned = $min >= 0;
256
            $size = MathUtils::howManyBytes($min, $min, $unsigned);
257
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
258
            return [
259
                'dbtype' => $extra['dbtype'],
260
                'min' => 0, // TODO: Si unsigned, será el menor entero negativo.
261
                'max' => $max,
262
                'size' => $extra['size'],
263
                'unsigned' => $extra['unsigned'],
264
            ];
265
        }
266
267
        // Mínimo y máximo son nulos
268
        $size = $data['size'] ?? self::DEFAULT_INTEGER_SIZE;
269
        $unsigned = $data['unsigned'] ?? self::DEFAULT_INTEGER_UNSIGNED;
270
        return DB::$helper::getIntegerMinMax($size, $unsigned);
271
    }
272
273
    private static function yamlFieldStringToSchema(array $data): array
274
    {
275
        return [
276
            'dbtype' => 'varchar',
277
            'minlength' => $data['minlength'] ?? 0,
278
            'length' => $data['length'] ?? self::DEFAULT_STRING_LENGTH,
279
        ];
280
    }
281
282
    /**
283
     * Tomando la definición de un campo de una tabla en un archivo yaml de definición,
284
     * genera toda la información necesaria para la creación, actualización de la tabla
285
     * y el mantenimiento de los datos del campo.
286
     *
287
     * @author Rafael San José Tovar <[email protected]>
288
     *
289
     * @param array $data
290
     *
291
     * @return array
292
     */
293
    private static function yamlFieldToSchema(array $data): array
294
    {
295
        /**
296
         * Los datos que vienen del yaml son los siguientes:
297
         * - name es el nombre del campo
298
         * - type es el tipo genérico del campo
299
         * El resto, será dependiente del tipo genérico de dato.
300
         * Puede ocurrir que no venga un tipo genérico, sino uno fijo, en ese caso
301
         * se intentará corregir, pero se notificará en la barra de depuración.
302
         * Si hay error en la conversión, se generará un error.
303
         */
304
        $column = [];
305
        $column['name'] = (string) $data['name'];
306
        $column['type'] = (string) $data['type'];
307
        $column['nullable'] = $data['nullable'] ?? 'yes';
308
        $column['default'] = $data['default'] ?? null;
309
        $column = array_merge($column, self::getGenericType($data));
310
311
        switch ($column['generictype']) {
312
            case Schema::TYPE_INTEGER:
313
                foreach (['min', 'max', 'unsigned', 'size'] as $field) {
314
                    if (isset($data[$field])) {
315
                        $column[$field] = $data[$field];
316
                        unset($data[$field]);
317
                    }
318
                }
319
                $result = self::yamlFieldIntegerToSchema($column);
320
                break;
321
            case Schema::TYPE_STRING:
322
                foreach (['minlength', 'length'] as $field) {
323
                    if (isset($data[$field])) {
324
                        $column[$field] = $data[$field];
325
                        unset($data[$field]);
326
                    }
327
                }
328
                $result = self::yamlFieldStringToSchema($column);
329
                break;
330
            // case Schema::TYPE_FLOAT:
331
            // case Schema::TYPE_DECIMAL:
332
            // case Schema::TYPE_TEXT:
333
            // case Schema::TYPE_DATE:
334
            // case Schema::TYPE_TIME:
335
            // case Schema::TYPE_DATETIME:
336
            // case Schema::TYPE_BOOLEAN:
337
            default:
338
                $result = self::yamlFieldAnyToSchema($column['generictype']);
339
        }
340
341
        unset($data['name']);
342
        unset($data['type']);
343
        unset($data['default']);
344
        unset($data['nullable']);
345
346
        $column = array_merge($column, $result);
347
348
        if (count($data) > 0) {
349
            dump(['Ignorado en data' => $data]);
350
        }
351
        return $column;
352
    }
353
354
    private static function yamlIndexToSchema(array $data): array
355
    {
356
        $index = [];
357
358
        $index['column'] = strtolower($data['column']);
359
        if ($data['primary']) {
360
            $index['primary'] = 'yes';
361
            $data['unique'] = 'yes';
362
        }
363
        $index['unique'] = $data['unique'] ?? 'no';
364
365
        // Es una relación
366
        if (isset($data['referencedtable'])) {
367
            $index['name'] = (string) $data['name'];
368
            $index['referencedtable'] = DB::$dbPrefix . strtolower($data['referencedtable']);
369
            $index['referencedfields'] = strtolower($data['referencedfields']) ?? self::DEFAULT_PRIMARY_KEY_FIELD;
370
            $index['updaterule'] = strtolower($data['updaterule']) ?? self::DEFAULT_UPDATE_RULE;
371
            $index['deleterule'] = strtolower($data['deleterule']) ?? self::DEFAULT_DELETE_RULE;
372
        }
373
374
        return $index;
375
    }
376
377
    private static function checkTable(string $tableName, string $path, bool $create = true): 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

377
    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...
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

377
    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...
378
    {
379
        $yaml = Yaml::parseFile($path);
380
        $fields = $yaml['fields'] ?? [];
381
382
        $dataFields = [];
383
        foreach ($fields as $key => $field) {
384
            $field['name'] = $key;
385
            $schema = Schema::yamlFieldToSchema($field);
386
            $dataFields['yamldef'][$key] = $field;
387
            $dataFields['schema'][$key] = $schema;
388
            $dataFields['db'][$key] = DB::$helper::yamlFieldToDb($schema);
389
        }
390
391
        $indexes = DB::$helper::yamlIndexToDb($yaml);
392
        $dataIndexes = [];
393
        foreach ($indexes as $key => $index) {
394
            $index['name'] = $key;
395
            $dataIndexes[$key] = Schema::yamlIndexToSchema($index);
396
        }
397
398
        return [
399
            'fields' => $dataFields,
400
            'indexes' => $dataIndexes,
401
        ];
402
    }
403
404
    /**
405
     * Comprueba la estructura de la tabla y la crea si no existe y así se solicita.
406
     * Si los datos de la estructura no están en la caché, los regenera y almacena.
407
     * Al regenerar los datos para la caché, también realiza una verificación de
408
     * la estructura por si hay cambios que aplicar en la misma.
409
     *
410
     * TODO: Es mejor que haya un checkStructure que genere TODAS las tablas e índices
411
     * Ese checkstructure se debe de generar tras limpiar caché.
412
     * La caché deberá de limpiarse cada vez que se active o desactive un módulo.
413
     * El último paso de la generación de tablas, sería comprobar las dependencias
414
     * de tablas para saber cuántas tablas usan una constraint de cada tabla para poder
415
     * realizar cambios en la base de datos y tener una visión más nítida de la misma en
416
     * cualquier momento, si bien, esa estructura no será clara hasta que no se hayan leído
417
     * todas, y si hay un cambio entre medias, pues igual la única solución viable es
418
     * determinarlo por la propia base de datos.
419
     *
420
     * @author  Rafael San José Tovar <[email protected]>
421
     * @version 2023.0105
422
     *
423
     * @param string $tableName
424
     * @param string $path
425
     * @param bool   $create
426
     *
427
     * @return bool
428
     * @throws DebugBarException
429
     */
430
    private static function checkStructure(string $tableName, string $path, bool $create = true): bool
431
    {
432
        // Si el dato ya ha sido cargado, retornamos porque no hay nada que hacer.
433
        if (!empty(self::$bbddStructure[$tableName])) {
434
            return true;
435
        }
436
437
        // Si no está, pero está cacheado, se recupera de la caché y se retorna.
438
        self::$bbddStructure[$tableName] = YamlSchema::loadCacheYamlFile(YamlSchema::YAML_CACHE_TABLES_DIR, $tableName);
439
        if (!empty(self::$bbddStructure[$tableName])) {
440
            return true;
441
        }
442
443
        // Si no está cacheado, entonces hay que comprobar si hay cambios en la estructura y regenerarla.
444
        self::$bbddStructure[$tableName] = self::checkTable($tableName, $path, $create);
445
446
        if (DB::tableExists($tableName)) {
447
            Debug::message('La tabla ' . $tableName . ' existe');
448
            if (!self::updateTable($tableName)) {
449
                FlashMessages::setError(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
450
            }
451
        } else {
452
            Debug::message('La tabla ' . $tableName . ' NO existe');
453
            if (!self::createTable($tableName)) {
454
                FlashMessages::setError(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
455
            }
456
        }
457
458
        if (!YamlSchema::saveCacheYamlFile(YamlSchema::YAML_CACHE_TABLES_DIR, $tableName, self::$bbddStructure[$tableName])) {
459
            Debug::message('No se ha podido guardar la información de caché para la tabla ' . $tableName);
460
            return false;
461
        }
462
        return true;
463
    }
464
465
    /**
466
     * Obtiene el tipo genérico del tipo de dato que se le ha pasado.
467
     *
468
     * @author  Rafael San José Tovar <[email protected]>
469
     * @version 2023.0101
470
     *
471
     * @param string $type
472
     *
473
     * @return string
474
     */
475
    public static function _getTypeOf(string $type): string
476
    {
477
        foreach (DB::getDataTypes() as $index => $types) {
0 ignored issues
show
Bug introduced by
The method getDataTypes() does not exist on Alxarafe\Database\DB. ( Ignorable by Annotation )

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

477
        foreach (DB::/** @scrutinizer ignore-call */ getDataTypes() as $index => $types) {

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...
478
            if (in_array(strtolower($type), $types)) {
479
                return $index;
480
            }
481
        }
482
        Debug::message($type . ' not found in DBSchema::getTypeOf()');
483
        return 'text';
484
    }
485
486
    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...
487
    {
488
        $replacesSources = [
489
            'character varying',
490
            // 'timestamp without time zone',
491
            'double precision',
492
        ];
493
        $replacesDestination = [
494
            'varchar',
495
            // 'timestamp',
496
            'double',
497
        ];
498
        $modifiedType = (str_replace($replacesSources, $replacesDestination, $originalType));
499
500
        if ($originalType !== $modifiedType) {
501
            Debug::message("XML: Uso de '{$originalType}' en lugar de '{$modifiedType}'.");
502
        }
503
        $explode = explode(' ', strtolower($modifiedType));
504
505
        $pos = strpos($explode[0], '(');
506
        if ($pos > 0) {
507
            $begin = $pos + 1;
508
            $end = strpos($explode[0], ')');
509
            $type = substr($explode[0], 0, $pos);
510
            $length = substr($explode[0], $begin, $end - $begin);
511
        } else {
512
            $type = $explode[0];
513
            $length = null;
514
        }
515
516
        $pos = array_search('unsigned', $explode, true);
517
        $unsigned = $pos ? 'yes' : 'no';
518
519
        $pos = array_search('zerofill', $explode, true);
520
        $zerofill = $pos ? 'yes' : 'no';
521
522
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
523
    }
524
525
    private static function getSeed($tableName): string
526
    {
527
        $tableNameWithPrefix = DB::$dbPrefix . $tableName;
528
529
        $seeds = Dispatcher::getFiles('Seeds', 'csv');
530
531
        if (!isset($seeds[$tableName])) {
532
            return '';
533
        }
534
535
        $filename = $seeds[$tableName];
536
        if (!file_exists($filename)) {
537
            return '';
538
        }
539
540
        $rows = 10; // Indicamos el número de registros que vamos a insertar de una vez
541
        $handle = fopen($filename, "r");
542
        if ($handle === false) {
543
            FlashMessages::setError('No ha sido posible abrir el archivo ' . $filename);
544
            return '';
545
        }
546
547
        // Asumimos que la primera fila es la cabecera...
548
        $header = fgetcsv($handle, 0, ';');
549
        if ($header === false) {
550
            FlashMessages::setError('No ha sido posible leer la primera línea del archivo ' . $filename);
551
            fclose($handle);
552
            return '';
553
        }
554
555
        $sqlHeader = "INSERT INTO `{$tableNameWithPrefix}` (`" . implode('`, `', $header) . '`) VALUES ';
556
        $row = 0;
557
        $sqlData = [];
558
        while (($data = fgetcsv($handle, 0, ';')) !== false) {
559
            // Entrecomillamos lo que no sea null.
560
            foreach ($data as $key => $datum) {
561
                if (mb_strtoupper($datum) !== 'NULL') {
562
                    $data[$key] = "'$datum'";
563
                }
564
            }
565
566
            if ($row % $rows === 0) {
567
                if (count($sqlData) > 0) {
568
                    $result .= ($sqlHeader . implode(', ', $sqlData) . ';' . PHP_EOL);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result seems to be never defined.
Loading history...
569
                }
570
                $sqlData = [];
571
            }
572
            $sqlData[] = '(' . implode(', ', $data) . ')';
573
            $row++;
574
        }
575
        if (count($sqlData) > 0) {
576
            $result .= ($sqlHeader . implode(', ', $sqlData) . ';' . PHP_EOL);
577
        }
578
        fclose($handle);
579
580
        return $result;
581
    }
582
583
    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...
584
    {
585
        dump([
586
            'tablename' => $tableName,
587
            'fieldname' => $fieldName,
588
            'new structure' => self::$bbddStructure[$tableName]['fields'][$fieldName],
589
            'structure' => $structure,
590
        ]);
591
        return '';
592
    }
593
594
    /**
595
     * Create a table in the database.
596
     * Build the default fields, indexes and values defined in the model.
597
     *
598
     * @param string $tableName
599
     *
600
     * @return bool
601
     * @throws DebugBarException
602
     */
603
    private static function createTable(string $tableName): bool
604
    {
605
        $tabla = self::$bbddStructure[$tableName];
606
        $sql = self::createFields($tableName, $tabla['fields']['db']);
607
608
        foreach ($tabla['indexes'] as $name => $index) {
609
            $sql .= DB::createIndex($tableName, $name, $index);
610
        }
611
612
        if (isset($tabla['values'])) {
613
            $sql .= self::setValues($tableName, $tabla['values']);
614
        } else {
615
            $sql .= self::getSeed($tableName);
616
        }
617
618
        return Engine::exec($sql);
619
    }
620
621
    private static function updateTable(string $tableName): bool
622
    {
623
        $yamlStructure = self::$bbddStructure[$tableName];
624
        $dbStructure = DB::getColumns($tableName);
625
626
        $changes = [];
627
        foreach ($yamlStructure['fields']['db'] as $field => $newStructure) {
628
            $oldStructure = DB::$helper::sanitizeDbStructure($yamlStructure['fields']['schema'][$field]['generictype'], $dbStructure[$field]);
629
            $dif = array_diff($oldStructure, $newStructure);
630
            if (count($dif) > 0) {
631
                $changes[] = DB::modify($tableName, $oldStructure, $newStructure);
632
            }
633
        }
634
635
        $indexes = DB::getIndexes($tableName);
636
        $newStructureArray = $yamlStructure['indexes'];
637
        // Primero se eliminan los índices que ya no existen
638
        foreach ($indexes as $index => $oldStructure) {
639
            $newStructure = $newStructureArray[$index] ?? null;
640
            if (!isset($newStructure)) {
641
                $changes[] = DB::removeIndex($tableName, $index);
642
                continue;
643
            }
644
            $changes[] = DB::changeIndex($tableName, $index, $oldStructure, $newStructure);
645
        }
646
        foreach ($newStructureArray as $index => $newStructure) {
647
            $oldStructure = $indexes[$index] ?? null;
648
            if (isset($oldStructure)) {
649
                continue;
650
            }
651
            $changes[] = DB::createIndex($tableName, $index, $newStructure);
652
        }
653
654
        if (empty($changes)) {
655
            return true;
656
        }
657
658
        $result = true;
659
        foreach ($changes as $change) {
660
            if (!empty($change)) {
661
                $result = $result && Engine::exec($change);
662
            }
663
        }
664
        return $result;
665
    }
666
667
    protected static function createFields(string $tablename, array $fieldList): string
668
    {
669
        $tableNameWithPrefix = DB::$dbPrefix . $tablename;
670
671
        $sql = "CREATE TABLE $tableNameWithPrefix ( ";
672
        foreach ($fieldList as $column) {
673
            $sql .= DB::$helper::getSqlField($column) . ', ';
674
        }
675
        $sql = substr($sql, 0, -2); // Quitamos la coma y el espacio del final
676
        $sql .= ') ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;' . self::CRLF;
677
678
        return $sql;
679
    }
680
681
    /**
682
     * Create the SQL statements to fill the table with default data.
683
     *
684
     * @param string $tableName
685
     * @param array  $values
686
     *
687
     * @return string
688
     */
689
    protected static function setValues(string $tableName, array $values): string
690
    {
691
        $tablenameWithPrefix = DB::$dbPrefix . $tableName;
692
693
        $sql = "INSERT INTO $tablenameWithPrefix ";
694
        $header = true;
695
        foreach ($values as $value) {
696
            $fields = "(";
697
            $datos = "(";
698
            foreach ($value as $fname => $fvalue) {
699
                $fields .= $fname . ", ";
700
                $datos .= "'$fvalue'" . ", ";
701
            }
702
            $fields = substr($fields, 0, -2) . ") ";
703
            $datos = substr($datos, 0, -2) . "), ";
704
705
            if ($header) {
706
                $sql .= $fields . " VALUES ";
707
                $header = false;
708
            }
709
710
            $sql .= $datos;
711
        }
712
713
        return substr($sql, 0, -2) . self::CRLF;
714
    }
715
}
716