Test Failed
Push — main ( a047e2...82fe17 )
by Rafael
05:55
created

Schema::updateTable()   B

Complexity

Conditions 11
Paths 135

Size

Total Lines 44
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 28
c 1
b 0
f 0
nc 135
nop 1
dl 0
loc 44
rs 7.025

How to fix   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\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
     * Longitud de un string si no se ha detallado ninguna
95
     */
96
    public const DEFAULT_STRING_LENGTH = 50;
97
98
    /**
99
     * Bytes que usará un integer si no se ha detallado tamaño
100
     */
101
    public const DEFAULT_INTEGER_SIZE = 4;
102
103
    /**
104
     * Si un integer usa signo por defecto o no. True si no utiliza signo por defecto.
105
     */
106
    public const DEFAULT_INTEGER_UNSIGNED = true;
107
108
    /**
109
     * Retorno de carro y salto de línea
110
     */
111
    const CRLF = "\r\n";
112
113
    /**
114
     * Contiene la definición ampliada de la estructura de la base de datos.
115
     *
116
     * @var array
117
     */
118
    public static array $bbddStructure;
119
120
    /**
121
     * Realiza una comprobación integral de la base de datos, verificando que la configuración
122
     * indicada en los archivos yaml de configuración de tablas, se corresponde con lo
123
     * creado en la base de datos.
124
     * Adecúa la base de datos a la información facilitada por los archivos yaml.
125
     *
126
     * @author Rafael San José Tovar <[email protected]>
127
     *
128
     * @throws DebugBarException
129
     */
130
    public static function checkDatabaseStructure()
131
    {
132
        // TODO: Eliminar cuando ya cree y actualice correctamente las tablas
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';
156
            // No se hace break intencionadamente
157
            case 'relationship':
158
                $type = Schema::TYPE_INTEGER;
159
                $result['size'] = 8;
160
                break;
161
        }
162
163
        // Si es un tipo genérico, se retorna automáticamente.
164
        if (isset(DB::$helper::$types[$type])) {
165
            $result['generictype'] = $type;
166
            return $result;
167
        }
168
169
        foreach (DB::$helper::$types as $key => $types) {
170
            if (in_array($type, $types)) {
171
                $result['generictype'] = $key;
172
                return $result;
173
            }
174
        }
175
176
        Debug::message("No se ha encontrado genérico para {$type}. Se asume 'string'.");
177
        $result['generictype'] = 'string';
178
        return $result;
179
    }
180
181
    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

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

343
    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

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

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

504
            FlashMessages::/** @scrutinizer ignore-call */ 
505
                           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...
505
            return '';
506
        }
507
508
        // Asumimos que la primera fila es la cabecera...
509
        $header = fgetcsv($handle, 0, ';');
510
        if ($header === false) {
511
            FlashMessages::addError('No ha sido posible leer la primera línea del archivo ' . $filename);
512
            fclose($handle);
513
            return '';
514
        }
515
516
        $sqlHeader = "INSERT INTO `{$tableNameWithPrefix}` (`" . implode('`, `', $header) . '`) VALUES ';
517
        $row = 0;
518
        $sqlData = [];
519
        while (($data = fgetcsv($handle, 0, ';')) !== false) {
520
            // Entrecomillamos lo que no sea null.
521
            foreach ($data as $key => $datum) {
522
                if (mb_strtoupper($datum) !== 'NULL') {
523
                    $data[$key] = "'$datum'";
524
                }
525
            }
526
527
            if ($row % $rows === 0) {
528
                if (count($sqlData) > 0) {
529
                    $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...
530
                }
531
                $sqlData = [];
532
            }
533
            $sqlData[] = '(' . implode(', ', $data) . ')';
534
            $row++;
535
        }
536
        if (count($sqlData) > 0) {
537
            $result .= ($sqlHeader . implode(', ', $sqlData) . ';' . PHP_EOL);
538
        }
539
        fclose($handle);
540
541
        return $result;
542
    }
543
544
    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...
545
    {
546
        dump([
547
            'tablename' => $tableName,
548
            'fieldname' => $fieldName,
549
            'new structure' => self::$bbddStructure[$tableName]['fields'][$fieldName],
550
            'structure' => $structure,
551
        ]);
552
        return '';
553
    }
