Passed
Push — main ( e3de8f...bc0c36 )
by Rafael
05:31
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

3 Methods

Rating   Name   Duplication   Size   Complexity  
A Schema::createTable() 0 14 3
A Schema::updateTable() 0 29 3
A Schema::updateField() 0 9 1

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\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);
0 ignored issues
show
Bug introduced by
The method normalize() does not exist on Alxarafe\Database\Schema. ( Ignorable by Annotation )

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

147
            /** @scrutinizer ignore-call */ 
148
            $result['fields'][$key]['info'] = Schema::normalize($datum);

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

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

Loading history...
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
        return true;
295
        die('Por aquí vamos ahora...');
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...
Unused Code introduced by
ExitNode 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...
296
297
        if (!YamlSchema::saveCacheYamlFile(YamlSchema::YAML_CACHE_TABLES_DIR, $tableName, self::$bbddStructure[$tableName])) {
298
            Debug::message('No se ha podido guardar la información de caché para la tabla ' . $tableName);
299
            return false;
300
        }
301
        return true;
302
    }
303
304
    /**
305
     * Obtiene el tipo genérico del tipo de dato que se le ha pasado.
306
     *
307
     * @author  Rafael San José Tovar <[email protected]>
308
     * @version 2023.0101
309
     *
310
     * @param string $type
311
     *
312
     * @return string
313
     */
314
    public static function getTypeOf(string $type): string
315
    {
316
        foreach (DB::getDataTypes() as $index => $types) {
317
            if (in_array(strtolower($type), $types)) {
318
                return $index;
319
            }
320
        }
321
        Debug::message($type . ' not found in DBSchema::getTypeOf()');
322
        return 'text';
323
    }
324
325
    private static function splitType(string $originalType): array
0 ignored issues
show
Unused Code introduced by
The method splitType() is not used, and could be removed.

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

