Test Failed
Push — main ( ad999b...e26c51 )
by Rafael
06:04
created

Schema::normalize()   F

Complexity

Conditions 24
Paths > 20000

Size

Total Lines 157
Code Lines 84

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 24
eloc 84
nc 41856
nop 1
dl 0
loc 157
rs 0
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Copyright (C) 2022-2023  Rafael San José Tovar   <[email protected]>
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License
16
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
 */
18
19
namespace Alxarafe\Database;
20
21
use Alxarafe\Core\Helpers\Dispatcher;
22
use Alxarafe\Core\Singletons\Config;
23
use Alxarafe\Core\Singletons\Debug;
24
use Alxarafe\Core\Singletons\PhpFileCache;
25
use DebugBar\DebugBarException;
26
use Symfony\Component\Yaml\Yaml;
27
use function Alxarafe\Core\Helpers\count;
0 ignored issues
show
introduced by
The function Alxarafe\Core\Helpers\count was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
28
use const Alxarafe\Core\Helpers\DEFAULT_INTEGER_SIZE;
0 ignored issues
show
Bug introduced by
The constant Alxarafe\Core\Helpers\DEFAULT_INTEGER_SIZE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
29
use const Alxarafe\Core\Helpers\DEFAULT_STRING_LENGTH;
0 ignored issues
show
Bug introduced by
The constant Alxarafe\Core\Helpers\DEFAULT_STRING_LENGTH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
30
31
/**
32
 * Class Schema
33
 *
34
 * La clase abstracta Schema, define un esquema de base de datos teórico al que
35
 * se traduce la base de datos real y viceversa, de manera que el código sea
36
 * en la medida de lo posible, no dependiente de la base de datos real.
37
 *
38
 * Lo dependiente de la base de datos está en DBSchema.
39
 *
40
 * TODO: ¿La información cacheada se procesa en YamlSchema o no merece la pena?
41
 *
42
 * @author  Rafael San José Tovar <[email protected]>
43
 * @version 2023.0101
44
 *
45
 * @package Alxarafe\Database
46
 */
