Passed
Push — main ( 8239ff...3163e6 )
by Rafael
05:45
created

Schema::yamlFieldToSchema()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 59
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 31
c 1
b 0
f 0
nc 8
nop 1
dl 0
loc 59
ccs 0
cts 29
cp 0
crap 72
rs 8.1795

How to fix   Long Method   

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
     * Nombre del campo que se usa por defecto como clave primaria
95
     */
96
    public const DEFAULT_PRIMARY_KEY_FIELD = 'id';
97
98
    /**
99
     * Nombre del campo que se usa por defecto como nombre
100
     */
101
    public const DEFAULT_NAME_FIELD = 'name';
102
103
    public const DEFAULT_UPDATE_RULE = 'restrict';
104
    public const DEFAULT_DELETE_RULE = 'restrict';
105
106
    /**
107
     * Longitud de un string si no se ha detallado ninguna
108
     */
109
    public const DEFAULT_STRING_LENGTH = 50;
110
111
    /**
112
     * Bytes que usará un integer si no se ha detallado tamaño
113
     */
114
    public const DEFAULT_INTEGER_SIZE = 4;
115
116
    /**
117
     * Si un integer usa signo por defecto o no. True si no utiliza signo por defecto.
118
     */
119
    public const DEFAULT_INTEGER_UNSIGNED = true;
120
121
    /**
122
     * Retorno de carro y salto de línea
123
     */
124
    const CRLF = "\r\n";
125
126
    /**
127
     * Contiene la definición ampliada de la estructura de la base de datos.
128
     *
129
     * @var array
130
     */
131
    public static array $bbddStructure;
132
133
    /**
134
     * Realiza una comprobación integral de la base de datos, verificando que la configuración
135
     * indicada en los archivos yaml de configuración de tablas, se corresponde con lo
136
     * creado en la base de datos.
137
     * Adecúa la base de datos a la información facilitada por los archivos yaml.
138
     *
139
     * @author Rafael San José Tovar <[email protected]>
140
     *
141
     * @throws DebugBarException
142
     */
143
    public static function checkDatabaseStructure()
144
    {
145
        // TODO: Eliminar cuando ya cree y actualice correctamente las tablas
146
        // DB::$engine->exec('DROP TABLE IF EXISTS `tc_users`;');
147
        // DB::$engine->exec('DROP TABLE IF EXISTS `tc_menus`;');
148
        // DB::$engine->exec('DROP TABLE IF EXISTS `tc_portfolio_assets`;');
149
150
        foreach (YamlSchema::getTables() as $key => $table) {
151
            if (!file_exists($table)) {
152
                Debug::message('No existe la tabla ' . $table);
153
            }
154
            Debug::message("Verificando la tabla $key, definida en $table.");
155
            if (!static::checkStructure($key, $table)) {
156
                FlashMessages::setError('Error al comprobar la estructura de la tabla ' . $table);
157
            }
158
        }
159
    }
160
161
    private static function getGenericType(array $data): array
162
    {
163
        $result = [];
164
        $type = $data['type'];
165
166
        switch ($type) {
167
            case 'autoincrement':
168
                $result['nullable'] = 'no';
169
            // No se hace break intencionadamente
170
            case 'relationship':
171
                $type = Schema::TYPE_INTEGER;
172
                $result['size'] = 8;
173
                break;
174
        }
175
176
        // Si es un tipo genérico, se retorna automáticamente.
177
        if (isset(DB::$helper::$types[$type])) {
178
            $result['generictype'] = $type;
179
            return $result;
180
        }
181
182
        foreach (DB::$helper::$types as $key => $types) {
183
            if (in_array($type, $types)) {
184
                $result['generictype'] = $key;
185
                return $result;
186
            }
187
        }
188
189
        Debug::message("No se ha encontrado genérico para {$type}. Se asume 'string'.");
190
        $result['generictype'] = 'string';
191
        return $result;
192
    }
193
194
    private static function yamlFieldAnyToSchema(string $genericType): array
195
    {
196
        $types = DB::$helper::getDataTypes();
197
        $type = $types[$genericType];
198
        $result = [];
199
        $result['generictype'] = $genericType;
200
        $result['dbtype'] = reset($type);
201
        return $result;
202
    }