Loading history...
326
    {
327
        $replacesSources = [
328
            'character varying',
329
            // 'timestamp without time zone',
330
            'double precision',
331
        ];
332
        $replacesDestination = [
333
            'varchar',
334
            // 'timestamp',
335
            'double',
336
        ];
337
        $modifiedType = (str_replace($replacesSources, $replacesDestination, $originalType));
338
339
        if ($originalType !== $modifiedType) {
340
            Debug::message("XML: Uso de '{$originalType}' en lugar de '{$modifiedType}'.");
341
        }
342
        $explode = explode(' ', strtolower($modifiedType));
343
344
        $pos = strpos($explode[0], '(');
345
        if ($pos > 0) {
346
            $begin = $pos + 1;
347
            $end = strpos($explode[0], ')');
348
            $type = substr($explode[0], 0, $pos);
349
            $length = substr($explode[0], $begin, $end - $begin);
350
        } else {
351
            $type = $explode[0];
352
            $length = null;
353
        }
354
355
        $pos = array_search('unsigned', $explode, true);
356
        $unsigned = $pos ? 'yes' : 'no';
357
358
        $pos = array_search('zerofill', $explode, true);
359
        $zerofill = $pos ? 'yes' : 'no';
360
361
        return ['type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'zerofill' => $zerofill];
362
    }
363
364
    public function compare_columns($table_name, $xml_cols, $db_cols)
365
    {
366
        $sql = '';
367
368
        foreach ($xml_cols as $xml_col) {
369
            if (mb_strtolower($xml_col['tipo']) == 'integer') {
370
                /**
371
                 * Desde la pestaña avanzado el panel de control se puede cambiar
372
                 * el tipo de entero a usar en las columnas.
373
                 */
374
                $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...
375
            }
376
377
            /**
378
             * Si el campo no está en la tabla, procedemos a su creación
379
             */
380
            $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

380
            /** @scrutinizer ignore-call */ 
381
            $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...
381
            if (empty($db_col)) {
382
                $sql .= 'ALTER TABLE `' . $table_name . '` ADD `' . $xml_col['nombre'] . '` ';
383
                if ($xml_col['tipo'] == 'serial') {
384
                    $sql .= '`' . $xml_col['nombre'] . '` ' . constant('FS_DB_INTEGER') . ' NOT NULL AUTO_INCREMENT;';
385
                    continue;
386
                }
387
                if ($xml_col['tipo'] == 'autoincrement') {
388
                    $sql .= '`' . $xml_col['nombre'] . '` ' . constant('DB_INDEX_TYPE') . ' NOT NULL AUTO_INCREMENT;';
389
                    continue;
390
                }
391
                if ($xml_col['tipo'] == 'relationship') {
392
                    $xml_col['tipo'] = constant('DB_INDEX_TYPE');
393
                }
394
395
                $sql .= $xml_col['tipo'];
396
                $sql .= ($xml_col['nulo'] == 'NO') ? " NOT NULL" : " NULL";
397
398
                if ($xml_col['defecto'] !== null) {
399
                    $sql .= " DEFAULT " . $xml_col['defecto'];
400
                } elseif ($xml_col['nulo'] == 'YES') {
401
                    $sql .= " DEFAULT NULL";
402
                }
403
404
                $sql .= ';';
405
406
                continue;
407
            }
408
409
            /**
410
             * Si el campo es un autoincremental o relacionado a uno, asignamos el tipo correcto para la constraint.
411
             * Si además es el índice, nos aseguramos de que no pueda ser nulo.
412
             */
413
            if (in_array($xml_col['tipo'], ['autoincrement', 'relationship'])) {
414
                if ($xml_col['tipo'] === 'autoincrement') {
415
                    $xml_col['nulo'] = 'NO';
416
                }
417
                $xml_col['tipo'] = constant('DB_INDEX_TYPE');
418
            }
419
420
            /// columna ya presente en db_cols. La modificamos
421
            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

421
            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...
422
                // Buscar todas las constraints relacionadas con este campo y eliminarlas
423
                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

423
                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...
424
                    $sql .= "ALTER TABLE `" . $constraint['TABLE_NAME'] . "` DROP FOREIGN KEY " . $constraint['CONSTRAINT_NAME'] . ";";
425
                }
426
                $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'] . ';';
427
            }
428
429
            if ($db_col['is_nullable'] == $xml_col['nulo']) {
430
                /// do nothing
431
            } elseif ($xml_col['nulo'] == 'YES') {
432
                $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'] . ' NULL;';
433
            } else {
434
                $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'] . ' NOT NULL;';
435
            }
436
437
            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

437
            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...
438
                /// do nothing
439
            } elseif (is_null($xml_col['defecto'])) {
440
                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

440
                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

440
                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...
441
                    $sql .= 'ALTER TABLE `' . $table_name . '` ALTER `' . $xml_col['nombre'] . '` DROP DEFAULT;';
442
                }
443
            } elseif (mb_strtolower(substr($xml_col['defecto'], 0, 9)) == "nextval('") { /// nextval es para postgresql
444
                if ($db_col['extra'] != 'auto_increment') {
445
                    $sql .= 'ALTER TABLE `' . $table_name . '` MODIFY `' . $xml_col['nombre'] . '` ' . $xml_col['tipo'];
446
                    $sql .= ($xml_col['nulo'] == 'YES') ? ' NULL AUTO_INCREMENT;' : ' NOT NULL AUTO_INCREMENT;';
447
                }
448
            } else {
449
                if ($db_col['default'] != $xml_col['defecto'] && ($db_col['default'] != null && $xml_col['defecto'] == 'NULL')) {
450
                    $sql .= 'ALTER TABLE `' . $table_name . '` ALTER `' . $xml_col['nombre'] . '` SET DEFAULT ' . $xml_col['defecto'] . ";";
451
                }
452
            }
453
        }
454
455
        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

455
        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...
456
    }
457
458
    /**
459
     * Create a table in the database.
460
     * Build the default fields, indexes and values defined in the model.
461
     *
462
     * @param string $tableName
463
     *
464
     * @return bool
465
     * @throws DebugBarException
466
     */
467
468
    private static function createTable(string $tableName): bool