47
class Schema
48
{
49
    public const TYPE_INTEGER = 'integer';
50
    public const TYPE_FLOAT = 'float';
51
    public const TYPE_DECIMAL = 'decimal';
52
    public const TYPE_STRING = 'string';
53
    public const TYPE_TEXT = 'text';
54
    public const TYPE_DATE = 'date';
55
    public const TYPE_TIME = 'time';
56
    public const TYPE_DATETIME = 'datetime';
57
    public const TYPE_BOOLEAN = 'bool';
58
59
    public const YAML_CACHE_TABLES_DIR = 'models';
60
61
    /**
62
     * Carriage Return and Line Feed
63
     */
64
    const CRLF = "\r\n";
65
    const DB_INDEX_TYPE = 'bigint (20) unsigned';
66
67
    public static array $tables = [];
68
69
    /**
70
     * Contains the database structure data.
71
     * Each table is an index of the associative array.
72
     *
73
     * @var array
74
     */
75
    public static array $bbddStructure;
76
77
    public static function checkDatabaseStructure()
78
    {
79
        // Se obtiene la relación de tablas definidas para la base de datos
80
        if (empty(self::$tables)) {
81
            Schema::getTables();
82
        }
83
84
        foreach (self::$tables as $key=>$table) {
85
            dump("Verificando la tabla $key, definida en $table.");
86
            static::checkTable($key);
87
        }
88
        die();
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...
89
    }
90
91
    /**
92
     * Return true if $tableName exists in database
93
     *
94
     * @param string $tableName
95
     *
96
     * @return bool
97
     * @throws DebugBarException
98
     */
99
    public static function tableExists($tableName): bool
100
    {
101
        $tableNameWithPrefix = Config::$dbPrefix . $tableName;
102
        $dbName = Config::$dbName;
103
        $sql = "SELECT COUNT(*) AS Total FROM information_schema.tables WHERE table_schema = '{$dbName}' AND table_name='{$tableNameWithPrefix}'";
104
105
        $data = Engine::select($sql);
106
        $result = reset($data);
107
108
        return $result['Total'] === '1';
109
    }
110
111
    private static function getFieldsAndIndexes($tableName):array
112
    {
113
        $yamlSourceFilename = self::$tables[$tableName];
114
        if (!file_exists($yamlSourceFilename)) {
115
            dump('No existe el archivo ' . $yamlSourceFilename);
116
        }
117
118
        $data = Yaml::parseFile($yamlSourceFilename);
119
120
        $result = [];
121
        foreach ($data['fields']??[] as $key => $datum) {
122
            $datum['key'] = $key;
123
            $result['fields'][$key] = Schema::normalize($datum);
124
            if ($result['fields'][$key]['type']==='autoincrement') {
125
                // TODO: Ver cómo tendría que ser la primary key
126
                $result['indexes']['primary'] = $key;
127
            }
128
        }
129
        foreach ($data['indexes']??[] as $key=>$datum) {
130
            $datum['key'] = $key;
131
            $result['indexes'][$key] = $datum;
132
        }
133
134
        /*
135
        Igual conviene crear una clase:
136
        - DBSchema (con los datos de la base de datos real)
137
        - DefinedSchema (con los datos definidos)
138
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
139
140
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
141
    oportunas siempre que no suponga una pérdida de datos.
142
        */
143
144
        return $result;
145
    }
146
147
    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...
148
    {
149
        $yamlSourceFilename = self::$tables[$tableName];
150
        if (!file_exists($yamlSourceFilename)) {
151
            dump('No existe el archivo ' . $yamlSourceFilename);
152
        }
153
154
        $data = Yaml::parseFile($yamlSourceFilename);
155
156
        $result = [];
157
        foreach ($data as $key => $datum) {
158
            $datum['key'] = $key;
159
            $result[$key] = Schema::normalize($datum);
160
        }
161
162
        /*
163
        Igual conviene crear una clase:
164
        - DBSchema (con los datos de la base de datos real)
165
        - DefinedSchema (con los datos definidos)
166
        y que Schema cree o adapte según los datos de ambas. Que cada una lleve lo suyo
167
168
        Que el resultado se guarde en el yaml y que se encargue de realizar las conversines
169
    oportunas siempre que no suponga una pérdida de datos.
170
        */
171
172
        return $result;
173
    }
174
175
    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

175
    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...
176
    {
177
        $result = [];
178
        return $result;
179
    }
180
181
    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

181
    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...
182
    {
183
        $result = [];
184
        return $result;
185
    }
186
187
    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

187
    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...
188
    {
189
        $result = [];
190
        return $result;
191
    }
192
193
    private static function checkTable(string $tableName, bool $create=true): array
194
    {
195
        $structure = self::getFieldsAndIndexes($tableName);
196
        //$structure['fields'] = self::getFields($tableName); // Del yaml
197
        //$structure['indexes'] = self::getIndexes($tableName); // Del yaml
198
        $structure['related'] = self::getRelated($tableName); // ¿De la base de datos, de la integridad referencial?. Igual hay que hacerlo en un 2º paso
199
        if ($create) {
200
            $structure['seed'] = self::getSeed($tableName);
201
        }
202
        return $structure;
203
    }
204
205
    /**
206
     * Comprueba la estructura de la tabla y la crea si no existe y así se solicita.
207
     * Si los datos de la estructura no están en la caché, los regenera y almacena.
208
     * Al regenerar los datos para la caché, también realiza una verificación de
209
     * la estructura por si hay cambios que aplicar en la misma.
210
     *
211
     * TODO: Es mejor que haya un checkStructure que genere TODAS las tablas e índices
212
     * Ese checkstructure se debe de generar tras limpiar caché.
213
     * La caché deberá de limpiarse cada vez que se active o desactive un módulo.
214
     * El último paso de la generación de tablas, sería comprobar las dependencias
215
     * de tablas para saber cuántas tablas usan una constraint de cada tabla para poder
216
     * realizar cambios en la base de datos y tener una visión más nítida de la misma en
217
     * cualquier momento, si bien, esa estructura no será clara hasta que no se hayan leído
218
     * todas, y si hay un cambio entre medias, pues igual la única solución viable es
219
     * determinarlo por la propia base de datos.
220
     *
221
     * @author  Rafael San José Tovar <[email protected]>
222
     * @version 2023.0105
223
     *
224
     * @param string $tableName
225
     * @param bool   $create
226
     *
227
     * @return bool
228
     */
229
    public static function checkStructure(string $tableName, bool $create = true): bool
230
    {
231
        if (empty(self::$tables)) {
232
            Schema::getTables();
233
        }
234
235
        // Si el dato ya ha sido cargado, retornamos porque no hay nada que hacer.
236
        if (!empty(self::$bbddStructure[$tableName])) {
237
            return true;
238
        }
239
240
        // Si no está, pero está cacheado, se recupera de la caché y se retorna.
241
        self::$bbddStructure[$tableName] = YamlSchema::loadCacheYamlFile(self::YAML_CACHE_TABLES_DIR, $tableName);
242
        if (!empty(self::$bbddStructure[$tableName])) {
243
            return true;
244
        }
245
246
        // Si no está cacheado, entonces hay que comprobar si hay cambios en la estructura y regenerarla.
247
        self::$bbddStructure[$tableName] = self::checkTable($tableName, $create);
248
        dump(self::$bbddStructure);
249
250
        if (!YamlSchema::saveCacheYamlFile(self::YAML_CACHE_TABLES_DIR, $tableName, self::$bbddStructure[$tableName])) {
251
            Debug::addMessage('No se ha podido guardar la información de caché para la tabla ' . $tableName);
0 ignored issues
show
Bug introduced by
The call to Alxarafe\Core\Singletons\Debug::addMessage() has too few arguments starting with message. ( Ignorable by Annotation )

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

251
            Debug::/** @scrutinizer ignore-call */ 
252
                   addMessage('No se ha podido guardar la información de caché para la tabla ' . $tableName);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
252
            return false;
253
        }
254
        return true;
255
    }
256
257
    /**
258
     * Obtiene el tipo genérico del tipo de dato que se le ha pasado.
259
     *
260
     * @author  Rafael San José Tovar <[email protected]>
261
     * @version 2023.0101
262
     *
263
     * @param string $type
264
     *
265
     * @return string
266
     */
267
    public static function getTypeOf(string $type): string
268
    {
269
        foreach (DB::getDataTypes() as $index => $types) {
270
            if (in_array(strtolower($type), $types)) {
271
                return $index;
272
            }
273
        }
274
        Debug::addMessage('messages', $type . ' not found in DBSchema::getTypeOf()');
275
        return 'text';
276
    }
277
278
    private static function splitType(string $originalType): array
279
    {
280
        $replacesSources = [
281
            'character varying',
282
            // 'timestamp without time zone',
283
            'double precision',
284
        ];
285
        $replacesDestination = [
286
            'varchar',
287
            // 'timestamp',
288
            'double',
289
        ];
290
        $modifiedType = (str_replace($replacesSources, $replacesDestination, $originalType));
291
292
        if ($originalType !== $modifiedType) {
293
            Debug::addMessage('messages', "XML: Uso de '{$originalType}' en lugar de '{$modifiedType}'.");
294
        }
295
        $explode = explode(' ', strtolower($modifiedType));
296
297
        $pos = strpos($explode[0], '(');
298
        if ($pos > 0) {
299
            $begin = $pos + 1;
300
            $end = strpos($explode[0], ')');
301
            $type = substr($explode[0], 0, $pos);
302
            $length = substr($explode[0], $begin, $end - $begin);
303
        } else {
304
            $type = $explode[0];
305
            $length = null;
306
        }
307
308
        $pos = array_search('unsigned', $explode, true);
309
        $unsigned = $pos ? 'yes' : 'no';
310
311
        $pos = array_search('zerofill', $explode, true);
312
        $zerofill = $pos ? 'yes' : 'no';
313
314
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
315
    }
316
317
    /**
318
     * Toma los datos del fichero de definición de una tabla y genera el definitivo.
319
     *
320
     * @author  Rafael San José Tovar <[email protected]>
321
     * @version 2022.1224
322
     *
323
     * @param array $structure
324
     *
325
     * @return array
326
     */
327
    protected static function normalize(array $structure): array
328
    {
329
        $column = [];
330
        $key = (string) $structure['key'];
331
        $type = (string) $structure['type'];
332
        $column['key'] = $key;
333
334
        /**
335
         * Entrada:
336
         * - type es el tipo lógico del campo y tiene que estar definido como índice en
337
         *   TYPES, o ser uno de los predefinidos como 'autoincrement', 'relationship', etc.
338
         *
339
         * Salida:
340
         * - type queda intacto.
341
         * - dbtype es como queda definido en la tabla, por ejemplo, varchar(20)
342
         * - realtype es el tipo resultado, por ejemplo varchar (sin el tamaño)
343
         * - generictype es uno de los índices de TYPE. P.E. autoincrement se cambiará por integer
344
         *
345
         */
346
347
        $column['type'] = $type;
348
        switch ($type) {
349
            case 'autoincrement':
350
            case 'relationship':
351
                $colType = self::DB_INDEX_TYPE;
352
                break;
353
            case 'boolean':
354
                $colType = 'tinyint(1) unsigned';
355
                break;
356
            default:
357
                $colType = $type;
358
        }
359
360
        $typeArray = static::splitType($colType);
361
        /**
362
         * ^ array:4 [▼
363
         *        "type" => "bigint"
364
         *        "length" => null
365
         *        "unsigned" => "yes"
366
         *        "zerofill" => "no"
367
         * ]
368
         */
369
        $type = $typeArray['type'];
370
        $length = $typeArray['length'] ?? $structure['length'];
371
        $unsigned = $typeArray['unsigned'] === 'yes';
372
        $zerofill = $typeArray['zerofill'] === 'yes';
0 ignored issues
show
Unused Code introduced by
The assignment to $zerofill is dead and can be removed.
Loading history...
373
        $genericType = static::getTypeOf($type);
374
375
        $column['dbtype'] = $colType;
376
        $column['realtype'] = $type;
377
        $column['generictype'] = $genericType;
378
379
        $column['null'] = 'YES';
380
        if ($structure['null'] && mb_strtolower($structure['null']) == 'no') {
381
            $column['null'] = 'NO';
382
        }
383
384
        if (empty($structure['default'])) {
385
            $column['default'] = null;
386
        } else {
387
            $column['default'] = (string) $structure['default'];
388
        }
389
390
        /**
391
         * Pueden existir otras definiciones de limitaciones físicas como min y max
392
         * De existir, tienen que ser contempladas en el método test y tener mayor peso que
393
         * la limitación en plantilla.
394
         */
395
        foreach (['min', 'max'] as $field) {
396
            if (isset($structure[$field])) {
397
                $column[$field] = (string) $structure[$field];
398
            }
399
        }
400
401
        if (isset($structure['comment'])) {
402
            $column['comentario'] = (string) $structure['comment'];
403
        }
404
405
        if (isset($structure['default'])) {
406
            $column['default'] = trim($structure['default'], " \"'`");
407
        }
408
409
        switch ($genericType) {
410
            case 'text':
411
                $column['dbtype'] = 'varchar(' . $length . ')';
412
                $column['maxlength'] = $length;
413
                break;
414
            case 'integer':
415
                /**
416
                 * Lo primero es ver la capacidad física máxima según el tipo de dato.
417
                 */
418
                $bytes = 4;
419
                switch ($type) {
420
                    case 'tinyint':
421
                        $bytes = 1;
422
                        break;
423
                    case 'smallint':
424
                        $bytes = 2;
425
                        break;
426
                    case 'mediumint':
427
                        $bytes = 3;
428
                        break;
429
                    case 'int':
430
                        $bytes = 4;
431
                        break;
432
                    case 'bigint':
433
                        $bytes = 8;
434
                        break;
435
                }
436
                $bits = 8 * (int) $bytes;
437
                $physicalMaxLength = 2 ** $bits;
438
439
                /**
440
                 * $minDataLength y $maxDataLength contendrán el mínimo y máximo valor que puede contener el campo.
441
                 */
442
                $minDataLength = $unsigned ? 0 : -$physicalMaxLength / 2;
443
                $maxDataLength = ($unsigned ? $physicalMaxLength : $physicalMaxLength / 2) - 1;
444
445
                /**
446
                 * De momento, se asignan los límites máximos por el tipo de dato.
447
                 * En $min y $max, iremos arrastrando los límites conforme se vayan comprobando.
448
                 * $min nunca podrá ser menor que $minDataLength.
449
                 * $max nunca podrá ser mayor que $maxDataLength.
450
                 */
451
                $min = $minDataLength;
452
                $max = $maxDataLength;
453
454
                /**
455
                 * Se puede hacer una limitación física Se puede haber definido en el xml un min y un max.
456
                 * A todos los efectos, lo definido en el XML como min o max se toma como limitación
457
                 * física del campo.
458
                 */
459
                if (isset($structure['min'])) {
460
                    $minXmlLength = $structure['min'];
461
                    if ($minXmlLength > $minDataLength) {
462
                        $min = $minXmlLength;
463
                    } else {
464
                        Debug::addMessage('messages', "({$key}): Se ha especificado un min {$minXmlLength} en el XML, pero por el tipo de datos, el mínimo es {$minDataLength}.");
465
                    }
466
                }
467
                if (isset($structure['max'])) {
468
                    $maxXmlLength = $structure['max'];
469
                    if ($maxXmlLength < $maxDataLength) {
470
                        $max = $maxXmlLength;
471
                    } else {
472
                        Debug::addMessage('messages', "({$key}): Se ha especificado un min {$maxXmlLength} en el XML, pero por el tipo de datos, el máximo es {$maxDataLength}.");
473
                    }
474
                }
475
476
                $column['min'] = $min;
477
                $column['max'] = $max;
478
                break;
479
            default:
480
                // ???
481
        }
482
483
        return $column;
484
    }
485
486
    private static function getTables()
487
    {
488
        self::$tables = Dispatcher::getFiles('Tables', 'yaml');
489
    }
490
491
    public function compare_columns($table_name, $xml_cols, $db_cols)
492
    {
493
        $sql = '';
494
495
        foreach ($xml_cols as $xml_col) {
496
            if (mb_strtolower($xml_col['tipo']) == 'integer') {
497
                /**
498
                 * Desde la pestaña avanzado el panel de control se puede cambiar
499
                 * el tipo de entero a usar en las columnas.
500
                 */
501
                $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...
502
            }
503
504
            /**
505
             * Si el campo no está en la tabla, procedemos a su creación
506
             */
507
            $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

507
            /** @scrutinizer ignore-call */ 
508
            $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...
508
            if (empty($db_col)) {
509
                $sql .= 'ALTER TABLE `' . $table_name . '` ADD `' . $xml_col['nombre'] . '` ';
510
                if ($xml_col['tipo'] == 'serial') {
511
                    $sql .= '`' . $xml_col['nombre'] . '` ' . constant('FS_DB_INTEGER') . ' NOT NULL AUTO_INCREMENT;';
512
                    continue;
513
                }
514
                if ($xml_col['tipo'] == 'autoincrement') {
515
                    $sql .= '`' . $xml_col['nombre'] . '` ' . constant('DB_INDEX_TYPE') . ' NOT NULL AUTO_INCREMENT;';
516
                    continue;
517
                }
518
                if ($xml_col['tipo'] == 'relationship') {
519
                    $xml_col['tipo'] = constant('DB_INDEX_TYPE');
520
                }
521
522
                $sql .= $xml_col['tipo'];
523
                $sql .= ($xml_col['nulo'] == 'NO') ? " NOT NULL" : " NULL";
524
525
                if ($xml_col['defecto'] !== null) {
526
                    $sql .= " DEFAULT " . $xml_col['defecto'];
527
                } elseif ($xml_col['nulo'] == 'YES') {
528
                    $sql .= " DEFAULT NULL";
529
                }
530
531
                $sql .= ';';
532
533
                continue;
534
            }
535
536
            /**
537
             * Si el campo es un autoincremental o relacionado a uno, asignamos el tipo correcto para la constraint.
538
             * Si además es el índice, nos aseguramos de que no pueda ser nulo.
539
             */
540
            if (in_array($xml_col['tipo'], ['autoincrement', 'relationship'])) {
541
                if ($xml_col['tipo'] === 'autoincrement') {
542
                    $xml_col['nulo'] = 'NO';
543
                }
544
                $xml_col['tipo'] = constant('DB_INDEX_TYPE');
545
            }
546
547
            /// columna ya presente en db_cols. La modificamos
548
            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

548
            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...
549
                // Buscar todas las constraints relacionadas con este campo y eliminarlas
550
                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

550
                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...
551
                    $sql .= "ALTER TABLE `" . $constraint['TABLE_NAME'] . "` DROP FOREIGN KEY " . $constraint['CONSTRAINT_NAME'] . ";";
552
                }
553
                $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'] . ';';
554
            }
555
556
            if ($db_col['is_nullable'] == $xml_col['nulo']) {
557
                /// do nothing
558
            } elseif ($xml_col['nulo'] == 'YES') {
559
                $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'] . ' NULL;';
560
            } else {
561
                $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'] . ' NOT NULL;';
562
            }
563
564
            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

564
            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...
565
                /// do nothing
566
            } elseif (is_null($xml_col['defecto'])) {
567
                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

567
                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

567
                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...
568
                    $sql .= 'ALTER TABLE `' . $table_name . '` ALTER `' . $xml_col['nombre'] . '` DROP DEFAULT;';
569
                }
