Test Failed
Push — main ( 570848...a047e2 )
by Rafael
05:32
created

Schema::getSeed()   B

Complexity

Conditions 11
Paths 24

Size

Total Lines 56
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 34
c 1
b 0
f 0
nc 24
nop 1
dl 0
loc 56
rs 7.3166

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Copyright (C) 2022-2023  Rafael San José Tovar   <[email protected]>
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License
16
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
 */
18
19
namespace Alxarafe\Database;
20
21
use Alxarafe\Core\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
        // TODO: Eliminar cuando ya cree y actualice correctamente las tablas
134
        DB::$engine->exec('DROP TABLE IF EXISTS `tc_users`;');
135
        DB::$engine->exec('DROP TABLE IF EXISTS `tc_menus`;');
136
        DB::$engine->exec('DROP TABLE IF EXISTS `tc_portfolio_assets`;');
137
138
        foreach (YamlSchema::getTables() as $key => $table) {
139
            if (!file_exists($table)) {
140
                Debug::message('No existe la tabla ' . $table);
141
            }
142
            Debug::message("Verificando la tabla $key, definida en $table.");
143
            if (!static::checkStructure($key, $table)) {
144
                FlashMessages::setError('Error al comprobar la estructura de la tabla ' . $table);
145
            }
146
        }
147
    }
148
149
    private static function getGenericType(array $data): array
150
    {
151
        $result = [];
152
        $type = $data['type'];
153
154
        switch ($type) {
155
            case 'autoincrement':
156
                $result['nullable'] = 'no';
157
            // No se hace break intencionadamente
158
            case 'relationship':
159
                $type = Schema::TYPE_INTEGER;
160
                $result['size'] = 8;
161
                break;
162
        }
163
164
        // Si es un tipo genérico, se retorna automáticamente.
165
        if (isset(DB::$helper::$types[$type])) {
166
            $result['generictype'] = $type;
167
            return $result;
168
        }
169
170
        foreach (DB::$helper::$types as $key => $types) {
171
            if (in_array($type, $types)) {
172
                $result['generictype'] = $key;
173
                return $result;
174
            }
175
        }
176
177
        Debug::message("No se ha encontrado genérico para {$type}. Se asume 'string'.");
178
        $result['generictype'] = 'string';
179
        return $result;
180
    }
181
182
    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

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

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

344
    private static function checkTable(string $tableName, string $path, /** @scrutinizer ignore-unused */ bool $create = true): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

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

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

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

505
            FlashMessages::/** @scrutinizer ignore-call */ 
506
                           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...
506
            return '';
507
        }
508
509
        // Asumimos que la primera fila es la cabecera...
510
        $header = fgetcsv($handle, 0, ';');
511
        if ($header === false) {
512
            FlashMessages::addError('No ha sido posible leer la primera línea del archivo ' . $filename);
513
            fclose($handle);
514
            return '';
515
        }
516
517
        $sqlHeader = "INSERT INTO `{$tableNameWithPrefix}` (`" . implode('`, `', $header) . '`) VALUES ';
518
        $row = 0;
519
        $sqlData = [];
520
        while (($data = fgetcsv($handle, 0, ';')) !== false) {
521
            // Entrecomillamos lo que no sea null.
522
            foreach ($data as $key => $datum) {
523
                if (mb_strtoupper($datum) !== 'NULL') {
524
                    $data[$key] = "'$datum'";
525
                }
526
            }
527
528
            if ($row % $rows === 0) {
529
                if (count($sqlData) > 0) {
530
                    $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...
531
                }
532
                $sqlData = [];
533
            }
534
            $sqlData[] = '(' . implode(', ', $data) . ')';
535
            $row++;
536
        }
537
        if (count($sqlData) > 0) {
538
            $result .= ($sqlHeader . implode(', ', $sqlData) . ';' . PHP_EOL);
539
        }
540
        fclose($handle);
541
542
        return $result;
543
    }
544
545
    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...
546
    {
547
        dump([
548
            'tablename' => $tableName,
549
            'fieldname' => $fieldName,
550
            'new structure' => self::$bbddStructure[$tableName]['fields'][$fieldName],
551
            'structure' => $structure,
552
        ]);
553
        return '';
554
    }
555
556
    /**
557
     * Create a table in the database.
558
     * Build the default fields, indexes and values defined in the model.
559
     *
560
     * @param string $tableName
561
     *
562
     * @return bool
563
     * @throws DebugBarException
564
     */
565
    private static function createTable(string $tableName): bool
566
    {
567
        $tabla = self::$bbddStructure[$tableName];
568
        $sql = self::createFields($tableName, $tabla['fields']['db']);
569
570
        foreach ($tabla['indexes'] as $name => $index) {
571
            $sql .= self::createIndex($tableName, $name, $index);
572
        }
573
574
        if (isset($tabla['values'])) {
575
            $sql .= self::setValues($tableName, $tabla['values']);
576
        } else {
577
            $sql .= self::getSeed($tableName);
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]);
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