Test Failed
Push — main ( 82fe17...bda903 )
by Rafael
10:29
created

Schema::yamlIndexToSchema()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 21
rs 9.8333
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 $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

194
    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...
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
                $result = self::yamlFieldAnyToSchema($column['generictype'], $column);
338
                break;
339
            default:
340
                $result = self::yamlFieldAnyToSchema($column['generictype'], $column);
341
        }
342
343
        unset($data['name']);
344
        unset($data['type']);
345
        unset($data['default']);
346
        unset($data['nullable']);
347
348
        $column = array_merge($column, $result);
349
350
        if (count($data) > 0) {
351
            dump(['Ignorado en data' => $data]);
352
        }
353
        return $column;
354
    }
355
356
    private static function yamlIndexToSchema(array $data): array
357
    {
358
        $index = [];
359
360
        $index['column'] = strtolower($data['column']);
361
        if ($data['primary']) {
362
            $index['primary'] = 'yes';
363
            $data['unique'] = 'yes';
364
        }
365
        $index['unique'] = $data['unique'] ?? 'no';
366
367
        // Es una relación
368
        if (isset($data['referencedtable'])) {
369
            $index['name'] = (string) $data['name'];
370
            $index['referencedtable'] = DB::$dbPrefix . strtolower($data['referencedtable']);
371
            $index['referencedfields'] = strtolower($data['referencedfields']) ?? self::DEFAULT_PRIMARY_KEY_FIELD;
372
            $index['updaterule'] = strtolower($data['updaterule']) ?? self::DEFAULT_UPDATE_RULE;
373
            $index['deleterule'] = strtolower($data['deleterule']) ?? self::DEFAULT_DELETE_RULE;
374
        }
375
376
        return $index;
377
    }
378
379
    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

379
    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

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

479
        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...
480
            if (in_array(strtolower($type), $types)) {
481
                return $index;
482
            }
483
        }
484
        Debug::message($type . ' not found in DBSchema::getTypeOf()');
485
        return 'text';
486
    }
487
488
    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...
489
    {
490
        $replacesSources = [
491
            'character varying',
492
            // 'timestamp without time zone',
493
            'double precision',
494
        ];
495
        $replacesDestination = [
496
            'varchar',
497
            // 'timestamp',
498
            'double',
499
        ];
500
        $modifiedType = (str_replace($replacesSources, $replacesDestination, $originalType));
501
502
        if ($originalType !== $modifiedType) {
503
            Debug::message("XML: Uso de '{$originalType}' en lugar de '{$modifiedType}'.");
504
        }
505
        $explode = explode(' ', strtolower($modifiedType));
506
507
        $pos = strpos($explode[0], '(');
508
        if ($pos > 0) {
509
            $begin = $pos + 1;
510
            $end = strpos($explode[0], ')');
511
            $type = substr($explode[0], 0, $pos);
512
            $length = substr($explode[0], $begin, $end - $begin);
513
        } else {
514
            $type = $explode[0];
515
            $length = null;
516
        }
517
518
        $pos = array_search('unsigned', $explode, true);
519
        $unsigned = $pos ? 'yes' : 'no';
520
521
        $pos = array_search('zerofill', $explode, true);
522
        $zerofill = $pos ? 'yes' : 'no';
523
524
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
525
    }
526
527
    private static function getSeed($tableName): string