570
            } elseif (mb_strtolower(substr($xml_col['defecto'], 0, 9)) == "nextval('") { /// nextval es para postgresql
571
                if ($db_col['extra'] != 'auto_increment') {
572
                    $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'];
573
                    $sql .= ($xml_col['nulo'] == 'YES') ? ' NULL AUTO_INCREMENT;' : ' NOT NULL AUTO_INCREMENT;';
574
                }
575
            } else {
576
                if ($db_col['default'] != $xml_col['defecto'] && ($db_col['default'] != null && $xml_col['defecto'] == 'NULL')) {
577
                    $sql .= 'ALTER TABLE `' . $table_name . '` ALTER `' . $xml_col['nombre'] . '` SET DEFAULT ' . $xml_col['defecto'] . ";";
578
                }
579
            }
580
        }
581
582
        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

582
        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...
583
    }
584
585
    /**
586
     * Create a table in the database.
587
     * Build the default fields, indexes and values defined in the model.
588
     *
589
     * @param string $tableName
590
     *
591
     * @return bool
592
     * @throws DebugBarException
593
     */
594
595
    public static function createTable(string $tableName): bool
596
    {
597
        $tabla = self::$bbddStructure[$tableName];
598
        $sql = self::createFields($tableName, $tabla['fields']);
599
600
        foreach ($tabla['keys'] as $name => $index) {
601
            $sql .= self::createIndex($tableName, $name, $index);
602
        }
603
        if (isset($tabla['values'])) {
604
            $sql .= self::setValues($tableName, $tabla['values']);
605
        }
606
607
        dump($sql);
608
        die('?');
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...
609
610
        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...
611
    }
