Test Failed
Push — main ( e26c51...cb447d )
by Rafael
10:44
created

Schema::checkStructure()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 22
nc 6
nop 3
dl 0
loc 39
rs 8.6346
c 0
b 0
f 0
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\Singletons\Config;
22
use Alxarafe\Core\Singletons\Debug;
23
use Alxarafe\Core\Singletons\FlashMessages;
24
use Alxarafe\Core\Singletons\Translator;
25
use DebugBar\DebugBarException;
26
use Symfony\Component\Yaml\Yaml;
27
28
/**
29
 * Class Schema
30
 *
31
 * La clase abstracta Schema, define un esquema de base de datos teórico al que
32
 * se traduce la base de datos real y viceversa, de manera que el código sea
33
 * en la medida de lo posible, no dependiente de la base de datos real.
34
 *
35
 * TODO: ¿La información cacheada se procesa en YamlSchema o no merece la pena?
36
 *
37
 * @author  Rafael San José Tovar <[email protected]>
38
 * @version 2023.0101
39
 *
40
 * @package Alxarafe\Database
41
 */
42
class Schema
43
{
44
    /**
45
     * Tipo entero. Número sin decimales.
46
     */
47
    public const TYPE_INTEGER = 'integer';
48
49
    /**
50
     * Tipo real o coma flotante. Número con decimales. Puede dar problema con redondeos.
51
     */
52
    public const TYPE_FLOAT = 'float';
53
54
    /**
55
     * Tipo numérico de coma fija. Número con N decimales y precisión absoluta.
56
     * Es igual que un integer, pero se asume que un número determinado de dígitos son decimales.
57
     */
58
    public const TYPE_DECIMAL = 'decimal';
59
60
    /**
61
     * Tipo cadena de texto
62
     */
63
    public const TYPE_STRING = 'string';
64
65
    /**
66
     * Tipo bloque de texto
67
     */
68
    public const TYPE_TEXT = 'text';
69
70
    /**
71
     * Tipo fecha
72
     */
73
    public const TYPE_DATE = 'date';
74
75
    /**
76
     * Tipo hora
77
     */
78
    public const TYPE_TIME = 'time';
79
80
    /**
81
     * Tipo fecha + hora.
82
     * TODO: Hay que revisar el tema de la zona horaria.
83
     *       De lógica, siempre se debe de almacenar como UTC y convertir al guardar y leer.
84
     */
85
    public const TYPE_DATETIME = 'datetime';
86
87
    /**
88
     * Tipo lógico: TRUE o FALSE.
89
     */
90
    public const TYPE_BOOLEAN = 'bool';
91
92
    /**
93
     * Retorno de carro y salto de línea
94
     */
95
    const CRLF = "\r\n";
96
97
    /**
98
     * Contiene la definición ampliada de la estructura de la base de datos.
99
     *
100
     * @var array
101
     */
102
    public static array $bbddStructure;
103
104
    public static function checkDatabaseStructure()
105
    {
106
        foreach (YamlSchema::getTables() as $key => $table) {
107
            if (!file_exists($table)) {
108
                Debug::message('No existe la tabla ' . $table);
109
            }
110
            dump("Verificando la tabla $key, definida en $table.");
111
            if (!static::checkStructure($key, $table)) {
112
                FlashMessages::setError('Error al comprobar la estructura de la tabla ' . $table);
113
            }
114
        }
115
    }
116
117
    /**
118
     * Return true if $tableName exists in database
119
     *
120
     * @param string $tableName
121
     *
122
     * @return bool
123
     * @throws DebugBarException
124
     */
125
    public static function tableExists($tableName): bool
126
    {
127
        $tableNameWithPrefix = Config::$dbPrefix . $tableName;
128
        $dbName = Config::$dbName;
129
        $sql = "SELECT COUNT(*) AS Total FROM information_schema.tables WHERE table_schema = '{$dbName}' AND table_name='{$tableNameWithPrefix}'";
130
131
        $data = Engine::select($sql);
132
        $result = reset($data);
133
134
        return $result['Total'] === '1';
135
    }
136
137
    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

137
    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...
138
    {
139
        $data = Yaml::parseFile($path);
140
141
        dump([$path => $data]);
142
143
        $result = [];
144
        foreach ($data['fields'] ?? [] as $key => $datum) {
145
            $datum['key'] = $key;
146
            $result['fields'][$key]['db'] = DB::normalizeFromYaml($datum);
147
            $result['fields'][$key]['info'] = Schema::normalize($datum);
148
            if ($result['fields'][$key]['type'] === 'autoincrement') {
149
                // TODO: Ver cómo tendría que ser la primary key
150
                $result['indexes']['primary'] = $key;
151
            }
152
        }
153
        foreach ($data['indexes'] ?? [] as $key => $datum) {
154
            $datum['key'] = $key;
155
            $result['indexes'][$key] = $datum;
156
        }
157
158
        /*
159
        Igual conviene crear una clase:
160
        - DBSchema (con los datos de la base de datos real)
161
        - DefinedSchema (con los datos definidos)
162
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
163
164
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
165
    oportunas siempre que no suponga una pérdida de datos.
166
        */
167
168
        return $result;
169
    }
170
171
    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...
172
    {
173
        $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...
174
        if (!file_exists($yamlSourceFilename)) {
175
            dump('No existe el archivo ' . $yamlSourceFilename);
176
        }
177
178
        $data = Yaml::parseFile($yamlSourceFilename);
179
180
        $result = [];
181
        foreach ($data as $key => $datum) {
182
            $datum['key'] = $key;
183
            $result[$key] = Schema::normalize($datum);
184
        }
185
186
        /*
187
        Igual conviene crear una clase:
188
        - DBSchema (con los datos de la base de datos real)
189
        - DefinedSchema (con los datos definidos)
190
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
191
192
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
193
    oportunas siempre que no suponga una pérdida de datos.
194
        */
195
196
        return $result;
197
    }
198
199
    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

199
    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...
200
    {
201
        $result = [];
202
        return $result;
203
    }
204
205
    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

205
    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...
206
    {
207
        $result = [];
208
        return $result;
209
    }
210
211
    private static function getSeed($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

211
    private static function getSeed(/** @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 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...
212
    {
213
        $result = [];
214
        return $result;
215
    }
216
217
    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

217
    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

217
    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...
218
    {
219
        $yaml = Yaml::parseFile($path);
220
        $fields = $yaml['fields'] ?? [];
221
222
        $data = [];
223
        foreach ($fields as $key => $field) {
224
            $field['key'] = $key;
225
            $schema = DB::yamlFieldToSchema($field);
226
            $data[$key]['db'] = DB::yamlFieldToDb($schema);
227
            $data[$key]['schema'] = $schema;
228
        }
229
230
        $indexes = $yaml['indexes'] ?? [];
231
232
        return [
233
            'fields' => $data,
234
            'indexes' => $indexes,
235
        ];
236
    }
237
238
    /**
239
     * Comprueba la estructura de la tabla y la crea si no existe y así se solicita.
240
     * Si los datos de la estructura no están en la caché, los regenera y almacena.
241
     * Al regenerar los datos para la caché, también realiza una verificación de
242
     * la estructura por si hay cambios que aplicar en la misma.
243
     *
244
     * TODO: Es mejor que haya un checkStructure que genere TODAS las tablas e índices
245
     * Ese checkstructure se debe de generar tras limpiar caché.
246
     * La caché deberá de limpiarse cada vez que se active o desactive un módulo.
247
     * El último paso de la generación de tablas, sería comprobar las dependencias
248
     * de tablas para saber cuántas tablas usan una constraint de cada tabla para poder
249
     * realizar cambios en la base de datos y tener una visión más nítida de la misma en
250
     * cualquier momento, si bien, esa estructura no será clara hasta que no se hayan leído
251
     * todas, y si hay un cambio entre medias, pues igual la única solución viable es
252
     * determinarlo por la propia base de datos.
253
     *
254
     * @author  Rafael San José Tovar <[email protected]>
255
     * @version 2023.0105
256
     *
257
     * @param string $tableName
258
     * @param bool   $create
259
     *
260
     * @return bool
261
     */
262
    private static function checkStructure(string $tableName, string $path, bool $create = true): bool
263
    {
264
        // Si el dato ya ha sido cargado, retornamos porque no hay nada que hacer.
265
        if (!empty(self::$bbddStructure[$tableName])) {
266
            return true;
267
        }
268
269
        // Si no está, pero está cacheado, se recupera de la caché y se retorna.
270
        self::$bbddStructure[$tableName] = YamlSchema::loadCacheYamlFile(YamlSchema::YAML_CACHE_TABLES_DIR, $tableName);
271
        if (!empty(self::$bbddStructure[$tableName])) {
272
            return true;
273
        }
274
275
        // Si no está cacheado, entonces hay que comprobar si hay cambios en la estructura y regenerarla.
276
        self::$bbddStructure[$tableName] = self::checkTable($tableName, $path, $create);
277
278
        dump(self::$bbddStructure);
279
280
        if (DB::tableExists($tableName)) {
281
            dump('La tabla ' . $tableName . ' existe');
282
            if (!self::updateTable($tableName)) {
283
                dump(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
284
                FlashMessages::setError(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
285
            }
286
        } else {
287
            dump('La tabla ' . $tableName . ' NO existe');
288
            if (!self::createTable($tableName)) {
289
                dump(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
290
                FlashMessages::setError(Translator::trans('table_creation_error', ['%tablename%' => $tableName]));
291
            }
292
        }
293
294
        die('Por aquí vamos ahora...');
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 boolean. 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...
295
296
        if (!YamlSchema::saveCacheYamlFile(YamlSchema::YAML_CACHE_TABLES_DIR, $tableName, self::$bbddStructure[$tableName])) {
0 ignored issues
show
Unused Code introduced by
IfNode is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
297
            Debug::message('No se ha podido guardar la información de caché para la tabla ' . $tableName);
298
            return false;
299
        }
300
        return true;
301
    }
302
303
    /**
304
     * Obtiene el tipo genérico del tipo de dato que se le ha pasado.
305
     *
306
     * @author  Rafael San José Tovar <[email protected]>
307
     * @version 2023.0101
308
     *
309
     * @param string $type
310
     *
311
     * @return string
312
     */
313
    public static function getTypeOf(string $type): string
314
    {
315
        foreach (DB::getDataTypes() as $index => $types) {
316
            if (in_array(strtolower($type), $types)) {
317
                return $index;
318
            }
319
        }
320
        Debug::message($type . ' not found in DBSchema::getTypeOf()');
321
        return 'text';
322
    }
323
324
    private static function splitType(string $originalType): array
325
    {
326
        $replacesSources = [
327
            'character varying',
328
            // 'timestamp without time zone',
329
            'double precision',
330
        ];
331
        $replacesDestination = [
332
            'varchar',
333
            // 'timestamp',
334
            'double',
335
        ];
336
        $modifiedType = (str_replace($replacesSources, $replacesDestination, $originalType));
337
338
        if ($originalType !== $modifiedType) {
339
            Debug::message("XML: Uso de '{$originalType}' en lugar de '{$modifiedType}'.");
340
        }
341
        $explode = explode(' ', strtolower($modifiedType));
342
343
        $pos = strpos($explode[0], '(');
344
        if ($pos > 0) {
345
            $begin = $pos + 1;
346
            $end = strpos($explode[0], ')');
347
            $type = substr($explode[0], 0, $pos);
348
            $length = substr($explode[0], $begin, $end - $begin);
349
        } else {
350
            $type = $explode[0];
351
            $length = null;
352
        }
353
354
        $pos = array_search('unsigned', $explode, true);
355
        $unsigned = $pos ? 'yes' : 'no';
356
357
        $pos = array_search('zerofill', $explode, true);
358
        $zerofill = $pos ? 'yes' : 'no';
359
360
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
361
    }
362
363
    /**
364
     * Toma los datos del fichero de definición de una tabla y genera el definitivo.
365
     *
366
     * TODO: Tiene que recopilar la estructura tal y como la tiene que retornar la base de datos.
367
     *       Se le pasan los datos de definición a SqlHelper para que retorne la estructura física final.
368
     *       Se generan datos comunes a cualquier base de datos para poder trabajar con comodidad.
369
     *
370
     * Si tomamos como ejemplo MySQL, se invocará a SqlMySql para que retorne algo parecido a ésto:
371
     *
372
     *   "database" => array:7 [▼
373
     *     "field" => "id"
374
     *     "type" => "bigint"
375
     *     "length" => 20
376
     *     "default" => null
377
     *     "nullable" => "YES"
378
     *     "primary" => ""
379
     *     "autoincrement" => 1
380
     *   ]
381
     *
382
     * En SqlMySql existe un normalize que convierte la respuesta a ese aspecto.
383
     *
384
     *   "logical" => array:9 [▼
385
     *     "key" => "id"
386
     *     "type" => "autoincrement"
387
     *     "dbtype" => "bigint (20) unsigned"
388
     *     "realtype" => "bigint"
389
     *     "generictype" => "integer"
390
     *     "null" => "YES"
391
     *     "default" => null
392
     *     "min" => 0
393
     *     "max" => 1.844674407371E+19
394
     *   ]
395
     *
396
     * Porque con esa estructura será mucho más facil crear el campo o compararlo con el de la base de datos.
397
     *
398
     * Aparte, se necesitará información adicional, que será común a todas las bases de datos:
399
     *
400
     * @author  Rafael San José Tovar <[email protected]>
401
     * @version 2022.1224
402
     *
403
     * @param array $structure
404
     *
405
     * @return array
406
     */
407
    protected static function normalize(array $structure): array
408
    {
409
        $column = [];
410
        $key = (string) $structure['key'];
411
        $type = (string) $structure['type'];
412
        $column['key'] = $key;
413
414
        /**
415
         * Entrada:
416
         * - type es el tipo lógico del campo y tiene que estar definido como índice en
417
         *   TYPES, o ser uno de los predefinidos como 'autoincrement', 'relationship', etc.
418
         *
419
         * Salida:
420
         * - type queda intacto.
421
         * - dbtype es como queda definido en la tabla, por ejemplo, varchar(20)
422
         * - realtype es el tipo resultado, por ejemplo varchar (sin el tamaño)
423
         * - generictype es uno de los índices de TYPE. P.E. autoincrement se cambiará por integer
424
         *
425
         */
426
427
        $column['type'] = $type;
428
        switch ($type) {
429
            case 'autoincrement':
430
            case 'relationship':
431
                $colType = self::DB_INDEX_TYPE;
0 ignored issues
show
Bug introduced by
The constant Alxarafe\Database\Schema::DB_INDEX_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
432
                break;
433
            case 'boolean':
434
                $colType = 'tinyint(1) unsigned';
435
                break;
436
            default:
437
                $colType = $type;
438
        }
439
440
        $typeArray = static::splitType($colType);
441
        /**
442
         * ^ array:4 [▼
443
         *        "type" => "bigint"
444
         *        "length" => null
445
         *        "unsigned" => "yes"
446
         *        "zerofill" => "no"
447
         * ]
448
         */
449
        $type = $typeArray['type'];
450
        $length = $typeArray['length'] ?? $structure['length'];
451
        $unsigned = $typeArray['unsigned'] === 'yes';
452
        $zerofill = $typeArray['zerofill'] === 'yes';
0 ignored issues
show
Unused Code introduced by
The assignment to $zerofill is dead and can be removed.
Loading history...
453
        $genericType = static::getTypeOf($type);
454
455
        $column['dbtype'] = $colType;
456
        $column['realtype'] = $type;
457
        $column['generictype'] = $genericType;
458
459
        $column['null'] = 'YES';
460
        if ($structure['null'] && mb_strtolower($structure['null']) == 'no') {
461
            $column['null'] = 'NO';
462
        }
463
464
        if (empty($structure['default'])) {
465
            $column['default'] = null;
466
        } else {
467
            $column['default'] = (string) $structure['default'];
468
        }
469
470
        /**
471
         * Pueden existir otras definiciones de limitaciones físicas como min y max
472
         * De existir, tienen que ser contempladas en el método test y tener mayor peso que
473
         * la limitación en plantilla.
474
         */
475
        foreach (['min', 'max'] as $field) {
476
            if (isset($structure[$field])) {
477
                $column[$field] = (string) $structure[$field];
478
            }
479
        }
480
481
        if (isset($structure['comment'])) {
482
            $column['comentario'] = (string) $structure['comment'];
483
        }
484
485
        if (isset($structure['default'])) {
486
            $column['default'] = trim($structure['default'], " \"'`");
487
        }
488
489
        switch ($genericType) {
490
            case 'text':
491
                $column['dbtype'] = 'varchar(' . $length . ')';
492
                $column['maxlength'] = $length;
493
                break;
494
            case 'integer':
495
                /**
496
                 * Lo primero es ver la capacidad física máxima según el tipo de dato.
497
                 */
498
                $bytes = 4;
499
                switch ($type) {
500
                    case 'tinyint':
501
                        $bytes = 1;
502
                        break;
503
                    case 'smallint':
504
                        $bytes = 2;
505
                        break;
506
                    case 'mediumint':
507
                        $bytes = 3;
508
                        break;
509
                    case 'int':
510
                        $bytes = 4;
511
                        break;
512
                    case 'bigint':
513
                        $bytes = 8;
514
                        break;
515
                }
516
                $bits = 8 * (int) $bytes;
517
                $physicalMaxLength = 2 ** $bits;
518
519
                /**
520
                 * $minDataLength y $maxDataLength contendrán el mínimo y máximo valor que puede contener el campo.
521
                 */
522
                $minDataLength = $unsigned ? 0 : -$physicalMaxLength / 2;
523
                $maxDataLength = ($unsigned ? $physicalMaxLength : $physicalMaxLength / 2) - 1;
524
525
                /**
526
                 * De momento, se asignan los límites máximos por el tipo de dato.
527
                 * En $min y $max, iremos arrastrando los límites conforme se vayan comprobando.
528
                 * $min nunca podrá ser menor que $minDataLength.
529
                 * $max nunca podrá ser mayor que $maxDataLength.
530
                 */
531
                $min = $minDataLength;
532
                $max = $maxDataLength;
533
534
                /**
535
                 * Se puede hacer una limitación física Se puede haber definido en el xml un min y un max.
536
                 * A todos los efectos, lo definido en el XML como min o max se toma como limitación
537
                 * física del campo.
538
                 */
539
                if (isset($structure['min'])) {
540
                    $minXmlLength = $structure['min'];
541
                    if ($minXmlLength > $minDataLength) {
542
                        $min = $minXmlLength;
543
                    } else {
544
                        Debug::message("({$key}): Se ha especificado un min {$minXmlLength} en el XML, pero por el tipo de datos, el mínimo es {$minDataLength}.");
545
                    }
546
                }
547
                if (isset($structure['max'])) {
548
                    $maxXmlLength = $structure['max'];
549
                    if ($maxXmlLength < $maxDataLength) {
550
                        $max = $maxXmlLength;
551
                    } else {
552
                        Debug::message("({$key}): Se ha especificado un min {$maxXmlLength} en el XML, pero por el tipo de datos, el máximo es {$maxDataLength}.");
553
                    }
554
                }
555
556
                $column['min'] = $min;
557
                $column['max'] = $max;
558
                break;
559
            default:
560
                // ???
561
        }
562
563
        return $column;
564
    }
565
566
    public function compare_columns($table_name, $xml_cols, $db_cols)
567
    {
568
        $sql = '';
569
570
        foreach ($xml_cols as $xml_col) {
571
            if (mb_strtolower($xml_col['tipo']) == 'integer') {
572
                /**
573
                 * Desde la pestaña avanzado el panel de control se puede cambiar
574
                 * el tipo de entero a usar en las columnas.
575
                 */
576
                $xml_col['tipo'] = FS_DB_INTEGER;
0 ignored issues
show
Bug introduced by
The constant Alxarafe\Database\FS_DB_INTEGER was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
577
            }
578
579
            /**
580
             * Si el campo no está en la tabla, procedemos a su creación
581
             */
582
            $db_col = $this->search_in_array($db_cols, 'name', $xml_col['nombre']);
0 ignored issues
show
Bug introduced by
The method search_in_array() 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

582
            /** @scrutinizer ignore-call */ 
583
            $db_col = $this->search_in_array($db_cols, 'name', $xml_col['nombre']);

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
            if (empty($db_col)) {
584
                $sql .= 'ALTER TABLE `' . $table_name . '` ADD `' . $xml_col['nombre'] . '` ';
585
                if ($xml_col['tipo'] == 'serial') {
586
                    $sql .= '`' . $xml_col['nombre'] . '` ' . constant('FS_DB_INTEGER') . ' NOT NULL AUTO_INCREMENT;';
587
                    continue;
588
                }
589
                if ($xml_col['tipo'] == 'autoincrement') {
590
                    $sql .= '`' . $xml_col['nombre'] . '` ' . constant('DB_INDEX_TYPE') . ' NOT NULL AUTO_INCREMENT;';
591
                    continue;
592
                }
593
                if ($xml_col['tipo'] == 'relationship') {
594
                    $xml_col['tipo'] = constant('DB_INDEX_TYPE');
595
                }
596
597
                $sql .= $xml_col['tipo'];
598
                $sql .= ($xml_col['nulo'] == 'NO') ? " NOT NULL" : " NULL";
599
600
                if ($xml_col['defecto'] !== null) {
601
                    $sql .= " DEFAULT " . $xml_col['defecto'];
602
                } elseif ($xml_col['nulo'] == 'YES') {
603
                    $sql .= " DEFAULT NULL";
604
                }
605
606
                $sql .= ';';
607
608
                continue;
609
            }
610
611
            /**
612
             * Si el campo es un autoincremental o relacionado a uno, asignamos el tipo correcto para la constraint.
613
             * Si además es el índice, nos aseguramos de que no pueda ser nulo.
614
             */
615
            if (in_array($xml_col['tipo'], ['autoincrement', 'relationship'])) {
616
                if ($xml_col['tipo'] === 'autoincrement') {
617
                    $xml_col['nulo'] = 'NO';
618
                }
619
                $xml_col['tipo'] = constant('DB_INDEX_TYPE');
620
            }
621
622
            /// columna ya presente en db_cols. La modificamos
623
            if (!$this->compare_data_types($db_col['type'], $xml_col['tipo'])) {
0 ignored issues
show
Bug introduced by
The method compare_data_types() 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

623
            if (!$this->/** @scrutinizer ignore-call */ compare_data_types($db_col['type'], $xml_col['tipo'])) {

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...
624
                // Buscar todas las constraints relacionadas con este campo y eliminarlas
625
                foreach ($this->get_referenced_field_constraint($table_name, $xml_col['nombre']) as $pos => $constraint) {
0 ignored issues
show
Bug introduced by
The method get_referenced_field_constraint() 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

625
                foreach ($this->/** @scrutinizer ignore-call */ get_referenced_field_constraint($table_name, $xml_col['nombre']) as $pos => $constraint) {

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...
626
                    $sql .= "ALTER TABLE `" . $constraint['TABLE_NAME'] . "` DROP FOREIGN KEY " . $constraint['CONSTRAINT_NAME'] . ";";
627
                }
628
                $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'] . ';';
629
            }
630
631
            if ($db_col['is_nullable'] == $xml_col['nulo']) {
632
                /// do nothing
633
            } elseif ($xml_col['nulo'] == 'YES') {
634
                $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'] . ' NULL;';
635
            } else {
636
                $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'] . ' NOT NULL;';
637
            }
638
639
            if ($this->compare_defaults($db_col['default'], $xml_col['defecto'])) {
0 ignored issues
show
Bug introduced by
The method compare_defaults() 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

639
            if ($this->/** @scrutinizer ignore-call */ compare_defaults($db_col['default'], $xml_col['defecto'])) {

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...
640
                /// do nothing
641
            } elseif (is_null($xml_col['defecto'])) {
642
                if ($this->exists_index($table_name, $db_col['name']) && !$this->unique_equals($table_name, $db_col, $xml_col)) {
0 ignored issues
show
Bug introduced by
The method exists_index() 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

642
                if ($this->/** @scrutinizer ignore-call */ exists_index($table_name, $db_col['name']) && !$this->unique_equals($table_name, $db_col, $xml_col)) {

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...
Bug introduced by
The method unique_equals() 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

642
                if ($this->exists_index($table_name, $db_col['name']) && !$this->/** @scrutinizer ignore-call */ unique_equals($table_name, $db_col, $xml_col)) {

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...
643
                    $sql .= 'ALTER TABLE `' . $table_name . '` ALTER `' . $xml_col['nombre'] . '` DROP DEFAULT;';
644
                }
645
            } elseif (mb_strtolower(substr($xml_col['defecto'], 0, 9)) == "nextval('") { /// nextval es para postgresql
646
                if ($db_col['extra'] != 'auto_increment') {
647
                    $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'];
648
                    $sql .= ($xml_col['nulo'] == 'YES') ? ' NULL AUTO_INCREMENT;' : ' NOT NULL AUTO_INCREMENT;';
649
                }
650
            } else {
651
                if ($db_col['default'] != $xml_col['defecto'] && ($db_col['default'] != null && $xml_col['defecto'] == 'NULL')) {
652
                    $sql .= 'ALTER TABLE `' . $table_name . '` ALTER `' . $xml_col['nombre'] . '` SET DEFAULT ' . $xml_col['defecto'] . ";";
653
                }
654
            }
655
        }
656
657
        return $this->fix_postgresql($sql);
0 ignored issues
show
Bug introduced by
The method fix_postgresql() 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

657
        return $this->/** @scrutinizer ignore-call */ fix_postgresql($sql);

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...
658
    }
659
660
    /**
661
     * Create a table in the database.
662
     * Build the default fields, indexes and values defined in the model.
663
     *
664
     * @param string $tableName
665
     *
666
     * @return bool
667
     * @throws DebugBarException
668
     */
669
670
    private static function createTable(string $tableName): bool
671
    {
672
        $tabla = self::$bbddStructure[$tableName];
673
        $sql = self::createFields($tableName, $tabla['fields']);
674
675
        foreach ($tabla['keys'] as $name => $index) {
676
            $sql .= self::createIndex($tableName, $name, $index);
677
        }
678
        // TODO: values no existe, hay que cargar los datos de seeds.
679
        if (isset($tabla['values'])) {
680
            $sql .= self::setValues($tableName, $tabla['values']);
681
        }
682
683
        return Engine::exec($sql);
684
    }
685
686
    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...
687
    {
688
        dump([
689
            'tablename' => $tableName,
690
            'fieldname' => $fieldName,
691
            'new structure' => self::$bbddStructure[$tableName]['fields'][$fieldName],
692
            'structure' => $structure,
693
        ]);
694
        return '';
695
    }
696
697
    private static function updateTable(string $tableName): bool
698
    {
699
        $yamlStructure = self::$bbddStructure[$tableName];
700
        $dbStructure = DB::getColumns($tableName);
701
702
        foreach ($yamlStructure['fields'] as $field => $newStructure) {
703
            $oldDb = $dbStructure[$field];
704
            $newDb = $newStructure['db'];
705
706
            $dif = array_diff($oldDb, $newDb);
707
            $data = [
708
                'field' => $field,
709
                'dbStructure' => $dbStructure[$field],
710
                'fields of ' . $tableName => $newStructure['db'],
711
                'oldDb' => $oldDb,
712
                'newDb' => $newDb,
713
            ];
714
            if (count($dif) > 0) {
715
                $data['diferencias 1'] = $dif;
716
                $data['diferencias 2'] = array_diff($newDb, $oldDb);
717
                $data['sql'] = DB::modify($tableName, $oldDb, $newDb);
718
            }
719
720
            dump($data);
721
        }
722
723
        die('Here');
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 boolean. 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...
724
725
        return Engine::exec($sql);
0 ignored issues
show
Unused Code introduced by
return Alxarafe\Database\Engine::exec($sql) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
726
    }
727
728
    /**
729
     * Build the SQL statement to create the fields in the table.
730
     * It can also create the primary key if the auto_increment attribute is defined.
731
     *
732
     * @param string $tablename
733
     * @param array  $fieldList
734
     *
735
     * @return string
736
     */
737
    protected static function createFields(string $tablename, array $fieldList): string
738
    {
739
        $tablenameWithPrefix = Config::$dbPrefix . $tablename;
740
741
        $sql = "CREATE TABLE $tablenameWithPrefix ( ";
742
        foreach ($fieldList as $index => $col) {
743
            if (!isset($col['dbtype'])) {
744
                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...
745
            }
746
747
            $sql .= '`' . $index . '` ' . $col['dbtype'];
748
            $nulo = isset($col['null']) && $col['null'];
749
750
            $sql .= ($nulo ? '' : ' NOT') . ' NULL';
751
752
            if (isset($col['extra']) && (strtolower($col['extra']) == 'auto_increment')) {
753
                $sql .= ' PRIMARY KEY AUTO_INCREMENT';
754
            }
755
756
            $tmpDefecto = $col['default'] ?? null;
757
            $defecto = '';
758
            if (isset($tmpDefecto)) {
759
                if ($tmpDefecto == 'CURRENT_TIMESTAMP') {
760
                    $defecto = "$tmpDefecto";
761
                } else {
762
                    $defecto = "'$tmpDefecto'";
763
                }
764
            } else {
765
                if ($nulo) {
766
                    $defecto = 'NULL';
767
                }
768
            }
769
770
            if ($defecto != '') {
771
                $sql .= ' DEFAULT ' . $defecto;
772
            }
773
774
            $sql .= ', ';
775
        }
776
        $sql = substr($sql, 0, -2); // Quitamos la coma y el espacio del final
777
        $sql .= ') ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;' . self::CRLF;
778
779
        return $sql;
780
    }
781
782
    /**
783
     * Create the SQL statements for the construction of one index.
784
     * In the case of the primary index, it is not necessary if it is auto_increment.
785
     *
786
     * TODO:
787
     *
788
     * Moreover, it should not be defined if it is auto_increment because it would
789
     * generate an error when it already exists.
790
     *
791
     * @param string $tableName
792
     * @param string $indexname
793
     * @param array  $indexData
794
     *
795
     * @return string
796
     */
797
    protected static function createIndex($tableName, $indexname, $indexData)
798
    {
799
        $sql = "ALTER TABLE $tableName ADD CONSTRAINT $indexname ";
800
801
        $command = '';
802
        // https://www.w3schools.com/sql/sql_primarykey.asp
803
        // ALTER TABLE Persons ADD CONSTRAINT PK_Person PRIMARY KEY (ID,LastName);
804
        if (isset($indexData['PRIMARY'])) {
805
            $command = 'PRIMARY KEY ';
806
            $fields = $indexData['PRIMARY'];
807
        }
808
809
        // https://www.w3schools.com/sql/sql_create_index.asp
810
        // CREATE INDEX idx_pname ON Persons (LastName, FirstName);
811
        if (isset($indexData['INDEX'])) {
812
            $command = 'INDEX ';
813
            $fields = $indexData['INDEX'];
814
        }
815
816
        // https://www.w3schools.com/sql/sql_unique.asp
817
        // ALTER TABLE Persons ADD CONSTRAINT UC_Person UNIQUE (ID,LastName);
818
        if (isset($indexData['UNIQUE'])) {
819
            $command = 'UNIQUE INDEX ';
820
            $fields = $indexData['UNIQUE'];
821
        }
822
823
        if ($command == '') {
824
            // https://www.w3schools.com/sql/sql_foreignkey.asp
825
            // ALTER TABLE Orders ADD CONSTRAINT FK_PersonOrder FOREIGN KEY (PersonID) REFERENCES Persons(PersonID);
826
            if (isset($indexData['FOREIGN'])) {
827
                $command = 'FOREIGN KEY ';
828
                $foreignField = $indexData['FOREIGN'];
829
                if (isset($indexData['REFERENCES'])) {
830
                    $references = $indexData['REFERENCES'];
831
                    if (!is_array($references)) {
832
                        die('Esperaba un array en REFERENCES: ' . $tableName . '/' . $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...
833
                    }
834
                    if (count($references) != 1) {
835
                        die('Esperaba un array de 1 elemento en REFERENCES: ' . $tableName . '/' . $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...
836
                    }
837
                    $refTable = key($references);
838
                    $fields = '(' . implode(',', $references) . ')';
839
                } else {
840
                    die('FOREIGN necesita REFERENCES en ' . $tableName . '/' . $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...
841
                }
842
843
                $sql .= $command . ' ' . $foreignField . ' REFERENCES ' . $refTable . $fields;
844
845
                if (isset($indexData['ON']) && is_array($indexData['ON'])) {
846
                    foreach ($indexData['ON'] as $key => $value) {
847
                        $sql .= ' ON ' . $key . ' ' . $value . ', ';
848
                    }
849
                    $sql = substr($sql, 0, -2); // Quitamos el ', ' de detrás
850
                }
851
            }
852
        } else {
853
            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...
854
                $fields = '(' . implode(',', $fields) . ')';
855
            } else {
856
                $fields = "($fields)";
857
            }
858
859
            if ($command == 'INDEX ') {
860
                $sql = "CREATE INDEX {$indexname} ON {$tableName}" . $fields;
861
            } else {
862
                $sql .= $command . ' ' . $fields;
863
            }
864
        }
865
866
        return $sql . ';' . self::CRLF;
867
    }
868
869
    /**
870
     * Create the SQL statements to fill the table with default data.
871
     *
872
     * @param string $tableName
873
     * @param array  $values
874
     *
875
     * @return string
876
     */
877
    protected static function setValues(string $tableName, array $values): string
878
    {
879
        $sql = "INSERT INTO $tableName ";
880
        $header = true;
881
        foreach ($values as $value) {
882
            $fields = "(";
883
            $datos = "(";
884
            foreach ($value as $fname => $fvalue) {
885
                $fields .= $fname . ", ";
886
                $datos .= "'$fvalue'" . ", ";
887
            }
888
            $fields = substr($fields, 0, -2) . ") ";
889
            $datos = substr($datos, 0, -2) . "), ";
890
891
            if ($header) {
892
                $sql .= $fields . " VALUES ";
893
                $header = false;
894
            }
895
896
            $sql .= $datos;
897
        }
898
899
        return substr($sql, 0, -2) . self::CRLF;
900
    }
901
}
902