203
204
    /**
205
     * Cumplimenta los datos faltantes del yaml de definición al de caché para
206
     * tipos enteros.
207
     * Posibles valores que se pueden recibir en $data:
208
     * - min, es el valor mínimo aceptado por el entero.
209
     * - max, es el valor máximo aceptado por el entero.
210
     * - size, es el número de bytes que ocupa el entero.
211
     * - unsigned, indica si necesita signo o no.
212
     * La respuesta puede modificar algunos de esos valores.
213
     *
214
     * @author Rafael San José Tovar <[email protected]>
215
     *
216
     * @param array $data
217
     *
218
     * @return array
219
     */
220
    private static function yamlFieldIntegerToSchema(array $data): array
221
    {
222
        $min = $data['min'] ?? null;
223
        $max = $data['max'] ?? null;
224
225
        // Si hay mínimo y máximo, se ajusta el resto de parámetros a esos datos.
226
        if ($min !== null && $max !== null) {
227
            $unsigned = $min >= 0;
228
            $size = MathUtils::howManyBytes($max, $min, $unsigned);
229
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
230
            return [
231
                'dbtype' => $extra['dbtype'],
232
                'min' => $min,
233
                'max' => $max,
234
                'size' => $extra['size'],
235
                'unsigned' => $extra['unsigned'],
236
            ];
237
        }
238
239
        // Si tenemos máximo, pero no tenemos mínimo, se ajusta al máximo y se toma signo por defecto
240
        if ($max !== null) {
241
            $unsigned = $data['unsigned'] ?? self::DEFAULT_INTEGER_UNSIGNED;
242
            $size = MathUtils::howManyBytes($max);
243
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
244
            return [
245
                'dbtype' => $extra['dbtype'],
246
                'min' => $extra['min'],
247
                'max' => $max,
248
                'size' => $extra['size'],
249
                'unsigned' => $extra['unsigned'],
250
            ];
251
        }
252
253
        // Si lo que no tenemos es máximo, ajustamos el tamaño al mínimo y se ajusta el signo al mínimo
254
        if ($min !== null) {
255
            $unsigned = $min >= 0;
256
            $size = MathUtils::howManyBytes($min, $min, $unsigned);
257
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
258
            return [
259
                'dbtype' => $extra['dbtype'],
260
                'min' => 0, // TODO: Si unsigned, será el menor entero negativo.
261
                'max' => $max,
262
                'size' => $extra['size'],
263
                'unsigned' => $extra['unsigned'],
264
            ];
265
        }
266
267
        // Mínimo y máximo son nulos
268
        $size = $data['size'] ?? self::DEFAULT_INTEGER_SIZE;
269
        $unsigned = $data['unsigned'] ?? self::DEFAULT_INTEGER_UNSIGNED;
270
        return DB::$helper::getIntegerMinMax($size, $unsigned);
271
    }
272
273
    private static function yamlFieldStringToSchema(array $data): array
274
    {
275
        return [
276
            'dbtype' => 'varchar',
277
            'minlength' => $data['minlength'] ?? 0,
278
            'length' => $data['length'] ?? self::DEFAULT_STRING_LENGTH,
279
        ];
280
    }
281
282
    /**
283
     * Tomando la definición de un campo de una tabla en un archivo yaml de definición,
284
     * genera toda la información necesaria para la creación, actualización de la tabla
285
     * y el mantenimiento de los datos del campo.
286
     *
287
     * @author Rafael San José Tovar <[email protected]>
288
     *
289
     * @param array $data
290
     *
291
     * @return array
292
     */
293
    private static function yamlFieldToSchema(array $data): array
