Test Failed
Push — main ( a17396...e328ab )
by Rafael
05:43
created

Schema::updateTable()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 14
c 1
b 0
f 0
nc 12
nop 1
dl 0
loc 23
rs 9.2222
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\Config;
23
use Alxarafe\Core\Singletons\Debug;
24
use Alxarafe\Core\Singletons\FlashMessages;
25
use Alxarafe\Core\Singletons\Translator;
26
use Alxarafe\Core\Utils\MathUtils;
27
use DebugBar\DebugBarException;
28
use Symfony\Component\Yaml\Yaml;
29
30
/**
31
 * Class Schema
32
 *
33
 * La clase abstracta Schema, define un esquema de base de datos teórico al que
34
 * se traduce la base de datos real y viceversa, de manera que el código sea
35
 * en la medida de lo posible, no dependiente de la base de datos real.
36
 *
37
 * TODO: ¿La información cacheada se procesa en YamlSchema o no merece la pena?
38
 *
39
 * @author  Rafael San José Tovar <[email protected]>
40
 * @version 2023.0101
41
 *
42
 * @package Alxarafe\Database
43
 */
44
class Schema
45
{
46
    /**
47
     * Tipo entero. Número sin decimales.
48
     */
49
    public const TYPE_INTEGER = 'integer';
50
51
    /**
52
     * Tipo real o coma flotante. Número con decimales. Puede dar problema con redondeos.
53
     */
54
    public const TYPE_FLOAT = 'float';
55
56
    /**
57
     * Tipo numérico de coma fija. Número con N decimales y precisión absoluta.
58
     * Es igual que un integer, pero se asume que un número determinado de dígitos son decimales.
59
     */
60
    public const TYPE_DECIMAL = 'decimal';
61
62
    /**
63
     * Tipo cadena de texto
64
     */
65
    public const TYPE_STRING = 'string';
66
67
    /**
68
     * Tipo bloque de texto
69
     */
70
    public const TYPE_TEXT = 'text';
71
72
    /**
73
     * Tipo fecha
74
     */
75
    public const TYPE_DATE = 'date';
76
77
    /**
78
     * Tipo hora
79
     */
80
    public const TYPE_TIME = 'time';
81
82
    /**
83
     * Tipo fecha + hora.
84
     * TODO: Hay que revisar el tema de la zona horaria.
85
     *       De lógica, siempre se debe de almacenar como UTC y convertir al guardar y leer.
86
     */
87
    public const TYPE_DATETIME = 'datetime';
88
89
    /**
90
     * Tipo lógico: TRUE o FALSE.
91
     */
92
    public const TYPE_BOOLEAN = 'bool';
93
94
    /**
95
     * Longitud de un string si no se ha detallado ninguna
96
     */
97
    public const DEFAULT_STRING_LENGTH = 50;
98
99
    /**
100
     * Bytes que usará un integer si no se ha detallado tamaño
101
     */
102
    public const DEFAULT_INTEGER_SIZE = 4;
103
104
    /**
105
     * Si un integer usa signo por defecto o no. True si no utiliza signo por defecto.
106
     */
107
    public const DEFAULT_INTEGER_UNSIGNED = true;
108
109
    /**
110
     * Retorno de carro y salto de línea
111
     */
112
    const CRLF = "\r\n";
113
114
    /**
115
     * Contiene la definición ampliada de la estructura de la base de datos.
116
     *
117
     * @var array
118
     */
119
    public static array $bbddStructure;
120
121
    /**
122
     * Realiza una comprobación integral de la base de datos, verificando que la configuración
123
     * indicada en los archivos yaml de configuración de tablas, se corresponde con lo
124
     * creado en la base de datos.
125
     * Adecúa la base de datos a la información facilitada por los archivos yaml.
126
     *
127
     * @author Rafael San José Tovar <[email protected]>
128
     *
129
     * @throws DebugBarException
130
     */
131
    public static function checkDatabaseStructure()
132
    {
133
        //        DB::$engine->exec('DROP TABLE IF EXISTS `tc_users`;');
134
        //        DB::$engine->exec('DROP TABLE IF EXISTS `tc_menus`;');
135
        //        DB::$engine->exec('DROP TABLE IF EXISTS `tc_portfolio_assets`;');
136
137
        foreach (YamlSchema::getTables() as $key => $table) {
138
            if (!file_exists($table)) {
139
                Debug::message('No existe la tabla ' . $table);
140
            }
141
            Debug::message("Verificando la tabla $key, definida en $table.");
142
            if (!static::checkStructure($key, $table)) {
143
                FlashMessages::setError('Error al comprobar la estructura de la tabla ' . $table);
144
            }
145
        }
146
    }
147
148
    private static function getGenericType(array $data): array
149
    {
150
        $result = [];
151
        $type = $data['type'];
152
153
        switch ($type) {
154
            case 'autoincrement':
155
                $result['nullable'] = 'no';
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
156
            case 'relationship':
157
                $type = Schema::TYPE_INTEGER;
158
                $result['size'] = 8;
159
                break;
160
        }
161
162
        // Si es un tipo genérico, se retorna automáticamente.
163
        if (isset(DB::$helper::$types[$type])) {
164
            $result['generictype'] = $type;
165
            return $result;
166
        }
167
168
        foreach (DB::$helper::$types as $key => $types) {
169
            if (in_array($type, $types)) {
170
                $result['generictype'] = $key;
171
                return $result;
172
            }
173
        }
174
175
        Debug::message("No se ha encontrado genérico para {$type}. Se asume 'string'.");
176
        $result['generictype'] = 'string';
177
        return $result;
178
    }
179
180
    private static function yamlFieldAnyToSchema(string $genericType, array $data): array
0 ignored issues
show
Unused Code introduced by
The parameter $data 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

180
    private static function yamlFieldAnyToSchema(string $genericType, /** @scrutinizer ignore-unused */ array $data): 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...
181
    {
182
        $types = DB::$helper::getDataTypes();
183
        $type = $types[$genericType];
184
        $result = [];
185
        $result['generictype'] = $genericType;
186
        $result['dbtype'] = reset($type);
187
        return $result;
188
    }
189
190
    /**
191
     * Cumplimenta los datos faltantes del yaml de definición al de caché para
192
     * tipos enteros.
193
     * Posibles valores que se pueden recibir en $data:
194
     * - min, es el valor mínimo aceptado por el entero.
195
     * - max, es el valor máximo aceptado por el entero.
196
     * - size, es el número de bytes que ocupa el entero.
197
     * - unsigned, indica si necesita signo o no.
198
     * La respuesta puede modificar algunos de esos valores.
199
     *
200
     * @author Rafael San José Tovar <[email protected]>
201
     *
202
     * @param array $data
203
     *
204
     * @return array
205
     */
206
    private static function yamlFieldIntegerToSchema(array $data): array
207
    {
208
        $min = $data['min'] ?? null;
209
        $max = $data['max'] ?? null;
210
211
        // Si hay mínimo y máximo, se ajusta el resto de parámetros a esos datos.
212
        if ($min !== null && $max !== null) {
213
            $unsigned = $min >= 0;
214
            $size = MathUtils::howManyBytes($max, $min, $unsigned);
215
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
216
            return [
217
                'dbtype' => $extra['dbtype'],
218
                'min' => $min,
219
                'max' => $max,
220
                'size' => $extra['size'],
221
                'unsigned' => $extra['unsigned'],
222
            ];
223
        }
224
225
        // Si tenemos máximo, pero no tenemos mínimo, se ajusta al máximo y se toma signo por defecto
226
        if ($max !== null) {
227
            $unsigned = $data['unsigned'] ?? self::DEFAULT_INTEGER_UNSIGNED;
228
            $size = MathUtils::howManyBytes($max);
229
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
230
            return [
231
                'dbtype' => $extra['dbtype'],
232
                'min' => $extra['min'],
233
                'max' => $max,
234
                'size' => $extra['size'],
235
                'unsigned' => $extra['unsigned'],
236
            ];
237
        }
238
239
        // Si lo que no tenemos es máximo, ajustamos el tamaño al mínimo y se ajusta el signo al mínimo
240
        if ($min !== null) {
241
            $unsigned = $min >= 0;
242
            $size = MathUtils::howManyBytes($min, $min, $unsigned);
243
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
244
            return [
245
                'dbtype' => $extra['dbtype'],
246
                'min' => 0, // TODO: Si unsigned, será el menor entero negativo.
247
                'max' => $max,
248
                'size' => $extra['size'],
249
                'unsigned' => $extra['unsigned'],
250
            ];
251
        }
252
253
        // Mínimo y máximo son nulos
254
        $size = $data['size'] ?? self::DEFAULT_INTEGER_SIZE;
255
        $unsigned = $data['unsigned'] ?? self::DEFAULT_INTEGER_UNSIGNED;
256
        return DB::$helper::getIntegerMinMax($size, $unsigned);
257
    }
258
259
    private static function yamlFieldStringToSchema(array $data): array
260
    {
261
        return [
262
            'dbtype' => 'varchar',
263
            'minlength' => $data['minlength'] ?? 0,
264
            'length' => $data['length'] ?? self::DEFAULT_STRING_LENGTH,
265
        ];
266
    }
267
268
    /**
269
     * Tomando la definición de un campo de una tabla en un archivo yaml de definición,
270
     * genera toda la información necesaria para la creación, actualización de la tabla
271
     * y el mantenimiento de los datos del campo.
272
     *
273
     * @author Rafael San José Tovar <[email protected]>
274
     *
275
     * @param array $data
276
     *
277
     * @return array
278
     */
279
    public static function yamlFieldToSchema(array $data): array
280
    {
281
        /**
282
         * Los datos que vienen del yaml son los siguientes:
283
         * - name es el nombre del campo
284
         * - type es el tipo genérico del campo
285
         * El resto, será dependiente del tipo genérico de dato.
286
         * Puede ocurrir que no venga un tipo genérico, sino uno fijo, en ese caso
287
         * se intentará corregir, pero se notificará en la barra de depuración.
288
         * Si hay error en la conversión, se generará un error.
289
         */
290
        $column = [];
291
        $column['name'] = (string) $data['name'];
292
        $column['type'] = (string) $data['type'];
293
        $column['nullable'] = $data['nullable'] ?? 'yes';
294
        $column['default'] = $data['default'] ?? null;
295
        $column = array_merge($column, self::getGenericType($data));
296
297
        switch ($column['generictype']) {
298
            case Schema::TYPE_INTEGER:
299
                foreach (['min', 'max', 'unsigned', 'size'] as $field) {
300
                    if (isset($data[$field])) {
301
                        $column[$field] = $data[$field];
302
                        unset($data[$field]);
303
                    }
304
                }
305
                $result = self::yamlFieldIntegerToSchema($column);
306
                break;
307
            case Schema::TYPE_STRING:
308
                foreach (['minlength', 'length'] as $field) {
309
                    if (isset($data[$field])) {
310
                        $column[$field] = $data[$field];
311
                        unset($data[$field]);
312
                    }
313
                }
314
                $result = self::yamlFieldStringToSchema($column);
315
                break;
316
            case Schema::TYPE_FLOAT:
317
            case Schema::TYPE_DECIMAL:
318
            case Schema::TYPE_TEXT:
319
            case Schema::TYPE_DATE:
320
            case Schema::TYPE_TIME:
321
            case Schema::TYPE_DATETIME:
322
            case Schema::TYPE_BOOLEAN:
323
                $result = self::yamlFieldAnyToSchema($column['generictype'], $column);
324
                break;
325
            default:
326
                $result = self::yamlFieldAnyToSchema($column['generictype'], $column);
327
        }
328
329
        unset($data['name']);
330
        unset($data['type']);
331
        unset($data['default']);
332
        unset($data['nullable']);
333
334
        $column = array_merge($column, $result);
335
336
        if (count($data) > 0) {
337
            dump(['Ignorado en data' => $data]);
338
        }
339
        return $column;
340
    }
341
342
    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

342
    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

342
    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...
343
    {
344
        $yaml = Yaml::parseFile($path);
345
        $fields = $yaml['fields'] ?? [];
346
347
        $data = [];
348
        foreach ($fields as $key => $field) {
349
            $field['name'] = $key;
350
            $schema = Schema::yamlFieldToSchema($field);
351
            $data['yamldef'][$key] = $field;
352
            $data['schema'][$key] = $schema;
353
            $data['db'][$key] = DB::$helper::yamlFieldToDb($schema);
354
        }
355
356
        $indexes = $yaml['indexes'] ?? [];
357
358
        return [
359
            'fields' => $data,
360
            'indexes' => $indexes,
361
        ];
362
    }
363
364
    /**
365
     * Comprueba la estructura de la tabla y la crea si no existe y así se solicita.
366
     * Si los datos de la estructura no están en la caché, los regenera y almacena.
367
     * Al regenerar los datos para la caché, también realiza una verificación de
368
     * la estructura por si hay cambios que aplicar en la misma.
369
     *
370
     * TODO: Es mejor que haya un checkStructure que genere TODAS las tablas e índices
371
     * Ese checkstructure se debe de generar tras limpiar caché.
372
     * La caché deberá de limpiarse cada vez que se active o desactive un módulo.
373
     * El último paso de la generación de tablas, sería comprobar las dependencias
374
     * de tablas para saber cuántas tablas usan una constraint de cada tabla para poder
375
     * realizar cambios en la base de datos y tener una visión más nítida de la misma en
376
     * cualquier momento, si bien, esa estructura no será clara hasta que no se hayan leído
377
     * todas, y si hay un cambio entre medias, pues igual la única solución viable es
378
     * determinarlo por la propia base de datos.
379
     *
380
     * @author  Rafael San José Tovar <[email protected]>
381
     * @version 2023.0105
382
     *
383
     * @param string $tableName
384
     * @param string $path
385
     * @param bool   $create
386
     *
387
     * @return bool
388
     * @throws DebugBarException
389
     */
390
    private static function checkStructure(string $tableName, string $path, bool $create = true): bool
391
    {
392
        // Si el dato ya ha sido cargado, retornamos porque no hay nada que hacer.
393
        if (!empty(self::$bbddStructure[$tableName])) {
394
            return true;
395
        }
396
397
        // Si no está, pero está cacheado, se recupera de la caché y se retorna.
398
        self::$bbddStructure[$tableName] = YamlSchema::loadCacheYamlFile(YamlSchema::YAML_CACHE_TABLES_DIR, $tableName);
399
        if (!empty(self::$bbddStructure[$tableName])) {
400
            return true;
401
        }
402
403
        // Si no está cacheado, entonces hay que comprobar si hay cambios en la estructura y regenerarla.
404
        self::$bbddStructure[$tableName] = self::checkTable($tableName, $path, $create);
405
406
        if (DB::tableExists($tableName)) {
407
            Debug::message('La tabla ' . $tableName . ' existe');
408
            if (!self::updateTable($tableName)) {
409
                FlashMessages::setError(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
410
            }
411
        } else {
412
            Debug::message('La tabla ' . $tableName . ' NO existe');
413
            if (!self::createTable($tableName)) {
414
                FlashMessages::setError(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
415
            }
416
        }
417
418
        if (!YamlSchema::saveCacheYamlFile(YamlSchema::YAML_CACHE_TABLES_DIR, $tableName, self::$bbddStructure[$tableName])) {
419
            Debug::message('No se ha podido guardar la información de caché para la tabla ' . $tableName);
420
            return false;
421
        }
422
        return true;
423
    }
424
425
    /**
426
     * Obtiene el tipo genérico del tipo de dato que se le ha pasado.
427
     *
428
     * @author  Rafael San José Tovar <[email protected]>
429
     * @version 2023.0101
430
     *
431
     * @param string $type
432
     *
433
     * @return string
434
     */
435
    public static function _getTypeOf(string $type): string
436
    {
437
        foreach (DB::getDataTypes() as $index => $types) {
0 ignored issues
show
Bug introduced by
The method getDataTypes() does not exist on Alxarafe\Database\DB. Did you maybe mean _getDataTypes()? ( Ignorable by Annotation )

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

437
        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...
438
            if (in_array(strtolower($type), $types)) {
439
                return $index;
440
            }
441
        }
442
        Debug::message($type . ' not found in DBSchema::getTypeOf()');
443
        return 'text';
444
    }
445
446
    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...
447
    {
448
        $replacesSources = [
449
            'character varying',
450
            // 'timestamp without time zone',
451
            'double precision',
452
        ];
453
        $replacesDestination = [
454
            'varchar',
455
            // 'timestamp',
456
            'double',
457
        ];
458
        $modifiedType = (str_replace($replacesSources, $replacesDestination, $originalType));
459
460
        if ($originalType !== $modifiedType) {
461
            Debug::message("XML: Uso de '{$originalType}' en lugar de '{$modifiedType}'.");
462
        }
463
        $explode = explode(' ', strtolower($modifiedType));
464
465
        $pos = strpos($explode[0], '(');
466
        if ($pos > 0) {
467
            $begin = $pos + 1;
468
            $end = strpos($explode[0], ')');
469
            $type = substr($explode[0], 0, $pos);
470
            $length = substr($explode[0], $begin, $end - $begin);
471
        } else {
472
            $type = $explode[0];
473
            $length = null;
474
        }
475
476
        $pos = array_search('unsigned', $explode, true);
477
        $unsigned = $pos ? 'yes' : 'no';
478
479
        $pos = array_search('zerofill', $explode, true);
480
        $zerofill = $pos ? 'yes' : 'no';
481
482
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
483
    }
484
485
    private static function _getSeed($tableName): string
0 ignored issues
show
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...
486
    {
487
        $tableNameWithPrefix = DB::$dbPrefix . $tableName;
488
489
        $seeds = Dispatcher::getFiles('Seeds', 'csv');
490
491
        if (!isset($seeds[$tableName])) {
492
            return '';
493
        }
494
495
        $filename = $seeds[$tableName];
496
        if (!file_exists($filename)) {
497
            return '';
498
        }
499
500
        $rows = 10; // Indicamos el número de registros que vamos a insertar de una vez
501
        $handle = fopen($filename, "r");
502
        if ($handle === false) {
503
            FlashMessages::addError('No ha sido posible abrir el archivo ' . $filename);
0 ignored issues
show
Bug introduced by
The method addError() does not exist on Alxarafe\Core\Singletons\FlashMessages. ( Ignorable by Annotation )

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

503
            FlashMessages::/** @scrutinizer ignore-call */ 
504
                           addError('No ha sido posible abrir el archivo ' . $filename);

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...
504
            return '';
505
        }
506
507
        // Asumimos que la primera fila es la cabecera...
508
        $header = fgetcsv($handle, 0, ';');
509
        if ($header === false) {
510
            FlashMessages::addError('No ha sido posible leer la primera línea del archivo ' . $filename);
511
            fclose($handle);
512
            return '';
513
        }
514
515
        $sqlHeader = "INSERT INTO `{$tableNameWithPrefix}` (`" . implode('`, `', $header) . '`) VALUES ';
516
        $row = 0;
517
        $sqlData = [];
518
        while (($data = fgetcsv($handle, 0, ';')) !== false) {
519
            // Entrecomillamos lo que no sea null.
520
            foreach ($data as $key => $datum) {
521
                if (mb_strtoupper($datum) !== 'NULL') {
522
                    $data[$key] = "'$datum'";
523
                }
524
            }
525
526
            if ($row % $rows === 0) {
527
                if (count($sqlData) > 0) {
528
                    $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...
529
                }
530
                $sqlData = [];
531
            }
532
            $sqlData[] = '(' . implode(', ', $data) . ')';
533
            $row++;
534
        }
535
        if (count($sqlData) > 0) {
536
            $result .= ($sqlHeader . implode(', ', $sqlData) . ';' . PHP_EOL);
537
        }
538
        fclose($handle);
539
540
        return $result;
541
    }
542
543
    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...
544
    {
545
        dump([
546
            'tablename' => $tableName,
547
            'fieldname' => $fieldName,
548
            'new structure' => self::$bbddStructure[$tableName]['fields'][$fieldName],
549
            'structure' => $structure,
550
        ]);
551
        return '';
552
    }
553
554
    /**
555
     * Create a table in the database.
556
     * Build the default fields, indexes and values defined in the model.
557
     *
558
     * @param string $tableName
559
     *
560
     * @return bool
561
     * @throws DebugBarException
562
     */
563
    private static function createTable(string $tableName): bool
564
    {
565
        $tabla = self::$bbddStructure[$tableName];
566
        $sql = self::createFields($tableName, $tabla['fields']['db']);
567
568
        /*
569
        foreach ($tabla['indexes'] as $name => $index) {
570
            $sql .= self::createIndex($tableName, $name, $index);
571
        }
572
573
        if (isset($tabla['values'])) {
574
            $sql .= self::setValues($tableName, $tabla['values']);
575
        } else {
576
            $sql .= self::getSeed($tableName);
577
        }
578
        */
579
580
        return Engine::exec($sql);
581
    }
582
583
    private static function updateTable(string $tableName): bool
584
    {
585
        $yamlStructure = self::$bbddStructure[$tableName];
586
        $dbStructure = DB::getColumns($tableName);
587
588
        $changes = [];
589
        foreach ($yamlStructure['fields']['db'] as $field => $newStructure) {
590
            $oldStructure = DB::$helper::sanitizeDbStructure($yamlStructure['fields']['schema'][$field]['generictype'], $dbStructure[$field]);
0 ignored issues
show
Bug introduced by
The method sanitizeDbStructure() does not exist on Alxarafe\Database\SqlHelper. Since it exists in all sub-types, consider adding an abstract or default implementation to Alxarafe\Database\SqlHelper. ( Ignorable by Annotation )

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

590
            /** @scrutinizer ignore-call */ 
591
            $oldStructure = DB::$helper::sanitizeDbStructure($yamlStructure['fields']['schema'][$field]['generictype'], $dbStructure[$field]);
Loading history...
591
            $dif = array_diff($oldStructure, $newStructure);
592
            if (count($dif) > 0) {
593
                $changes[] = DB::modify($tableName, $oldStructure, $newStructure);
594
            }
595
        }
596
597
        if (empty($changes)) {
598
            return true;
599
        }
600
601
        $result = true;
602
        foreach ($changes as $change) {
603
            $result = $result && Engine::exec($change);
604
        }
605
        return $result;
606
    }
607
608
    /**
609
     * Build the SQL statement to create the fields in the table.
610
     * It can also create the primary key if the auto_increment attribute is defined.
611
     *
612
     * @param string $tablename
613
     * @param array  $fieldList
614
     *
615
     * @return string
616
     */
617
    protected static function _createFields(string $tablename, array $fieldList): string
618
    {
619
        $tablenameWithPrefix = DB::$dbPrefix . $tablename;
620
621
        $sql = "CREATE TABLE $tablenameWithPrefix ( ";
622
        foreach ($fieldList as $index => $column) {
623
            $col = $column['schema'];
624
            if (!isset($col['dbtype'])) {
625
                die('Tipo no especificado en createTable ' . $index);
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 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...
626
            }
627
628
            $sql .= '`' . $index . '` ' . $col['dbtype'];
629
            $nulo = isset($col['null']) && $col['null'];
630
631
            if (strtolower($col['type']) === 'autoincrement') {
632
                $nulo = false;
633
                $sql .= ' PRIMARY KEY AUTO_INCREMENT';
634
            }
635
636
            $sql .= ($nulo ? '' : ' NOT') . ' NULL';
637
638
            $tmpDefecto = $col['default'] ?? null;
639
            $defecto = '';
640
            if (isset($tmpDefecto)) {
641
                if ($tmpDefecto == 'CURRENT_TIMESTAMP') {
642
                    $defecto = "$tmpDefecto";
643
                } else {
644
                    $defecto = "'$tmpDefecto'";
645
                }
646
            } else {
647
                if ($nulo) {
648
                    $defecto = 'NULL';
649
                }
650
            }
651
652
            if ($defecto != '') {
653
                $sql .= ' DEFAULT ' . $defecto;
654
            }
655
656
            $sql .= ', ';
657
        }
658
        $sql = substr($sql, 0, -2); // Quitamos la coma y el espacio del final
659
        $sql .= ') ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;' . self::CRLF;
660
661
        return $sql;
662
    }
663
664
    protected static function createFields(string $tablename, array $fieldList): string
665
    {
666
        $tablenameWithPrefix = DB::$dbPrefix . $tablename;
667
668
        $sql = "CREATE TABLE $tablenameWithPrefix ( ";
669
        foreach ($fieldList as $column) {
670
            $sql .= DB::$helper::getSqlField($column) . ', ';
671
        }
672
        $sql = substr($sql, 0, -2); // Quitamos la coma y el espacio del final
673
        $sql .= ') ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;' . self::CRLF;
674
675
        return $sql;
676
    }
677
678
    /**
679
     * Create the SQL statements for the construction of one index.
680
     * In the case of the primary index, it is not necessary if it is auto_increment.
681
     *
682
     * TODO:
683
     *
684
     * Moreover, it should not be defined if it is auto_increment because it would
685
     * generate an error when it already exists.
686
     *
687
     * @param string $tableName
688
     * @param string $indexname
689
     * @param array  $indexData
690
     *
691
     * @return string
692
     */
693
    protected static function _createIndex($tableName, $indexname, $indexData)
694
    {
695
        $tableNameWithPrefix = DB::$dbPrefix . $tableName;
696
697
        $sql = "ALTER TABLE $tableNameWithPrefix ADD CONSTRAINT $indexname ";
698
699
        $command = '';
700
        // https://www.w3schools.com/sql/sql_primarykey.asp
701
        // ALTER TABLE Persons ADD CONSTRAINT PK_Person PRIMARY KEY (ID,LastName);
702
        if (isset($indexData['primary'])) {
703
            $command = 'PRIMARY KEY ';
704
            $fields = $indexData['primary'];
705
        }
706
707
        // https://www.w3schools.com/sql/sql_create_index.asp
708
        // CREATE INDEX idx_pname ON Persons (LastName, FirstName);
709
        if (isset($indexData['index'])) {
710
            $command = 'INDEX ';
711
            $fields = $indexData['index'];
712
        }
713
714
        // https://www.w3schools.com/sql/sql_unique.asp
715
        // ALTER TABLE Persons ADD CONSTRAINT UC_Person UNIQUE (ID,LastName);
716
        if (isset($indexData['unique'])) {
717
            $command = 'UNIQUE INDEX ';
718
            $fields = $indexData['column'];
719
        }
720
721
        if ($command == '') {
722
            // https://www.w3schools.com/sql/sql_foreignkey.asp
723
            // ALTER TABLE Orders ADD CONSTRAINT FK_PersonOrder FOREIGN KEY (PersonID) REFERENCES Persons(PersonID);
724
            if (isset($indexData['FOREIGN'])) {
725
                $command = 'FOREIGN KEY ';
726
                $foreignField = $indexData['FOREIGN'];
727
                if (isset($indexData['REFERENCES'])) {
728
                    $references = $indexData['REFERENCES'];
729
                    if (!is_array($references)) {
730
                        die('Esperaba un array en REFERENCES: ' . $tableNameWithPrefix . '/' . $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...
731
                    }
732
                    if (count($references) != 1) {
733
                        die('Esperaba un array de 1 elemento en REFERENCES: ' . $tableNameWithPrefix . '/' . $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...
734
                    }
735
                    $refTable = key($references);
736
                    $fields = '(' . implode(',', $references) . ')';
737
                } else {
738
                    die('FOREIGN necesita REFERENCES en ' . $tableNameWithPrefix . '/' . $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...
739
                }
740
741
                $sql .= $command . ' ' . $foreignField . ' REFERENCES ' . $refTable . $fields;
742
743
                if (isset($indexData['ON']) && is_array($indexData['ON'])) {
744
                    foreach ($indexData['ON'] as $key => $value) {
745
                        $sql .= ' ON ' . $key . ' ' . $value . ', ';
746
                    }
747
                    $sql = substr($sql, 0, -2); // Quitamos el ', ' de detrás
748
                }
749
            }
750
        } else {
751
            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...
752
                $fields = '(' . implode(',', $fields) . ')';
753
            } else {
754
                $fields = "($fields)";
755
            }
756
757
            if ($command == 'INDEX ') {
758
                $sql = "CREATE INDEX {$indexname} ON {$tableNameWithPrefix}" . $fields;
759
            } else {
760
                $sql .= $command . ' ' . $fields;
761
            }
762
        }
763
764
        return $sql . ';' . self::CRLF;
765
    }
766
767
    /**
768
     * Create the SQL statements to fill the table with default data.
769
     *
770
     * @param string $tableName
771
     * @param array  $values
772
     *
773
     * @return string
774
     */
775
    protected static function _setValues(string $tableName, array $values): string
776
    {
777
        $tablenameWithPrefix = DB::$dbPrefix . $tableName;
778
779
        $sql = "INSERT INTO $tablenameWithPrefix ";
780
        $header = true;
781
        foreach ($values as $value) {
782
            $fields = "(";
783
            $datos = "(";
784
            foreach ($value as $fname => $fvalue) {
785
                $fields .= $fname . ", ";
786
                $datos .= "'$fvalue'" . ", ";
787
            }
788
            $fields = substr($fields, 0, -2) . ") ";
789
            $datos = substr($datos, 0, -2) . "), ";
790
791
            if ($header) {
792
                $sql .= $fields . " VALUES ";
793
                $header = false;
794
            }
795
796
            $sql .= $datos;
797
        }
798
799
        return substr($sql, 0, -2) . self::CRLF;
800
    }
801
802
    /**
803
     * Return true if $tableName exists in database
804
     *
805
     * @param string $tableName
806
     *
807
     * @return bool
808
     * @throws DebugBarException
809
     */
810
    public static function _tableExists($tableName): bool
811
    {
812
        $tableNameWithPrefix = DB::$dbPrefix . $tableName;
813
        $dbName = DB::$dbName;
814
        $sql = "SELECT COUNT(*) AS Total FROM information_schema.tables WHERE table_schema = '{$dbName}' AND table_name='{$tableNameWithPrefix}'";
815
816
        $data = Engine::select($sql);
817
        $result = reset($data);
818
819
        return $result['Total'] === '1';
820
    }
821
822
    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

822
    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...
823
    {
824
        $data = Yaml::parseFile($path);
825
826
        $result = [];
827
        foreach ($data['fields'] ?? [] as $key => $datum) {
828
            $datum['key'] = $key;
829
            $result['fields'][$key]['db'] = DB::normalizeFromYaml($datum);
0 ignored issues
show
Bug introduced by
The method normalizeFromYaml() does not exist on Alxarafe\Database\DB. Did you maybe mean _normalizeFromYaml()? ( Ignorable by Annotation )

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

829
            /** @scrutinizer ignore-call */ 
830
            $result['fields'][$key]['db'] = DB::normalizeFromYaml($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...
830
            $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

830
            /** @scrutinizer ignore-call */ 
831
            $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...
831
            if ($result['fields'][$key]['type'] === 'autoincrement') {
832
                // TODO: Ver cómo tendría que ser la primary key
833
                $result['indexes']['primary'] = $key;
834
            }
835
        }
836
        foreach ($data['indexes'] ?? [] as $key => $datum) {
837
            $datum['key'] = $key;
838
            $result['indexes'][$key] = $datum;
839
        }
840
841
        /*
842
        Igual conviene crear una clase:
843
        - DBSchema (con los datos de la base de datos real)
844
        - DefinedSchema (con los datos definidos)
845
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
846
847
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
848
    oportunas siempre que no suponga una pérdida de datos.
849
        */
850
851
        return $result;
852
    }
853
854
    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...
855
    {
856
        $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...
857
        if (!file_exists($yamlSourceFilename)) {
858
            dump('No existe el archivo ' . $yamlSourceFilename);
859
        }
860
861
        $data = Yaml::parseFile($yamlSourceFilename);
862
863
        $result = [];
864
        foreach ($data as $key => $datum) {
865
            $datum['key'] = $key;
866
            $result[$key] = Schema::normalize($datum);
867
        }
868
869
        /*
870
        Igual conviene crear una clase:
871
        - DBSchema (con los datos de la base de datos real)
872
        - DefinedSchema (con los datos definidos)
873
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
874
875
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
876
    oportunas siempre que no suponga una pérdida de datos.
877
        */
878
879
        return $result;
880
    }
881
882
    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

882
    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...
883
    {
884
        $result = [];
885
        return $result;
886
    }
887
888
    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

888
    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...
889
    {
890
        $result = [];
891
        return $result;
892
    }
893
}
894