Test Failed
Push — main ( 31a78f...703d27 )
by Rafael
10:23
created

Schema::_createFields()   B

Complexity

Conditions 10
Paths 66

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 28
nc 66
nop 2
dl 0
loc 45
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Copyright (C) 2022-2023  Rafael San José Tovar   <[email protected]>
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License
16
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
 */
18
19
namespace Alxarafe\Database;
20
21
use Alxarafe\Core\Helpers\Dispatcher;
22
use Alxarafe\Core\Singletons\Config;
23
use Alxarafe\Core\Singletons\Debug;
24
use Alxarafe\Core\Singletons\FlashMessages;
25
use Alxarafe\Core\Singletons\Translator;
26
use Alxarafe\Core\Utils\MathUtils;
27
use DebugBar\DebugBarException;
28
use Symfony\Component\Yaml\Yaml;
29
30
/**
31
 * Class Schema
32
 *
33
 * La clase abstracta Schema, define un esquema de base de datos teórico al que
34
 * se traduce la base de datos real y viceversa, de manera que el código sea
35
 * en la medida de lo posible, no dependiente de la base de datos real.
36
 *
37
 * TODO: ¿La información cacheada se procesa en YamlSchema o no merece la pena?
38
 *
39
 * @author  Rafael San José Tovar <[email protected]>
40
 * @version 2023.0101
41
 *
42
 * @package Alxarafe\Database
43
 */