469
    {
470
        $tabla = self::$bbddStructure[$tableName];
471
        $sql = self::createFields($tableName, $tabla['fields']);
472
473
        foreach ($tabla['keys'] as $name => $index) {
474
            $sql .= self::createIndex($tableName, $name, $index);
475
        }
476
        // TODO: values no existe, hay que cargar los datos de seeds.
477
        if (isset($tabla['values'])) {
478
            $sql .= self::setValues($tableName, $tabla['values']);
479
        }
480
481
        return Engine::exec($sql);
482
    }
483
484
    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...
485
    {
486
        dump([
487
            'tablename' => $tableName,
488
            'fieldname' => $fieldName,
489
            'new structure' => self::$bbddStructure[$tableName]['fields'][$fieldName],
490
            'structure' => $structure,
491
        ]);
492
        return '';
493
    }
494
495
    private static function updateTable(string $tableName): bool
496
    {
497
        $yamlStructure = self::$bbddStructure[$tableName];
498
        $dbStructure = DB::getColumns($tableName);
499
500
        foreach ($yamlStructure['fields'] as $field => $newStructure) {
501
            $oldDb = $dbStructure[$field];
502
            $newDb = $newStructure['db'];
503
504
            $dif = array_diff($oldDb, $newDb);
505
            $data = [
506
                'field' => $field,
507
                'dbStructure' => $dbStructure[$field],
508
                'fields of ' . $tableName => $newStructure['db'],
509
                'oldDb' => $oldDb,
510
                'newDb' => $newDb,
511
            ];
512
            if (count($dif) > 0) {
513
                $data['diferencias 1'] = $dif;
514
                $data['diferencias 2'] = array_diff($newDb, $oldDb);
515
                $data['sql'] = DB::modify($tableName, $oldDb, $newDb);
516
            }
517
518
            dump($data);
519
        }
520
521
//        die('Here');
522
523
        return Engine::exec(DB::modify($tableName, $oldDb, $newDb));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $newDb seems to be defined by a foreach iteration on line 500. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
Comprehensibility Best Practice introduced by
The variable $oldDb seems to be defined by a foreach iteration on line 500. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
524
    }
525
526
    /**
527
     * Build the SQL statement to create the fields in the table.
528
     * It can also create the primary key if the auto_increment attribute is defined.
529
     *
530
     * @param string $tablename
531
     * @param array  $fieldList
532
     *
533
     * @return string
534
     */
535
    protected static function createFields(string $tablename, array $fieldList): string
536
    {
537
        $tablenameWithPrefix = Config::$dbPrefix . $tablename;
538
539
        $sql = "CREATE TABLE $tablenameWithPrefix ( ";
540
        foreach ($fieldList as $index => $column) {
541
            $col = $column['schema'];
542
            if (!isset($col['dbtype'])) {
543
                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...
544
            }
545
546
            $sql .= '`' . $index . '` ' . $col['dbtype'];
547
            $nulo = isset($col['null']) && $col['null'];
548
549
            $sql .= ($nulo ? '' : ' NOT') . ' NULL';
550
551
            if (isset($col['extra']) && (strtolower($col['extra']) == 'auto_increment')) {
552
                $sql .= ' PRIMARY KEY AUTO_INCREMENT';
553
            }
554
555
            $tmpDefecto = $col['default'] ?? null;
556
            $defecto = '';
557
            if (isset($tmpDefecto)) {
558
                if ($tmpDefecto == 'CURRENT_TIMESTAMP') {
559
                    $defecto = "$tmpDefecto";
560
                } else {
561
                    $defecto = "'$tmpDefecto'";
562
                }
563
            } else {
564
                if ($nulo) {
565
                    $defecto = 'NULL';
566
                }
567
            }
568
569
            if ($defecto != '') {
570
                $sql .= ' DEFAULT ' . $defecto;
571
            }
572
573
            $sql .= ', ';
574
        }
575
        $sql = substr($sql, 0, -2); // Quitamos la coma y el espacio del final
576
        $sql .= ') ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;' . self::CRLF;
577
578
        return $sql;
579
    }
580
581
    /**
582
     * Create the SQL statements for the construction of one index.
583
     * In the case of the primary index, it is not necessary if it is auto_increment.
584
     *
585
     * TODO:
586
     *
587
     * Moreover, it should not be defined if it is auto_increment because it would
588
     * generate an error when it already exists.
589
     *
590
     * @param string $tableName
591
     * @param string $indexname
592
     * @param array  $indexData
593
     *
594
     * @return string
595
     */