294
    {
295
        /**
296
         * Los datos que vienen del yaml son los siguientes:
297
         * - name es el nombre del campo
298
         * - type es el tipo genérico del campo
299
         * El resto, será dependiente del tipo genérico de dato.
300
         * Puede ocurrir que no venga un tipo genérico, sino uno fijo, en ese caso
301
         * se intentará corregir, pero se notificará en la barra de depuración.
302
         * Si hay error en la conversión, se generará un error.
303
         */
304
        $column = [];
305
        $column['name'] = (string) $data['name'];
306
        $column['type'] = (string) $data['type'];
307
        $column['nullable'] = $data['nullable'] ?? 'yes';
308
        $column['default'] = $data['default'] ?? null;
309
        $column = array_merge($column, self::getGenericType($data));
310
311
        switch ($column['generictype']) {
312
            case Schema::TYPE_INTEGER:
313
                foreach (['min', 'max', 'unsigned', 'size'] as $field) {
314
                    if (isset($data[$field])) {
315
                        $column[$field] = $data[$field];
316
                        unset($data[$field]);
317
                    }
318
                }
319
                $result = self::yamlFieldIntegerToSchema($column);
320
                break;
321
            case Schema::TYPE_STRING:
322
                foreach (['minlength', 'length'] as $field) {
323
                    if (isset($data[$field])) {
324
                        $column[$field] = $data[$field];
325
                        unset($data[$field]);
326
                    }
327
                }
328
                $result = self::yamlFieldStringToSchema($column);
329
                break;
330
            // case Schema::TYPE_FLOAT:
331
            // case Schema::TYPE_DECIMAL:
332
            // case Schema::TYPE_TEXT:
333
            // case Schema::TYPE_DATE:
334
            // case Schema::TYPE_TIME:
335
            // case Schema::TYPE_DATETIME:
336
            // case Schema::TYPE_BOOLEAN:
337
            default:
338
                $result = self::yamlFieldAnyToSchema($column['generictype']);
339
        }
340
341
        unset($data['name']);
342
        unset($data['type']);
343
        unset($data['default']);
344
        unset($data['nullable']);
345
346
        $column = array_merge($column, $result);
347
348
        if (count($data) > 0) {
349
            dump(['Ignorado en data' => $data]);
350
        }
351
        return $column;
352
    }
353
354
    private static function yamlIndexToSchema(array $data): array
355
    {
356
        $index = [];
357
358
        $index['column'] = strtolower($data['column']);
359
        if ($data['primary']) {
360
            $index['primary'] = 'yes';
361
            $data['unique'] = 'yes';
362
        }
363
        $index['unique'] = $data['unique'] ?? 'no';
364
365
        // Es una relación
366
        if (isset($data['referencedtable'])) {
367
            $index['name'] = (string) $data['name'];
368
            $index['referencedtable'] = DB::$dbPrefix . strtolower($data['referencedtable']);
369
            $index['referencedfields'] = strtolower($data['referencedfields']) ?? self::DEFAULT_PRIMARY_KEY_FIELD;
370
            $index['updaterule'] = strtolower($data['updaterule']) ?? self::DEFAULT_UPDATE_RULE;
371
            $index['deleterule'] = strtolower($data['deleterule']) ?? self::DEFAULT_DELETE_RULE;
372
        }
373
374
        return $index;
375
    }
376
377
    private static function checkTable(string $tableName, string $path, bool $create = true): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
478
            if (in_array(strtolower($type), $types)) {
479
                return $index;
480
            }
481
        }
482
        Debug::message($type . ' not found in DBSchema::getTypeOf()');
483
        return 'text';
484
    }
485
486
    private static function _splitType(string $originalType): array
0 ignored issues
show
Unused Code introduced by
The method _splitType() is not used, and could be removed.

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

Loading history...
487
    {
488
        $replacesSources = [
489
            'character varying',
490
            // 'timestamp without time zone',
491
            'double precision',
492
        ];
493
        $replacesDestination = [
494
            'varchar',
495
            // 'timestamp',
496
            'double',
497
        ];
498
        $modifiedType = (str_replace($replacesSources, $replacesDestination, $originalType));
499
500
        if ($originalType !== $modifiedType) {
501
            Debug::message("XML: Uso de '{$originalType}' en lugar de '{$modifiedType}'.");
502
        }
503
        $explode = explode(' ', strtolower($modifiedType));
504
505
        $pos = strpos($explode[0], '(');
506
        if ($pos > 0) {
507
            $begin = $pos + 1;
508
            $end = strpos($explode[0], ')');
509
            $type = substr($explode[0], 0, $pos);
510
            $length = substr($explode[0], $begin, $end - $begin);
511
        } else {
512
            $type = $explode[0];
513
            $length = null;
514
        }
515
516
        $pos = array_search('unsigned', $explode, true);
517
        $unsigned = $pos ? 'yes' : 'no';
518
519
        $pos = array_search('zerofill', $explode, true);
520
        $zerofill = $pos ? 'yes' : 'no';
521
522
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
523
    }
524
525
    private static function getSeed($tableName): string
