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

Engine   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 77
dl 0
loc 344
rs 9.28
c 0
b 0
f 0
wmc 39

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getEngines() 0 10 5
A beginTransaction() 0 15 4
A exec() 0 7 3
A connect() 0 17 3
A __destruct() 0 3 1
A rollbackTransactions() 0 4 2
A select() 0 7 4
A __construct() 0 3 1
A execute() 0 6 3
A commit() 0 14 3
A prepare() 0 7 2
A checkConnection() 0 3 1
A getLastInserted() 0 7 2
A getTableName() 0 3 1
A rollBack() 0 18 4
1
<?php
2
/**
3
 * Alxarafe. Development of PHP applications in a flash!
4
 * Copyright (C) 2023 Alxarafe <[email protected]>
5
 */
6
7
namespace Alxarafe\Database;
8
9
use Alxarafe\Core\Singletons\Config;
10
use Alxarafe\Core\Singletons\Debug;
11
use DebugBar\DataCollector\PDO\PDOCollector;
12
use DebugBar\DataCollector\PDO\TraceablePDO;
13
use PDO;
14
use PDOException;
15
use PDOStatement;
16
17
/**
18
 * Class Engine
19
 *
20
 * Proporciona soporte genérico a bases de datos usando PDO.
21
 * Cada motor que desee desarrollarse extenderá esta clase y añadirá sus particularidades.
22
 * Se usa desde la clase estática DB.
23
 *
24
 * @author  Rafael San José Tovar <[email protected]>
25
 * @version 2023.0108
26
 *
27
 * @package Alxarafe\Database
28
 */