44
class Schema
45
{
46
    /**
47
     * Tipo entero. Número sin decimales.
48
     */
49
    public const TYPE_INTEGER = 'integer';
50
51
    /**
52
     * Tipo real o coma flotante. Número con decimales. Puede dar problema con redondeos.
53
     */
54
    public const TYPE_FLOAT = 'float';
55
56
    /**
57
     * Tipo numérico de coma fija. Número con N decimales y precisión absoluta.
58
     * Es igual que un integer, pero se asume que un número determinado de dígitos son decimales.
59
     */
60
    public const TYPE_DECIMAL = 'decimal';
61
62
    /**
63
     * Tipo cadena de texto
64
     */
65
    public const TYPE_STRING = 'string';
66
67
    /**
68
     * Tipo bloque de texto
69
     */
70
    public const TYPE_TEXT = 'text';
71
72
    /**
73
     * Tipo fecha
74
     */
75
    public const TYPE_DATE = 'date';
76
77
    /**
78
     * Tipo hora
79
     */
80
    public const TYPE_TIME = 'time';
81
82
    /**
83
     * Tipo fecha + hora.
84
     * TODO: Hay que revisar el tema de la zona horaria.
85
     *       De lógica, siempre se debe de almacenar como UTC y convertir al guardar y leer.
86
     */
87
    public const TYPE_DATETIME = 'datetime';
88
89
    /**
90
     * Tipo lógico: TRUE o FALSE.
91
     */
92
    public const TYPE_BOOLEAN = 'bool';
93
94
    /**
95
     * Longitud de un string si no se ha detallado ninguna
96
     */
97
    public const DEFAULT_STRING_LENGTH = 50;
98
99
    /**
100
     * Bytes que usará un integer si no se ha detallado tamaño
101
     */
102
    public const DEFAULT_INTEGER_SIZE = 4;
103
104
    /**
105
     * Si un integer usa signo por defecto o no. True si no utiliza signo por defecto.
106
     */
107
    public const DEFAULT_INTEGER_UNSIGNED = true;
108
109
    /**
110
     * Retorno de carro y salto de línea
111
     */
112
    const CRLF = "\r\n";
113
114
    /**
115
     * Contiene la definición ampliada de la estructura de la base de datos.
116
     *
117
     * @var array
118
     */
119
    public static array $bbddStructure;
120
121
    /**
122
     * Realiza una comprobación integral de la base de datos, verificando que la configuración
123
     * indicada en los archivos yaml de configuración de tablas, se corresponde con lo
124
     * creado en la base de datos.
125
     * Adecúa la base de datos a la información facilitada por los archivos yaml.
126
     *
127
     * @author Rafael San José Tovar <[email protected]>
128
     *
129
     * @throws DebugBarException
130
     */
131
    public static function checkDatabaseStructure()
132
    {
133
        DB::$engine->exec('DROP TABLE `tc_menus`;');
134
        foreach (YamlSchema::getTables() as $key => $table) {
135
            if (!file_exists($table)) {
136
                Debug::message('No existe la tabla ' . $table);
137
            }
138
            Debug::message("Verificando la tabla $key, definida en $table.");
139
            if (!static::checkStructure($key, $table)) {
140
                FlashMessages::setError('Error al comprobar la estructura de la tabla ' . $table);
141
            }
142
        }
143
    }
144
145
    private static function getGenericType(array $data): array
146
    {
147
        $result = [];
148
        $type = $data['type'];
149
150
        switch ($type) {
151
            case 'autoincrement':
152
            case 'relationship':
153
                $type = Schema::TYPE_INTEGER;
154
                $result['size'] = 8;
155
                break;
156
        }
157
158
        // Si es un tipo genérico, se retorna automáticamente.
159
        if (isset(DB::$helper::$types[$type])) {
160
            $result['generictype'] = $type;
161
            return $result;
162
        }
163
164
        foreach (DB::$helper::$types as $key => $types) {
165
            if (in_array($type, $types)) {
166
                $result['generictype'] = $key;
167
                return $result;
168
            }
169
        }
170
171
        Debug::message("No se ha encontrado genérico para {$type}. Se asume 'string'.");
172
        $result['generictype'] = 'string';
173
        return $result;
174
    }
175
176
    private static function yamlFieldAnyToSchema(string $genericType, array $data): array
177
    {
178
        $type = DB::$helper::getDataTypes()[$data['type']];
179
        $result = [];
180
        $result['genericType'] = $genericType;
181
        $result['dbtype'] = $type;
182
        dump(['ANY' => $data]);
183
        return $result;
184
    }
185
186
    /**
187
     * Cumplimenta los datos faltantes del yaml de definición al de caché para
188
     * tipos enteros.
189
     * Posibles valores que se pueden recibir en $data:
190
     * - min, es el valor mínimo aceptado por el entero.
191
     * - max, es el valor máximo aceptado por el entero.
192
     * - size, es el número de bytes que ocupa el entero.
193
     * - unsigned, indica si necesita signo o no.
194
     * La respuesta puede modificar algunos de esos valores.
195
     *
196
     * @author Rafael San José Tovar <[email protected]>
197
     *
198
     * @param array $data
199
     *
200
     * @return array
201
     */
202
    private static function yamlFieldIntegerToSchema(array $data): array
203
    {
204
        $min = $data['min'] ?? null;
205
        $max = $data['max'] ?? null;
206
207
        // Si hay mínimo y máximo, se ajusta el resto de parámetros a esos datos.
208
        if ($min !== null && $max !== null) {
209
            $unsigned = $min >= 0;
210
            $size = MathUtils::howManyBytes($max, $min, $unsigned);
211
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
212
            return [
213
                'dbtype' => $extra['dbtype'],
214
                'min' => $min,
215
                'max' => $max,
216
                'size' => $extra['size'],
217
                'unsigned' => $extra['unsigned'],
218
            ];
219
        }
220
221
        // Si tenemos máximo, pero no tenemos mínimo, se ajusta al máximo y se toma signo por defecto
222
        if ($max !== null) {
223
            $unsigned = $data['unsigned'] ?? self::DEFAULT_INTEGER_UNSIGNED;
224
            $size = MathUtils::howManyBytes($max);
225
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
226
            return [
227
                'dbtype' => $extra['dbtype'],
228
                'min' => $extra['min'],
229
                'max' => $max,
230
                'size' => $extra['size'],
231
                'unsigned' => $extra['unsigned'],
232
            ];
233
        }
234
235
        // Si lo que no tenemos es máximo, ajustamos el tamaño al mínimo y se ajusta el signo al mínimo
236
        if ($min !== null) {
237
            $unsigned = $min >= 0;
238
            $size = MathUtils::howManyBytes($min, $min, $unsigned);
239
            $extra = DB::$helper::getIntegerMinMax($size, $unsigned);
240
            return [
241
                'dbtype' => $extra['dbtype'],
242
                'min' => 0, // TODO: Si unsigned, será el menor entero negativo.
243
                'max' => $max,
244
                'size' => $extra['size'],
245
                'unsigned' => $extra['unsigned'],
246
            ];
247
        }
248
249
        // Mínimo y máximo son nulos
250
        $size = $data['size'] ?? self::DEFAULT_INTEGER_SIZE;
251
        $unsigned = $data['unsigned'] ?? self::DEFAULT_INTEGER_UNSIGNED;
252
        return DB::$helper::getIntegerMinMax($size, $unsigned);
253
    }
254
255
    private static function yamlFieldStringToSchema(array $data): array
256
    {
257
        return [
258
            'dbtype' => 'varchar',
259
            'minlength' => $data['minlength'] ?? 0,
260
            'length' => $data['length'] ?? self::DEFAULT_STRING_LENGTH,
261
        ];
262
    }
263
264
    /**
265
     * Tomando la definición de un campo de una tabla en un archivo yaml de definición,
266
     * genera toda la información necesaria para la creación, actualización de la tabla
267
     * y el mantenimiento de los datos del campo.
268
     *
269
     * @author Rafael San José Tovar <[email protected]>
270
     *
271
     * @param array $data
272
     *
273
     * @return array
274
     */
275
    public static function yamlFieldToSchema(array $data): array
276
    {
277
        /**
278
         * Los datos que vienen del yaml son los siguientes:
279
         * - name es el nombre del campo
280
         * - type es el tipo genérico del campo
281
         * El resto, será dependiente del tipo genérico de dato.
282
         * Puede ocurrir que no venga un tipo genérico, sino uno fijo, en ese caso
283
         * se intentará corregir, pero se notificará en la barra de depuración.
284
         * Si hay error en la conversión, se generará un error.
285
         */
286
        $column = [];
287
        $column['name'] = (string) $data['name'];
288
        $column['type'] = (string) $data['type'];
289
        $column['nullable'] = $data['nullable'] ?? 'yes';
290
        $column['default'] = $data['default'] ?? null;
291
        $column = array_merge($column, self::getGenericType($data));
292
293
        switch ($column['generictype']) {
294
            case Schema::TYPE_INTEGER:
295
                foreach (['min', 'max', 'unsigned', 'size'] as $field) {
296
                    if (isset($data[$field])) {
297
                        $column[$field] = $data[$field];
298
                        unset($data[$field]);
299
                    }
300
                }
301
                $result = self::yamlFieldIntegerToSchema($column);
302
                break;
303
            case Schema::TYPE_STRING:
304
                foreach (['minlength', 'length'] as $field) {
305
                    if (isset($data[$field])) {
306
                        $column[$field] = $data[$field];
307
                        unset($data[$field]);
308
                    }
309
                }
310
                $result = self::yamlFieldStringToSchema($column);
311
                break;
312
            case Schema::TYPE_FLOAT:
313
            case Schema::TYPE_DECIMAL:
314
            case Schema::TYPE_TEXT:
315
            case Schema::TYPE_DATE:
316
            case Schema::TYPE_TIME:
317
            case Schema::TYPE_DATETIME:
318
            case Schema::TYPE_BOOLEAN:
319
            default:
320
                $result = self::yamlFieldAnyToSchema($column['generictype'], $column);
321
        }
322
323
        unset($data['name']);
324
        unset($data['type']);
325
        unset($data['default']);
326
        unset($data['nullable']);
327
328
        $column = array_merge($column, $result);
329
330
        if (count($data) > 0) {
331
            dump(['Ignorado en data' => $data]);
332
        }
333
        return $column;
334
    }
335
336
    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

336
    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

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

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

404
            if (!self::/** @scrutinizer ignore-call */ updateTable($tableName)) {

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...
405
                FlashMessages::setError(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
406
            }
407
        } else {
408
            Debug::message('La tabla ' . $tableName . ' NO existe');
409
            if (!self::createTable($tableName)) {
410
                FlashMessages::setError(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
411
            }
412
        }
413
414
        if (!YamlSchema::saveCacheYamlFile(YamlSchema::YAML_CACHE_TABLES_DIR, $tableName, self::$bbddStructure[$tableName])) {
415
            Debug::message('No se ha podido guardar la información de caché para la tabla ' . $tableName);
416
            return false;
417
        }
418
        return true;
419
    }
420
421
    /**
422
     * Obtiene el tipo genérico del tipo de dato que se le ha pasado.
423
     *
424
     * @author  Rafael San José Tovar <[email protected]>
425
     * @version 2023.0101
426
     *
427
     * @param string $type
428
     *
429
     * @return string
430
     */
431
    public static function _getTypeOf(string $type): string
432
    {
433
        foreach (DB::getDataTypes() as $index => $types) {
0 ignored issues
show
Bug introduced by
The method getDataTypes() does not exist on Alxarafe\Database\DB. Did you maybe mean _getDataTypes()? ( Ignorable by Annotation )

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

433
        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...
434
            if (in_array(strtolower($type), $types)) {
435
                return $index;
436
            }
437
        }
438
        Debug::message($type . ' not found in DBSchema::getTypeOf()');
439
        return 'text';
440
    }
441
442
    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...
443
    {
444
        $replacesSources = [
445
            'character varying',
446
            // 'timestamp without time zone',
447
            'double precision',
448
        ];
449
        $replacesDestination = [
450
            'varchar',
451
            // 'timestamp',
452
            'double',
453
        ];
454
        $modifiedType = (str_replace($replacesSources, $replacesDestination, $originalType));
455
456
        if ($originalType !== $modifiedType) {
457
            Debug::message("XML: Uso de '{$originalType}' en lugar de '{$modifiedType}'.");
458
        }
459
        $explode = explode(' ', strtolower($modifiedType));
460
461
        $pos = strpos($explode[0], '(');
462
        if ($pos > 0) {
463
            $begin = $pos + 1;
464
            $end = strpos($explode[0], ')');
465
            $type = substr($explode[0], 0, $pos);
466
            $length = substr($explode[0], $begin, $end - $begin);
467
        } else {
468
            $type = $explode[0];
469
            $length = null;
470
        }
471
472
        $pos = array_search('unsigned', $explode, true);
473
        $unsigned = $pos ? 'yes' : 'no';
474
475
        $pos = array_search('zerofill', $explode, true);
476
        $zerofill = $pos ? 'yes' : 'no';
477
478
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
479
    }
480
481
    /**
482
     * Create a table in the database.
483
     * Build the default fields, indexes and values defined in the model.
484
     *
485
     * @param string $tableName
486
     *
487
     * @return bool
488
     * @throws DebugBarException
489
     */
490
    private static function createTable(string $tableName): bool
491
    {
492
        $tabla = self::$bbddStructure[$tableName];
493
        $sql = self::createFields($tableName, $tabla['fields']['db']);
494
495
        /*
496
        foreach ($tabla['indexes'] as $name => $index) {
497
            $sql .= self::createIndex($tableName, $name, $index);
498
        }
499
500
        if (isset($tabla['values'])) {
501
            $sql .= self::setValues($tableName, $tabla['values']);
502
        } else {
503
            $sql .= self::getSeed($tableName);
504
        }
505
        */
506
507
        return Engine::exec($sql);
508
    }
509
510
    private static function _getSeed($tableName): string
0 ignored issues
show
Unused Code introduced by
The method _getSeed() is not used, and could be removed.

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

Loading history...
511
    {
512
        $tableNameWithPrefix = DB::$dbPrefix . $tableName;
513
514
        $seeds = Dispatcher::getFiles('Seeds', 'csv');
515
516
        if (!isset($seeds[$tableName])) {
517
            return '';
518
        }
519
520
        $filename = $seeds[$tableName];
521
        if (!file_exists($filename)) {
522
            return '';
523
        }
524
525
        $rows = 10; // Indicamos el número de registros que vamos a insertar de una vez
526
        $handle = fopen($filename, "r");
527
        if ($handle === false) {
528
            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

528
            FlashMessages::/** @scrutinizer ignore-call */ 
529
                           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...
529
            return '';
530
        }
531
532
        // Asumimos que la primera fila es la cabecera...
533
        $header = fgetcsv($handle, 0, ';');
534
        if ($header === false) {
535
            FlashMessages::addError('No ha sido posible leer la primera línea del archivo ' . $filename);
536
            fclose($handle);
537
            return '';
538
        }
539
540
        $sqlHeader = "INSERT INTO `{$tableNameWithPrefix}` (`" . implode('`, `', $header) . '`) VALUES ';
541
        $row = 0;
542
        $sqlData = [];
543
        while (($data = fgetcsv($handle, 0, ';')) !== false) {
544
            // Entrecomillamos lo que no sea null.
545
            foreach ($data as $key => $datum) {
546
                if (mb_strtoupper($datum) !== 'NULL') {
547
                    $data[$key] = "'$datum'";
548
                }
549
            }
550
551
            if ($row % $rows === 0) {
552
                if (count($sqlData) > 0) {
553
                    $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...
554
                }
555
                $sqlData = [];
556
            }
557
            $sqlData[] = '(' . implode(', ', $data) . ')';
558
            $row++;
559
        }
560
        if (count($sqlData) > 0) {
561
            $result .= ($sqlHeader . implode(', ', $sqlData) . ';' . PHP_EOL);
562
        }
563
        fclose($handle);
564
565
        return $result;
566
    }
567
568
    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...
569
    {
570
        dump([
571
            'tablename' => $tableName,
572
            'fieldname' => $fieldName,
573
            'new structure' => self::$bbddStructure[$tableName]['fields'][$fieldName],
574
            'structure' => $structure,
575
        ]);
576
        return '';
577
    }
578
579
    private static function _updateTable(string $tableName): bool
0 ignored issues
show
Unused Code introduced by
The method _updateTable() 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...
580
    {
581
        $yamlStructure = self::$bbddStructure[$tableName];
582
        $dbStructure = DB::getColumns($tableName);
0 ignored issues
show
Bug introduced by
The method getColumns() does not exist on Alxarafe\Database\DB. Did you maybe mean _getColumns()? ( Ignorable by Annotation )

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

582
        /** @scrutinizer ignore-call */ 
583
        $dbStructure = DB::getColumns($tableName);

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...
583
584
        $changes = [];
585
        foreach ($yamlStructure['fields'] as $field => $newStructure) {
586
            $oldDb = $dbStructure[$field];
587
            $newDb = $newStructure['db'];
588
589
            $dif = array_diff($oldDb, $newDb);
590
            if (count($dif) > 0) {
591
                $changes[] = DB::modify($tableName, $oldDb, $newDb);
0 ignored issues
show
Bug introduced by
The method modify() does not exist on Alxarafe\Database\DB. Did you maybe mean _modify()? ( Ignorable by Annotation )

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

591
                /** @scrutinizer ignore-call */ 
592
                $changes[] = DB::modify($tableName, $oldDb, $newDb);

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...
592
            }
593
        }
594
595
        if (empty($changes)) {
596
            return true;
597
        }
598
599
        // dump(['changes in ' . $tableName => $changes]);
600
        $result = true;
601
        foreach ($changes as $change) {
602
            $result = $result && Engine::exec($change);
603
        }
604
        return $result;
605
    }
606
607
    /**
608
     * Build the SQL statement to create the fields in the table.
609
     * It can also create the primary key if the auto_increment attribute is defined.
610
     *
611
     * @param string $tablename
612
     * @param array  $fieldList
613
     *
614
     * @return string
615
     */
616
    protected static function _createFields(string $tablename, array $fieldList): string
617
    {
618
        $tablenameWithPrefix = DB::$dbPrefix . $tablename;
619
620
        $sql = "CREATE TABLE $tablenameWithPrefix ( ";
621
        foreach ($fieldList as $index => $column) {
622
            $col = $column['schema'];
623
            if (!isset($col['dbtype'])) {
624
                die('Tipo no especificado en createTable ' . $index);
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

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

821
    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...
822
    {
823
        $data = Yaml::parseFile($path);
824
825
        $result = [];
826
        foreach ($data['fields'] ?? [] as $key => $datum) {
827
            $datum['key'] = $key;
828
            $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

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

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

881
    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...
882
    {
883
        $result = [];
884
        return $result;
885
    }
886
887
    private static function _getRelated($tableName): array
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

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

887
    private static function _getRelated(/** @scrutinizer ignore-unused */ $tableName): array

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

Loading history...
Unused Code introduced by
The method _getRelated() is not used, and could be removed.

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

Loading history...
888
    {
889
        $result = [];
890
        return $result;
891
    }
892
}
893