526
    {
527
        $tableNameWithPrefix = DB::$dbPrefix . $tableName;
528
529
        $seeds = Dispatcher::getFiles('Seeds', 'csv');
530
531
        if (!isset($seeds[$tableName])) {
532
            return '';
533
        }
534
535
        $filename = $seeds[$tableName];
536
        if (!file_exists($filename)) {
537
            return '';
538
        }
539
540
        $rows = 10; // Indicamos el número de registros que vamos a insertar de una vez
541
        $handle = fopen($filename, "r");
542
        if ($handle === false) {
543
            FlashMessages::setError('No ha sido posible abrir el archivo ' . $filename);
544
            return '';
545
        }
546
547
        // Asumimos que la primera fila es la cabecera...
548
        $header = fgetcsv($handle, 0, ';');
549
        if ($header === false) {
550
            FlashMessages::setError('No ha sido posible leer la primera línea del archivo ' . $filename);
551
            fclose($handle);
552
            return '';
553
        }
554
555
        $sqlHeader = "INSERT INTO `{$tableNameWithPrefix}` (`" . implode('`, `', $header) . '`) VALUES ';
556
        $row = 0;
557
        $sqlData = [];
558
        while (($data = fgetcsv($handle, 0, ';')) !== false) {
559
            // Entrecomillamos lo que no sea null.
560
            foreach ($data as $key => $datum) {
561
                if (mb_strtoupper($datum) !== 'NULL') {
562
                    $data[$key] = "'$datum'";
563
                }
564
            }
565
566
            if ($row % $rows === 0) {
567
                if (count($sqlData) > 0) {
568
                    $result .= ($sqlHeader . implode(', ', $sqlData) . ';' . PHP_EOL);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result seems to be never defined.
Loading history...
569
                }
570
                $sqlData = [];
571
            }
572
            $sqlData[] = '(' . implode(', ', $data) . ')';
573
            $row++;
574
        }
575
        if (count($sqlData) > 0) {
576
            $result .= ($sqlHeader . implode(', ', $sqlData) . ';' . PHP_EOL);
577
        }
578
        fclose($handle);
579
580
        return $result;
581
    }
582
583
    private static function _updateField(string $tableName, string $fieldName, array $structure): string
0 ignored issues
show
Unused Code introduced by
The method _updateField() is not used, and could be removed.

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

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

887
    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...
888
    {
889
        $data = Yaml::parseFile($path);
890
891
        $result = [];
892
        foreach ($data['fields'] ?? [] as $key => $datum) {
893
            $datum['key'] = $key;
894
            $result['fields'][$key]['db'] = DB::normalizeFromYaml($datum);
0 ignored issues
show
Bug introduced by
The method normalizeFromYaml() 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

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

895
            /** @scrutinizer ignore-call */ 
896
            $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...
896
            if ($result['fields'][$key]['type'] === 'autoincrement') {
897
                // TODO: Ver cómo tendría que ser la primary key
898
                $result['indexes']['primary'] = $key;
899
            }
900
        }
901
        foreach ($data['indexes'] ?? [] as $key => $datum) {
902
            $datum['key'] = $key;
903
            $result['indexes'][$key] = $datum;
904
        }
905
906
        /*
907
        Igual conviene crear una clase:
908
        - DBSchema (con los datos de la base de datos real)
909
        - DefinedSchema (con los datos definidos)
910
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
911
912
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
913
    oportunas siempre que no suponga una pérdida de datos.
914
        */
915
916
        return $result;
917
    }
918
919
    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...
920
    {
921
        $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...
922
        if (!file_exists($yamlSourceFilename)) {
923
            dump('No existe el archivo ' . $yamlSourceFilename);
924
        }
925
926
        $data = Yaml::parseFile($yamlSourceFilename);
927
928
        $result = [];
929
        foreach ($data as $key => $datum) {
930
            $datum['key'] = $key;
931
            $result[$key] = Schema::normalize($datum);
932
        }
933
934
        /*
935
        Igual conviene crear una clase:
936
        - DBSchema (con los datos de la base de datos real)
937
        - DefinedSchema (con los datos definidos)
938
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
939
940
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
941
    oportunas siempre que no suponga una pérdida de datos.
942
        */
943
944
        return $result;
945
    }
946
947
    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

947
    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...
948
    {
949
        $result = [];
950
        return $result;
951
    }
952
953
    private static function _getRelated($tableName): array
0 ignored issues
show
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...
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

953
    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...
954
    {
955
        $result = [];
956
        return $result;
957
    }
958
}
959