612
613
    /**
614
     * Build the SQL statement to create the fields in the table.
615
     * It can also create the primary key if the auto_increment attribute is defined.
616
     *
617
     * @param string $tablename
618
     * @param array  $fieldList
619
     *
620
     * @return string
621
     */
622
    protected static function createFields(string $tablename, array $fieldList): string
623
    {
624
        $tablenameWithPrefix = Config::$dbPrefix . $tablename;
625
626
        $sql = "CREATE TABLE $tablenameWithPrefix ( ";
627
        foreach ($fieldList as $index => $col) {
628
            if (!isset($col['dbtype'])) {
629
                die('Tipo no especificado en createTable ' . $index);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

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

Loading history...
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

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

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

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
630
            }
631
632
            $sql .= '`' . $index . '` ' . $col['dbtype'];
633
            $nulo = isset($col['null']) && $col['null'];
634
635
            $sql .= ($nulo ? '' : ' NOT') . ' NULL';
636
637
            if (isset($col['extra']) && (strtolower($col['extra']) == 'auto_increment')) {
638
                $sql .= ' PRIMARY KEY AUTO_INCREMENT';
639
            }
640
641
            $tmpDefecto = $col['default'] ?? null;
642
            $defecto = '';
643
            if (isset($tmpDefecto)) {
644
                if ($tmpDefecto == 'CURRENT_TIMESTAMP') {
645
                    $defecto = "$tmpDefecto";
646
                } else {
647
                    $defecto = "'$tmpDefecto'";
648
                }
649
            } else {
650
                if ($nulo) {
651
                    $defecto = 'NULL';
652
                }
653
            }
654
655
            if ($defecto != '') {
656
                $sql .= ' DEFAULT ' . $defecto;
657
            }
658
659
            $sql .= ', ';
660
        }
661
        $sql = substr($sql, 0, -2); // Quitamos la coma y el espacio del final
662
        $sql .= ') ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;' . self::CRLF;
663
664
        return $sql;
665
    }