29
abstract class Engine
30
{
31
    /**
32
     * Ruta en la que se encuentran los motores de la base de datos
33
     */
34
    const ENGINES_FOLDER = BASE_DIR . '/src/Database/Engines';
35
36
    /**
37
     * Data Source Name
38
     *
39
     * @var string
40
     */
41
    protected static string $dsn;
42
43
    /**
44
     * Array con los parámetros de acceso a la base de datos
45
     *
46
     * @var array
47
     */
48
    protected static array $dbConfig;
49
50
    /**
51
     * Instancia PDO
52
     *
53
     * @var PDO
54
     */
55
    protected static PDO $dbHandler;
56
57
    /**
58
     * Instancia PDOStatement
59
     *
60
     * @var PDOStatement|false
61
     */
62
    protected static $statement;
63
64
    /**
65
     * Indica si la base de datos soporta SAVEPOINT en las transacciones.
66
     * Si la base de datos no lo usa, el descendiente deberá de ponerlo a "false".
67
     * SAVEPOINT es una forma de anidar transacciones.
68
     *
69
     * @source https://coderwall.com/p/rml5fa/nested-pdo-transactions
70
     *
71
     * @var bool
72
     */
73
    protected static bool $savePointsSupport = true;
74
75
    /**
76
     * Indica el número de transacciones que están siendo ejecutadas.
77
     *
78
     * @var int
79
     */
80
    protected static int $transactionDepth = 0;
81
82
    /**
83
     * Engine constructor
84
     *
85
     * @param array $dbConfig
86
     */
87
    public function __construct(array $dbConfig)
88
    {
89
        self::$dbConfig = $dbConfig;
90
    }
91
92
    /**
93
     * Engine destructor. Cancela las transacciones pendientes.
94
     */
95
    public function __destruct()
96
    {
97
        $this->rollbackTransactions();
98
    }
99
100
    /**
101
     * Obtiene un array con los motores de base de datos disponibles.
102
     *
103
     * TODO: La extracción de archivos de una ruta por su extensión se usa en más
104
     *       de una ocasión. Habría que plantearse un método estático que lo facilite.
105
     *
106
     * @author  Rafael San José Tovar <[email protected]>
107
     * @version 2023.0108
108
     *
109
     * @return array
110
     */
111
    public static function getEngines(): array
112
    {
113
        $engines = scandir(self::ENGINES_FOLDER);
114
        $ret = [];
115
        foreach ($engines as $engine) {
116
            if ($engine != '.' && $engine != '..' && substr($engine, -4) == '.php') {
117
                $ret[] = substr($engine, 0, strlen($engine) - 4);
118
            }
119
        }
120
        return $ret;
121
    }
122
123
    /**
124
     * Retorna el nombre físico de la tabla. Estará en minúsculas y contendrá el prefijo de
125
     * la base de datos.
126
     *
127
     * @author  Rafael San José Tovar <[email protected]>
128
     * @version 2023.0108
129
     *
130
     * @param $tablename
131
     *
132
     * @return string
133
     */
134
    public static function getTableName($tableName): string
135
    {
136
        return Config::$dbPrefix . strtolower($tableName);
137
    }
138
139
    /**
140
     * Cancela las transacciones pendientes.
141
     *
142
     * @author  Rafael San José Tovar <[email protected]>
143
     * @version 2023.0108
144
     *
145
     */
146
    final public static function rollbackTransactions()
147
    {
148
        while (self::$transactionDepth > 0) {
149
            self::rollback();
150
        }
151
    }
152
153
    /**
154
     * Cancela la transacción en curso
155
     *
156
     * @author  Rafael San José Tovar <[email protected]>
157
     * @version 2023.0108
158
     *
159
     * @return bool
160
     */
161
    final public static function rollBack(): bool
162
    {
163
        $ret = true;
164
165
        if (self::$transactionDepth == 0) {
166
            throw new PDOException('Rollback error : There is no transaction started');
167
        }
168
169
        Debug::sqlMessage('Rollback, savepoint LEVEL' . self::$transactionDepth);
170
        self::$transactionDepth--;
171
172
        if (self::$transactionDepth == 0 || !self::$savePointsSupport) {
173
            $ret = self::$dbHandler->rollBack();
174
        } else {
175
            self::exec('ROLLBACK TO SAVEPOINT LEVEL' . self::$transactionDepth);
176
        }
177
178
        return $ret;
179
    }
180
181
    /**
182
     * Ejecuta una sentencia SQL (INSERT, UPDATE o DELETE).
183
     *
184
     * @author  Rafael San José Tovar <[email protected]>
185
     * @version 2023.0108
186
     *
187
     * @param string $query
188
     *
189
     * @return bool
190
     */
191
    final public static function exec(string $query): bool
192
    {
193
        self::$statement = self::$dbHandler->prepare($query);
194
        if (self::$statement != null && self::$statement) {
195
            return self::$statement->execute([]);
196
        }
197
        return false;
198
    }
199
200
    /**
201
     * Retorna el ID del último registro insertado. Vacío si no ha habido inserción.
202
     *
203
     * @author  Rafael San José Tovar <[email protected]>
204
     * @version 2023.0108
205
     *
206
     * @return string
207
     */
208
    final public static function getLastInserted(): string
209
    {
210
        $data = self::select('SELECT @@identity AS id');
211
        if (count($data) > 0) {
212
            return $data[0]['id'];
213
        }
214
        return '';
215
    }
216
217
    /**
218
     * Ejecuta el comando SELECT de SQL retornando la respuesta en un array.
219
     * Si no hay datos, retorna un array vacío.
220
     * En caso de error, retorna "false".
221
     *
222
     * @author  Rafael San José Tovar <[email protected]>
223
     * @version 2023.0108
224
     *
225
     * @param string $query
226
     *
227
     * @return array|false
228
     */
229
    public static function select(string $query)
230
    {
231
        self::$statement = self::$dbHandler->prepare($query);
232
        if (self::$statement != null && self::$statement && self::$statement->execute([])) {
233
            return self::$statement->fetchAll(PDO::FETCH_ASSOC);
234
        }
235
        return [];
236
    }
237
238
    /**
239
     * Comprueba si hay una conexión activa a la base de datos.
240
     *
241
     * @author  Rafael San José Tovar <[email protected]>
242
     * @version 2023.0108
243
     *
244
     * @return bool
245
     */
246
    public static function checkConnection(): bool
247
    {
248
        return (self::$dbHandler != null);
249
    }
250
251
    /**
252
     * Establece conexión con la base de datos.
253
     * Si ya existía una conexión, simplemente retorna éxito.
254
     * Retorna true si existe conexión, asignando el handler a self::$dbHandler.
255
     *
256
     * @author  Rafael San José Tovar <[email protected]>
257
     * @version 2023.0108
258
     *
259
     * @param array $config
260
     *
261
     * @return bool
262
     */
263
    public static function connect(array $config = []): bool
264
    {
265
        if (isset(self::$dbHandler)) {
266
            Debug::sqlMessage("PDO: Already connected " . self::$dsn);
267
            return true;
268
        }
269
        Debug::sqlMessage("PDO: " . self::$dsn);
270
        try {
271
            // Logs SQL queries. You need to wrap your PDO object into a DebugBar\DataCollector\PDO\TraceablePDO object.
272
            // http://phpdebugbar.com/docs/base-collectors.html
273
            self::$dbHandler = new TraceablePDO(new PDO(self::$dsn, self::$dbConfig['dbUser'], self::$dbConfig['dbPass'], $config));
274
            Debug::addCollector(new PDOCollector(self::$dbHandler));
275
        } catch (PDOException $e) {
276
            Debug::addException($e);
277
            return false;
278
        }
279
        return isset(self::$dbHandler);
280
    }
281
282
    /**
283
     * Prepara una sentencia para su ejecución y la retorna.
284
     *
285
     * @source https://php.net/manual/en/pdo.prepare.php
286
     *
287
     * @author  Rafael San José Tovar <[email protected]>
288
     * @version 2023.0108
289
     *
290
     * @param string $sql
291
     * @param array  $options
292
     *
293
     * @return bool
294
     */
295
    final public static function prepare(string $sql, array $options = []): bool
296
    {
297
        if (!isset(self::$dbHandler)) {
298
            return false;
299
        }
300
        self::$statement = self::$dbHandler->prepare($sql, $options);
301
        return (self::$statement != false);
302
    }
303
304
    /**
305
     * Ejecuta una sentencia SQL previamente preparada.
306
     *
307
     * @source https://php.net/manual/en/pdostatement.execute.php
308
     *
309
     * @author  Rafael San José Tovar <[email protected]>
310
     * @version 2023.0108
311
     *
312
     * @param array $inputParameters
313
     *
314
     * @return bool
315
     */
316
    final public static function execute(array $inputParameters = []): bool
317
    {
318
        if (!isset(self::$statement) || !self::$statement) {
319
            return false;
320
        }
321
        return self::$statement->execute($inputParameters);
322
    }
323
324
    /**
325
     * Inicia una transacción
326
     *
327
     * @source https://www.ibm.com/support/knowledgecenter/es/SSEPGG_9.1.0/com.ibm.db2.udb.apdv.php.doc/doc/t0023166.htm
328
     *
329
     * @author  Rafael San José Tovar <[email protected]>
330
     * @version 2023.0108
331
     *
332
     * @return bool
333
     */
334
    final public static function beginTransaction(): bool
335
    {
336
        if (self::$transactionDepth == 0 || !self::$savePointsSupport) {
337
            $ret = self::$dbHandler->beginTransaction();
338
        } else {
339
            $ret = self::exec('SAVEPOINT LEVEL' . self::$transactionDepth);
340
        }
341
342
        if (!$ret) {
343
            return false;
344
        }
345
        self::$transactionDepth++;
346
        Debug::sqlMessage('Transaction started, savepoint LEVEL' . self::$transactionDepth . ' saved');
347
348
        return true;
349
    }
350
351
    /**
352
     * Confirma la transacción en curso
353
     *
354
     * @author  Rafael San José Tovar <[email protected]>
355
     * @version 2023.0108
356
     *
357
     * @return bool
358
     */
359
    final public static function commit(): bool
360
    {
361
        $ret = true;
362
363
        Debug::sqlMessage('Commit, savepoint LEVEL' . self::$transactionDepth);
364
        self::$transactionDepth--;
365
366
        if (self::$transactionDepth == 0 || !self::$savePointsSupport) {
367
            $ret = self::$dbHandler->commit();
368
        } else {
369
            self::exec('RELEASE SAVEPOINT LEVEL' . self::$transactionDepth);
370
        }
371
372
        return $ret;
373
    }
374
}
375