596
    protected static function createIndex($tableName, $indexname, $indexData)
597
    {
598
        $sql = "ALTER TABLE $tableName ADD CONSTRAINT $indexname ";
599
600
        $command = '';
601
        // https://www.w3schools.com/sql/sql_primarykey.asp
602
        // ALTER TABLE Persons ADD CONSTRAINT PK_Person PRIMARY KEY (ID,LastName);
603
        if (isset($indexData['PRIMARY'])) {
604
            $command = 'PRIMARY KEY ';
605
            $fields = $indexData['PRIMARY'];
606
        }
607
608
        // https://www.w3schools.com/sql/sql_create_index.asp
609
        // CREATE INDEX idx_pname ON Persons (LastName, FirstName);
610
        if (isset($indexData['INDEX'])) {
611
            $command = 'INDEX ';
612
            $fields = $indexData['INDEX'];
613
        }
614
615
        // https://www.w3schools.com/sql/sql_unique.asp
616
        // ALTER TABLE Persons ADD CONSTRAINT UC_Person UNIQUE (ID,LastName);
617
        if (isset($indexData['UNIQUE'])) {
618
            $command = 'UNIQUE INDEX ';
619
            $fields = $indexData['UNIQUE'];
620
        }
621
622
        if ($command == '') {
623
            // https://www.w3schools.com/sql/sql_foreignkey.asp
624
            // ALTER TABLE Orders ADD CONSTRAINT FK_PersonOrder FOREIGN KEY (PersonID) REFERENCES Persons(PersonID);
625
            if (isset($indexData['FOREIGN'])) {
626
                $command = 'FOREIGN KEY ';
627
                $foreignField = $indexData['FOREIGN'];
628
                if (isset($indexData['REFERENCES'])) {
629
                    $references = $indexData['REFERENCES'];
630
                    if (!is_array($references)) {
631
                        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...
632
                    }
633
                    if (count($references) != 1) {
634
                        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...
635
                    }
636
                    $refTable = key($references);
637
                    $fields = '(' . implode(',', $references) . ')';
638
                } else {
639
                    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...
640
                }
641
642
                $sql .= $command . ' ' . $foreignField . ' REFERENCES ' . $refTable . $fields;
643
644
                if (isset($indexData['ON']) && is_array($indexData['ON'])) {
645
                    foreach ($indexData['ON'] as $key => $value) {
646
                        $sql .= ' ON ' . $key . ' ' . $value . ', ';
647
                    }
648
                    $sql = substr($sql, 0, -2); // Quitamos el ', ' de detrás
649
                }
650
            }
651
        } else {
652
            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...
653
                $fields = '(' . implode(',', $fields) . ')';
654
            } else {
655
                $fields = "($fields)";
656
            }
657
658
            if ($command == 'INDEX ') {
659
                $sql = "CREATE INDEX {$indexname} ON {$tableName}" . $fields;
660
            } else {
661
                $sql .= $command . ' ' . $fields;
662
            }
663
        }
664
665
        return $sql . ';' . self::CRLF;
666
    }
667
668
    /**
669
     * Create the SQL statements to fill the table with default data.
670
     *
671
     * @param string $tableName
672
     * @param array  $values
673
     *
674
     * @return string
675
     */
676
    protected static function setValues(string $tableName, array $values): string
677
    {
678
        $sql = "INSERT INTO $tableName ";
679
        $header = true;
680
        foreach ($values as $value) {
681
            $fields = "(";
682
            $datos = "(";
683
            foreach ($value as $fname => $fvalue) {
684
                $fields .= $fname . ", ";
685
                $datos .= "'$fvalue'" . ", ";
686
            }
687
            $fields = substr($fields, 0, -2) . ") ";
688
            $datos = substr($datos, 0, -2) . "), ";
689
690
            if ($header) {
691
                $sql .= $fields . " VALUES ";
692
                $header = false;
693
            }
694
695
            $sql .= $datos;
696
        }
697
698
        return substr($sql, 0, -2) . self::CRLF;
699
    }
700
}
701