666
667
    /**
668
     * Create the SQL statements for the construction of one index.
669
     * In the case of the primary index, it is not necessary if it is auto_increment.
670
     *
671
     * TODO:
672
     *
673
     * Moreover, it should not be defined if it is auto_increment because it would
674
     * generate an error when it already exists.
675
     *
676
     * @param string $tableName
677
     * @param string $indexname
678
     * @param array  $indexData
679
     *
680
     * @return string
681
     */
682
    protected static function createIndex($tableName, $indexname, $indexData)
683
    {
684
        $sql = "ALTER TABLE $tableName ADD CONSTRAINT $indexname ";
685
686
        $command = '';
687
        // https://www.w3schools.com/sql/sql_primarykey.asp
688
        // ALTER TABLE Persons ADD CONSTRAINT PK_Person PRIMARY KEY (ID,LastName);
689
        if (isset($indexData['PRIMARY'])) {
690
            $command = 'PRIMARY KEY ';
691
            $fields = $indexData['PRIMARY'];
692
        }
693
694
        // https://www.w3schools.com/sql/sql_create_index.asp
695
        // CREATE INDEX idx_pname ON Persons (LastName, FirstName);
696
        if (isset($indexData['INDEX'])) {
697
            $command = 'INDEX ';
698
            $fields = $indexData['INDEX'];
699
        }
700
701
        // https://www.w3schools.com/sql/sql_unique.asp
702
        // ALTER TABLE Persons ADD CONSTRAINT UC_Person UNIQUE (ID,LastName);
703
        if (isset($indexData['UNIQUE'])) {
704
            $command = 'UNIQUE INDEX ';
705
            $fields = $indexData['UNIQUE'];
706
        }
707
708
        if ($command == '') {
709
            // https://www.w3schools.com/sql/sql_foreignkey.asp
710
            // ALTER TABLE Orders ADD CONSTRAINT FK_PersonOrder FOREIGN KEY (PersonID) REFERENCES Persons(PersonID);
711
            if (isset($indexData['FOREIGN'])) {
712
                $command = 'FOREIGN KEY ';
713
                $foreignField = $indexData['FOREIGN'];
714
                if (isset($indexData['REFERENCES'])) {
715
                    $references = $indexData['REFERENCES'];
716
                    if (!is_array($references)) {
717
                        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...
718
                    }
719
                    if (count($references) != 1) {
720
                        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...
721
                    }
722
                    $refTable = key($references);
723
                    $fields = '(' . implode(',', $references) . ')';
724
                } else {
725
                    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...
726
                }
727
728
                $sql .= $command . ' ' . $foreignField . ' REFERENCES ' . $refTable . $fields;
729
730
                if (isset($indexData['ON']) && is_array($indexData['ON'])) {
731
                    foreach ($indexData['ON'] as $key => $value) {
732
                        $sql .= ' ON ' . $key . ' ' . $value . ', ';
733
                    }
734
                    $sql = substr($sql, 0, -2); // Quitamos el ', ' de detrás
735
                }
736
            }
737
        } else {
738
            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...
739
                $fields = '(' . implode(',', $fields) . ')';
740
            } else {
741
                $fields = "($fields)";
742
            }
743
744
            if ($command == 'INDEX ') {
745
                $sql = "CREATE INDEX {$indexname} ON {$tableName}" . $fields;
746
            } else {
747
                $sql .= $command . ' ' . $fields;
748
            }
749
        }
750
751
        return $sql . ';' . self::CRLF;
752
    }
753
754
    /**
755
     * Create the SQL statements to fill the table with default data.
756
     *
757
     * @param string $tableName
758
     * @param array  $values
759
     *
760
     * @return string
761
     */
762
    protected static function setValues(string $tableName, array $values): string
763
    {
764
        $sql = "INSERT INTO $tableName ";
765
        $header = true;
766
        foreach ($values as $value) {
767
            $fields = "(";
768
            $datos = "(";
769
            foreach ($value as $fname => $fvalue) {
770
                $fields .= $fname . ", ";
771
                $datos .= "'$fvalue'" . ", ";
772
            }
773
            $fields = substr($fields, 0, -2) . ") ";
774
            $datos = substr($datos, 0, -2) . "), ";
775
776
            if ($header) {
777
                $sql .= $fields . " VALUES ";
778
                $header = false;
779
            }
780
781
            $sql .= $datos;
782
        }
783
784
        return substr($sql, 0, -2) . self::CRLF;
785
    }
786
}
787