554
555
    /**
556
     * Create a table in the database.
557
     * Build the default fields, indexes and values defined in the model.
558
     *
559
     * @param string $tableName
560
     *
561
     * @return bool
562
     * @throws DebugBarException
563
     */
564
    private static function createTable(string $tableName): bool
565
    {
566
        $tabla = self::$bbddStructure[$tableName];
567
        $sql = self::createFields($tableName, $tabla['fields']['db']);
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
        return Engine::exec($sql);
580
    }
581
582
    private static function updateTable(string $tableName): bool
583
    {
584
        $yamlStructure = self::$bbddStructure[$tableName];
585
        $dbStructure = DB::getColumns($tableName);
586
587
        $changes = [];
588
        foreach ($yamlStructure['fields']['db'] as $field => $newStructure) {
589
            $oldStructure = DB::$helper::sanitizeDbStructure($yamlStructure['fields']['schema'][$field]['generictype'], $dbStructure[$field]);
590
            $dif = array_diff($oldStructure, $newStructure);
591
            if (count($dif) > 0) {
592
                $changes[] = DB::modify($tableName, $oldStructure, $newStructure);
593
            }
594
        }
595
596
        $indexes = DB::getIndexes($tableName);
597
        $newStructureArray = $yamlStructure['indexes'];
598
        // Primero se eliminan los índices que ya no existen
599
        foreach ($indexes as $index => $oldStructure) {
600
            $newStructure = $newStructureArray[$index] ?? null;
601
            if (!isset($newStructure)) {
602
                $changes[] = DB::removeIndex($tableName, $index, $oldStructure);
0 ignored issues
show
Unused Code introduced by
The call to Alxarafe\Database\DB::removeIndex() has too many arguments starting with $oldStructure. ( Ignorable by Annotation )

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

602
                /** @scrutinizer ignore-call */ 
603
                $changes[] = DB::removeIndex($tableName, $index, $oldStructure);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

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

847
    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...
848
    {
849
        $data = Yaml::parseFile($path);
850
851
        $result = [];
852
        foreach ($data['fields'] ?? [] as $key => $datum) {
853
            $datum['key'] = $key;
854
            $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

854
            /** @scrutinizer ignore-call */ 
855
            $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...
855
            $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

855
            /** @scrutinizer ignore-call */ 
856
            $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...
856
            if ($result['fields'][$key]['type'] === 'autoincrement') {
857
                // TODO: Ver cómo tendría que ser la primary key
858
                $result['indexes']['primary'] = $key;
859
            }
860
        }
861
        foreach ($data['indexes'] ?? [] as $key => $datum) {
862
            $datum['key'] = $key;
863
            $result['indexes'][$key] = $datum;
864
        }
865
866
        /*
867
        Igual conviene crear una clase:
868
        - DBSchema (con los datos de la base de datos real)
869
        - DefinedSchema (con los datos definidos)
870
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
871
872
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
873
    oportunas siempre que no suponga una pérdida de datos.
874
        */
875
876
        return $result;
877
    }
878
879
    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...
880
    {
881
        $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...
882
        if (!file_exists($yamlSourceFilename)) {
883
            dump('No existe el archivo ' . $yamlSourceFilename);
884
        }
885
886
        $data = Yaml::parseFile($yamlSourceFilename);
887
888
        $result = [];
889
        foreach ($data as $key => $datum) {
890
            $datum['key'] = $key;
891
            $result[$key] = Schema::normalize($datum);
892
        }
893
894
        /*
895
        Igual conviene crear una clase:
896
        - DBSchema (con los datos de la base de datos real)
897
        - DefinedSchema (con los datos definidos)
898
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
899
900
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
901
    oportunas siempre que no suponga una pérdida de datos.
902
        */
903
904
        return $result;
905
    }
906
907
    private static function _getIndexes($tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

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

907
    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...
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...
908
    {
909
        $result = [];
910
        return $result;
911
    }
912
913
    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

913
    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...
914
    {
915
        $result = [];
916
        return $result;
917
    }
918
}
919