Engine::select()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 2
dl 0
loc 9
ccs 5
cts 6
cp 0.8333
crap 3.0416
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Alxarafe. Development of PHP applications in a flash!
4
 * Copyright (C) 2018-2020 Alxarafe <[email protected]>
5
 */
6
7
namespace Alxarafe\Core\Database;
8
9
use Alxarafe\Core\Base\CacheCore;
10
use Alxarafe\Core\Helpers\Utils\ClassUtils;
11
use Alxarafe\Core\Helpers\Utils\FileSystemUtils;
12
use Alxarafe\Core\Providers\Database;
13
use Alxarafe\Core\Providers\DebugTool;
14
use Alxarafe\Core\Providers\FlashMessages;
15
use Alxarafe\Core\Providers\Logger;
16
use DebugBar\DataCollector\PDO as PDODataCollector;
17
use DebugBar\DebugBarException;
18
use PDO;
19
use PDOException;
20
use PDOStatement;
21
use Psr\Cache\InvalidArgumentException;
22
23
/**
24
 * Engine provides generic support for databases.
25
 * The class will have to be extended by completing the particularities of each of them.
26
 */
27
abstract class Engine
28
{
29
    /**
30
     * The debug tool used.
31
     *
32
     * @var DebugTool
33
     */
34
    public static $debugTool;
35
36
    /**
37
     * Contains the database structure data.
38
     * Each table is a index of the associative array.
39
     *
40
     * @var array
41
    * public static $dbStructure = [];
42
     */
43
44
    /**
45
     * Data Source Name
46
     *
47
     * @var string
48
     */
49
    protected static $dsn;
50
51
    /**
52
     * Array that contains the access data to the database.
53
     *
54
     * @var array
55
     */
56
    protected static $dbConfig;
57
58
    /**
59
     * The handler of the database.
60
     *
61
     * @var PDO
62
     */
63
    protected static $dbHandler;
64
65
    /**
66
     * Represents a prepared statement and, after the statement is executed,
67
     * an associated result set.
68
     *
69
     * @var PDOStatement|bool
70
     */
71
    protected static $statement;
72
73
    /**
74
     * True if the database engine supports SAVEPOINT in transactions
75
     *
76
     * @var bool
77
     */
78
    protected static $savePointsSupport = true;
79
80
    /**
81
     * Number of transactions in execution
82
     *
83
     * @var int
84
     */
85
    protected static $transactionDepth = 0;
86
87
    /**
88
     * PDO Data collector.
89
     *
90
     * @var PDODataCollector\PDOCollector
91
     */
92
    protected static $pdoCollector;
93
94
    /**
95
     * Connection between PHP and a database server.
96
     *
97
     * @var PDO
98
     */
99
    protected static $pdo;
100
101
    /**
102
     * Engine constructor
103
     *
104
     * @param array $dbConfig
105
     */
106
    public function __construct(array $dbConfig)
107
    {
108
        $shortName = ClassUtils::getShortName($this, static::class);
109
110
        if (!isset(self::$dbConfig)) {
111
            self::$dbConfig = $dbConfig;
112
            self::$debugTool = DebugTool::getInstance();
113
            self::$debugTool->startTimer($shortName, $shortName . ' Engine Constructor');
114
            self::$debugTool->stopTimer($shortName);
115
        }
116
    }
117
118
    /**
119
     * Return a list of available database engines.
120
     *
121
     * @return array
122
     */
123 5
    public static function getEngines(): array
124
    {
125 5
        $path = constant('ALXARAFE_FOLDER') . DIRECTORY_SEPARATOR . 'Database' . DIRECTORY_SEPARATOR . 'Engines';
126 5
        $engines = FileSystemUtils::scandir($path);
127 5
        $ret = [];
128
        // Unset engines not fully supported
129 5
        $unsupported = self::unsupportedEngines();
130 5
        foreach ($engines as $engine) {
131 5
            if ($engine->getExtension() === 'php') {
132 5
                $engineName = substr($engine->getFilename(), 0, strlen($engine->getFilename()) - strlen($engine->getExtension()) - 1);
133 5
                if (in_array($engineName, $unsupported, true)) {
134 5
                    continue;
135
                }
136 5
                $ret[] = $engineName;
137
            }
138
        }
139 5
        return $ret;
140
    }
141
142
    /**
143
     * Returns a list of unsupported engines.
144
     * The unsupported engines here are the not fully supported yet.
145
     *
146
     * @return array
147
     */
148 5
    public static function unsupportedEngines(): array
149
    {
150
        //return [];
151 5
        return ['PdoFirebird'];
152
    }
153
154
    /**
155
     * Obtain an array with the table structure with a standardized format.
156
     *
157
     * @param string $tableName
158
     * @param bool   $usePrefix
159
     *
160
     * @return array
161
     */
162
    public static function getStructure(string $tableName, bool $usePrefix = true): array
163
    {
164
        return [
165
            'fields' => Database::getInstance()->getSqlHelper()->getColumns($tableName, $usePrefix),
166
            'indexes' => Database::getInstance()->getSqlHelper()->getIndexes($tableName, $usePrefix),
167
        ];
168
    }
169
170
    /**
171
     * Execute SQL statements on the database (INSERT, UPDATE or DELETE).
172
     *
173
     * @param array $queries
174
     *
175
     * @return bool
176
     */
177
    final public static function batchExec(array $queries): bool
178
    {
179
        $ok = true;
180
        foreach ($queries as $query) {
181
            $query = trim($query);
182
            if ($query !== '') {
183
                // TODO: The same variables are passed for all queries.
184
                $ok &= self::exec($query);
185
            }
186
        }
187
        return (bool) $ok;
188
    }
189
190
    /**
191
     * Prepare and execute the query.
192
     *
193
     * @param string $query
194
     * @param array  $vars
195
     *
196
     * @return bool
197
     */
198 15
    final public static function exec(string $query, $vars = []): bool
199
    {
200
        // Remove extra blankspace to be more readable
201 15
        $query = preg_replace('/\s+/', ' ', $query);
202 15
        $ok = false;
203 15
        self::$statement = self::$dbHandler->prepare($query);
204 15
        if (self::$statement) {
205 15
            $ok = self::$statement->execute($vars);
206
        }
207 15
        if (!$ok) {
208 9
            self::$debugTool->addMessage('SQL', 'PDO ERROR in exec: ' . $query);
209
        }
210 15
        return $ok;
211
    }
212
213
    /**
214
     * Executes a SELECT SQL statement on the core cache.
215
     *
216
     * @param string $cachedName
217
     * @param string $query
218
     * @param array  $vars
219
     *
220
     * @return array
221
     */
222 3
    final public static function selectCoreCache(string $cachedName, string $query, array $vars = []): array
223
    {
224 3
        if (constant('CORE_CACHE_ENABLED') === true) {
225
            $cacheEngine = CacheCore::getInstance()->getEngine();
226
            try {
227
                $cacheItem = $cacheEngine->getItem($cachedName);
228
            } catch (InvalidArgumentException $e) {
229
                $cacheItem = null;
230
                Logger::getInstance()::exceptionHandler($e);
231
            }
232
            if ($cacheItem && !$cacheItem->isHit()) {
233
                $cacheItem->set(self::select($query, $vars));
234
                if ($cacheEngine->save($cacheItem)) {
235
                    self::$debugTool->addMessage('messages', "Cache data saved to '" . $cachedName . "'.");
236
                } else {
237
                    self::$debugTool->addMessage('messages', 'Cache data not saved.');
238
                }
239
            }
240
            if ($cacheItem && $cacheEngine->hasItem($cachedName)) {
241
                return $cacheItem->get();
242
            }
243
            return [];
244
        }
245 3
        return self::select($query, $vars);
246
    }
247
248
    /**
249
     * Executes a SELECT SQL statement on the database, returning the result in an array.
250
     * In case of failure, return NULL. If there is no data, return an empty array.
251
     *
252
     * @param string $query
253
     * @param array  $vars
254
     *
255
     * @return array
256
     */
257 24
    public static function select(string $query, array $vars = []): array
258
    {
259
        // Remove extra blankspace to be more readable
260 24
        $query = preg_replace('/\s+/', ' ', $query);
261 24
        self::$statement = self::$dbHandler->prepare($query);
262 24
        if (self::$statement && self::$statement->execute($vars)) {
263 24
            return self::$statement->fetchAll(PDO::FETCH_ASSOC);
264
        }
265
        return [];
266
    }
267
268
    /**
269
     * Clear item from cache.
270
     *
271
     * @param string $cachedName
272
     *
273
     * @return bool
274
     */
275
    final public static function clearCoreCache(string $cachedName): bool
276
    {
277
        $cacheEngine = CacheCore::getInstance()->getEngine();
278
        if (isset($cacheEngine)) {
279
            try {
280
                return $cacheEngine->deleteItem($cachedName);
281
            } catch (InvalidArgumentException $e) {
282
                Logger::getInstance()::exceptionHandler($e);
283
            }
284
        }
285
        return false;
286
    }
287
288
    /**
289
     * Engine destructor
290
     */
291 650
    public function __destruct()
292
    {
293 650
        $this->rollBackTransactions();
294 650
    }
295
296
    /**
297
     * Undo all active transactions
298
     */
299 650
    private function rollBackTransactions(): void
300
    {
301 650
        while (self::$transactionDepth > 0) {
302
            $this->rollBack();
303
        }
304 650
    }
305
306
    /**
307
     * Rollback current transaction,
308
     *
309
     * @return bool
310
     */
311
    final public function rollBack(): bool
312
    {
313
        $ret = true;
314
315
        if (self::$transactionDepth === 0) {
316
            throw new PDOException('Rollback error : There is no transaction started');
317
        }
318
319
        self::$debugTool->addMessage('SQL', 'Rollback, savepoint LEVEL' . self::$transactionDepth);
320
        self::$transactionDepth--;
321
322
        if (self::$transactionDepth === 0 || !self::$savePointsSupport) {
323
            $ret = self::$dbHandler->rollBack();
324
        } else {
325
            $transactionDepth = self::$transactionDepth;
326
            $sql = "ROLLBACK TO SAVEPOINT LEVEL{$transactionDepth};";
327
            self::exec($sql);
328
        }
329
330
        return $ret;
331
    }
332
333
    /**
334
     * Returns the id of the last inserted record. Failing that, it returns ''.
335
     *
336
     * @return string
337
     */
338 2
    final public function getLastInserted(): string
339
    {
340 2
        $data = self::select('SELECT @@identity AS id');
341 2
        if (count($data) > 0) {
342 2
            return $data[0]['id'];
343
        }
344
        return '';
345
    }
346
347
    /**
348
     * Returns if a database connection exists or not.
349
     *
350
     * @return bool
351
     */
352
    public function checkConnection(): bool
353
    {
354
        return (self::$dbHandler != null);
355
    }
356
357
    /**
358
     * Establish a connection to the database.
359
     * If a connection already exists, it returns it. It does not establish a new one.
360
     * Returns true in case of success, assigning the handler to self::$dbHandler.
361
     *
362
     * @param array $config
363
     *
364
     * @return bool
365
     */
366
    public function connect(array $config = []): bool
367
    {
368
        if (self::$dbHandler != null) {
369
            self::$debugTool->addMessage('SQL', 'PDO: Already connected ' . self::$dsn);
370
            return true;
371
        }
372
        self::$debugTool->addMessage('SQL', 'PDO: ' . self::$dsn);
373
        try {
374
            // Logs SQL queries. You need to wrap your PDO object into a DebugBar\DataCollector\PDO\TraceablePDO object.
375
            // http://phpdebugbar.com/docs/base-collectors.html
376
            self::$pdo = new PDO(self::$dsn, self::$dbConfig['dbUser'], self::$dbConfig['dbPass'], $config);
377
            self::$dbHandler = new PDODataCollector\TraceablePDO(self::$pdo);
378
            self::$pdoCollector = new PDODataCollector\PDOCollector(self::$dbHandler);
379
            self::$debugTool->getDebugTool()->addCollector(self::$pdoCollector);
380
        } catch (PDOException $e) {
381
            Logger::getInstance()::exceptionHandler($e);
382
            FlashMessages::getInstance()::setError($e->getMessage());
383
            return false;
384
        } catch (DebugBarException $e) {
385
            Logger::getInstance()::exceptionHandler($e);
386
            FlashMessages::getInstance()::setError($e->getMessage());
387
            return false;
388
        }
389
        return isset(self::$dbHandler);
390
    }
391
392
    /**
393
     * Prepares a statement for execution and returns a statement object
394
     *
395
     * @doc http://php.net/manual/en/pdo.prepare.php
396
     *
397
     * @param string $sql
398
     * @param array  $options
399
     *
400
     * @return bool
401
     */
402
    final public function prepare(string $sql, array $options = []): bool
403
    {
404
        if (!isset(self::$dbHandler)) {
405
            return false;
406
        }
407
        // Remove extra blankspace to be more readable
408
        $sql = preg_replace('/\s+/', ' ', $sql);
409
        self::$statement = self::$dbHandler->prepare($sql, $options);
410
        return (bool) self::$statement;
411
    }
412
413
    /**
414
     * Returns an array containing all of the result set rows
415
     *
416
     * @return array
417
     */
418
    final public function resultSet(): array
419
    {
420
        $this->execute();
421
        return self::$statement->fetchAll(PDO::FETCH_ASSOC);
422
    }
423
424
    /**
425
     * Executes a prepared statement
426
     *
427
     * @doc http://php.net/manual/en/pdostatement.execute.php
428
     *
429
     * @param array $inputParameters
430
     *
431
     * @return bool
432
     */
433
    final public function execute(array $inputParameters = []): bool
434
    {
435
        if (!isset(self::$statement) || !self::$statement) {
436
            return false;
437
        }
438
        return self::$statement->execute($inputParameters);
439
    }
440
441
    /**
442
     * Start transaction
443
     *
444
     * @doc https://www.ibm.com/support/knowledgecenter/es/SSEPGG_9.1.0/com.ibm.db2.udb.apdv.php.doc/doc/t0023166.htm
445
     * Transactions support
446
     * @doc https://coderwall.com/p/rml5fa/nested-pdo-transactions
447
     *
448
     * @return bool
449
     */
450 1
    final public function beginTransaction(): bool
451
    {
452 1
        $ret = true;
453 1
        if (self::$transactionDepth === 0 || !self::$savePointsSupport) {
454 1
            $ret = self::$dbHandler->beginTransaction();
455
        } else {
456
            $transactionDepth = self::$transactionDepth;
457
            $sql = "SAVEPOINT LEVEL{$transactionDepth};";
458
            self::exec($sql);
459
        }
460
461 1
        self::$transactionDepth++;
462 1
        self::$debugTool->addMessage('SQL', 'Transaction started, savepoint LEVEL' . self::$transactionDepth . ' saved');
463
464 1
        return $ret;
465
    }
466
467
    /**
468
     * Commit current transaction
469
     *
470
     * @return bool
471
     */
472 1
    final public function commit(): bool
473
    {
474 1
        $ret = true;
475
476 1
        self::$debugTool->addMessage('SQL', 'Commit, savepoint LEVEL' . self::$transactionDepth);
477 1
        self::$transactionDepth--;
478
479 1
        if (self::$transactionDepth === 0 || !self::$savePointsSupport) {
480 1
            $ret = self::$dbHandler->commit();
481
        } else {
482
            $transactionDepth = self::$transactionDepth;
483
            $sql = "RELEASE SAVEPOINT LEVEL{$transactionDepth};";
484
            self::exec($sql);
485
        }
486
487 1
        return $ret;
488
    }
489
490
    /**
491
     * Returns database structure.
492
     *
493
     * @return array
494
    * final public function getDbStructure(): array
495
    * {
496
        * return self::$dbStructure;
497
     * }
498
     */
499
500
    /**
501
     * Returns database table structure.
502
     *
503
     * @param string $tablename
504
     *
505
     * @return array
506
    * final public function getDbTableStructure(string $tablename)
507
    * {
508
        * return self::$dbStructure[$tablename] ?? [];
509
     * }
510
     */
511
512
    /**
513
     * Returns if table is set to database structure.
514
     *
515
     * @param string $tablename
516
     *
517
     * @return bool
518
    * final public function issetDbTableStructure(string $tablename): bool
519
    * {
520
        * return isset(self::$dbStructure[$tablename]);
521
    * }
522
     */
523
524
    /**
525
     * Returns if key is set to database structure.
526
     *
527
     * @param string $tablename
528
     * @param string $key
529
     *
530
     * @return bool
531
    * final public function issetDbTableStructureKey(string $tablename, string $key): bool
532
    * {
533
        * return isset(self::$dbStructure[$tablename][$key]);
534
    * }
535
     */
536
537
    /**
538
     * Sets database structure for a tablename.
539
     *
540
     * @param string $tablename
541
     * @param array  $data
542
     *     final public function setDbTableStructure(string $tablename, array $data): void
543
     *     {
544
     *     self::$dbStructure[$tablename] = $data;
545
     *     }
546
     */
547
548
    /**
549
     * Returns details about last error.
550
     *
551
     * @return string
552
     */
553
    abstract public function getError(): string;
554
}
555