528
    {
529
        $tableNameWithPrefix = DB::$dbPrefix . $tableName;
530
531
        $seeds = Dispatcher::getFiles('Seeds', 'csv');
532
533
        if (!isset($seeds[$tableName])) {
534
            return '';
535
        }
536
537
        $filename = $seeds[$tableName];
538
        if (!file_exists($filename)) {
539
            return '';
540
        }
541
542
        $rows = 10; // Indicamos el número de registros que vamos a insertar de una vez
543
        $handle = fopen($filename, "r");
544
        if ($handle === false) {
545
            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

545
            FlashMessages::/** @scrutinizer ignore-call */ 
546
                           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...
546
            return '';
547
        }
548
549
        // Asumimos que la primera fila es la cabecera...
550
        $header = fgetcsv($handle, 0, ';');
551
        if ($header === false) {
552
            FlashMessages::addError('No ha sido posible leer la primera línea del archivo ' . $filename);
553
            fclose($handle);
554
            return '';
555
        }
556
557
        $sqlHeader = "INSERT INTO `{$tableNameWithPrefix}` (`" . implode('`, `', $header) . '`) VALUES ';
558
        $row = 0;
559
        $sqlData = [];
560
        while (($data = fgetcsv($handle, 0, ';')) !== false) {
561
            // Entrecomillamos lo que no sea null.
562
            foreach ($data as $key => $datum) {
563
                if (mb_strtoupper($datum) !== 'NULL') {
564
                    $data[$key] = "'$datum'";
565
                }
566
            }
567
568
            if ($row % $rows === 0) {
569
                if (count($sqlData) > 0) {
570
                    $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...
571
                }
572
                $sqlData = [];
573
            }
574
            $sqlData[] = '(' . implode(', ', $data) . ')';
575
            $row++;
576
        }
577
        if (count($sqlData) > 0) {
578
            $result .= ($sqlHeader . implode(', ', $sqlData) . ';' . PHP_EOL);
579
        }
580
        fclose($handle);
581
582
        return $result;
583
    }
584
585
    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...
586
    {
587
        dump([
588
            'tablename' => $tableName,
589
            'fieldname' => $fieldName,
590
            'new structure' => self::$bbddStructure[$tableName]['fields'][$fieldName],
591
            'structure' => $structure,
592
        ]);
593
        return '';
594
    }
595
596
    /**
597
     * Create a table in the database.
598
     * Build the default fields, indexes and values defined in the model.
599
     *
600
     * @param string $tableName
601
     *
602
     * @return bool
603
     * @throws DebugBarException
604
     */
605
    private static function createTable(string $tableName): bool
606
    {
607
        $tabla = self::$bbddStructure[$tableName];
608
        $sql = self::createFields($tableName, $tabla['fields']['db']);
609
610
        foreach ($tabla['indexes'] as $name => $index) {
611
            $sql .= DB::createIndex($tableName, $name, $index);
612
        }
613
614
        if (isset($tabla['values'])) {
615
            $sql .= self::setValues($tableName, $tabla['values']);
616
        } else {
617
            $sql .= self::getSeed($tableName);
618
        }
619
620
        return Engine::exec($sql);
621
    }
622
623
    private static function updateTable(string $tableName): bool
624
    {
625
        $yamlStructure = self::$bbddStructure[$tableName];
626
        $dbStructure = DB::getColumns($tableName);
627
628
        $changes = [];
629
        foreach ($yamlStructure['fields']['db'] as $field => $newStructure) {
630
            $oldStructure = DB::$helper::sanitizeDbStructure($yamlStructure['fields']['schema'][$field]['generictype'], $dbStructure[$field]);
631
            $dif = array_diff($oldStructure, $newStructure);
632
            if (count($dif) > 0) {
633
                $changes[] = DB::modify($tableName, $oldStructure, $newStructure);
634
            }
635
        }
636
637
        $indexes = DB::getIndexes($tableName);
638
        $newStructureArray = $yamlStructure['indexes'];
639
        // Primero se eliminan los índices que ya no existen
640
        foreach ($indexes as $index => $oldStructure) {
641
            $newStructure = $newStructureArray[$index] ?? null;
642
            if (!isset($newStructure)) {
643
                $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

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

888
    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...
889
    {
890
        $data = Yaml::parseFile($path);
891
892
        $result = [];
893
        foreach ($data['fields'] ?? [] as $key => $datum) {
894
            $datum['key'] = $key;
895
            $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

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

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

948
    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...
949
    {
950
        $result = [];
951
        return $result;
952
    }
953
954
    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

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