BaseDBAL::createRecordset()   A
last analyzed

Complexity

Conditions 2
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 13
ccs 6
cts 6
cp 1
rs 10
cc 2
nc 3
nop 4
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace EngineWorks\DBAL\Abstracts;
6
7
use EngineWorks\DBAL\CommonTypes;
8
use EngineWorks\DBAL\DBAL;
9
use EngineWorks\DBAL\Exceptions\QueryException;
10
use EngineWorks\DBAL\Internal\ConvertObjectToStringMethod;
11
use EngineWorks\DBAL\Internal\NumericParser;
12
use EngineWorks\DBAL\Pager;
13
use EngineWorks\DBAL\Recordset;
14
use EngineWorks\DBAL\Settings;
15
use InvalidArgumentException;
16
use Psr\Log\LoggerInterface;
17
use Psr\Log\NullLogger;
18
use RuntimeException;
19
use Throwable;
20
21
abstract class BaseDBAL implements DBAL
22
{
23
    use ConvertObjectToStringMethod;
1 ignored issue
show
introduced by
The trait EngineWorks\DBAL\Interna...ertObjectToStringMethod requires some properties which are not provided by EngineWorks\DBAL\Abstracts\BaseDBAL: $value, $name
Loading history...
24
25
    /** @var LoggerInterface */
26
    protected $logger;
27
28
    /**
29
     * Settings object
30
     * @var Settings
31
     */
32
    protected $settings;
33
34
    /**
35
     * Contains the transaction level to do nested transactions
36
     * @var int
37
     */
38
    protected $transactionLevel = 0;
39
40
    /**
41
     * Contains the "prevent commit state" of transactions
42
     * @var bool
43
     */
44
    protected $preventCommit = false;
45
46
    /**
47
     * @param Settings $settings
48
     * @param LoggerInterface|null $logger If null then a NullLogger will be used
49
     */
50 388
    public function __construct(Settings $settings, LoggerInterface $logger = null)
51
    {
52 388
        $this->settings = $settings;
53 388
        $this->setLogger($logger ?? new NullLogger());
54
    }
55
56
    /**
57
     * Destructor - force call disconnect
58
     */
59 11
    public function __destruct()
60
    {
61 11
        $this->disconnect();
62
    }
63
64
    /**
65
     * Implement the transaction begin command
66
     */
67 150
    protected function commandTransactionBegin(): void
68
    {
69 150
        $this->execute('BEGIN TRANSACTION', 'Cannot start transaction');
70
    }
71
72
    /**
73
     * Implement the transaction commit command
74
     */
75 218
    protected function commandTransactionCommit(): void
76
    {
77 218
        $this->execute('COMMIT', 'Cannot commit transaction');
78
    }
79
80
    /**
81
     * Implement the transaction rollback command
82
     */
83 3
    protected function commandTransactionRollback(): void
84
    {
85 3
        $this->execute('ROLLBACK', 'Cannot rollback transaction');
86
    }
87
88
    /**
89
     * Implement the savepoint command
90
     * @param string $name
91
     */
92 4
    protected function commandSavepoint(string $name): void
93
    {
94 4
        $this->execute("SAVEPOINT $name", "Cannot create savepoint $name");
95
    }
96
97
    /**
98
     * Implement the release savepoint command
99
     * @param string $name
100
     */
101 4
    protected function commandReleaseSavepoint(string $name): void
102
    {
103 4
        $this->execute("RELEASE SAVEPOINT $name", "Cannot release savepoint $name");
104
    }
105
106
    /**
107
     * Implement the rollback to savepoint command
108
     * @param string $name
109
     */
110 2
    protected function commandRollbackToSavepoint(string $name): void
111
    {
112 2
        $this->execute("ROLLBACK TO SAVEPOINT $name", "Cannot rollback to savepoint $name");
113
    }
114
115 8
    final public function getTransactionLevel(): int
116
    {
117 8
        return $this->transactionLevel;
118
    }
119
120 293
    final public function transBegin(): void
121
    {
122 293
        $this->logger->info('-- TRANSACTION BEGIN');
123 293
        if (0 === $this->transactionLevel) {
124 293
            $this->commandTransactionBegin();
125
        } else {
126 8
            $this->commandSavepoint("LEVEL_{$this->transactionLevel}");
127
        }
128 293
        $this->transactionLevel = $this->transactionLevel + 1;
129
    }
130
131 293
    final public function transCommit(): void
132
    {
133 293
        $this->logger->info('-- TRANSACTION COMMIT');
134
        // reduce the transaction level
135 293
        if (0 === $this->transactionLevel) {
136 4
            trigger_error('Try to call commit without a transaction', E_USER_NOTICE);
137 4
            return;
138
        }
139 293
        $this->transactionLevel = $this->transactionLevel - 1;
140
        // do commit or savepoint
141 293
        if (0 === $this->transactionLevel) {
142 293
            if ($this->transPreventCommit()) {
143 4
                $this->transactionLevel = 1;
144 4
                trigger_error('Try to call final commit with prevent commit enabled', E_USER_ERROR);
145
            }
146 293
            $this->commandTransactionCommit();
147
        } else {
148 8
            $this->commandReleaseSavepoint("LEVEL_{$this->transactionLevel}");
149
        }
150
    }
151
152 8
    final public function transRollback(): void
153
    {
154 8
        $this->logger->info('-- TRANSACTION ROLLBACK ');
155
        // reduce the transaction level
156 8
        if (0 === $this->transactionLevel) {
157 4
            trigger_error('Try to call rollback without a transaction', E_USER_NOTICE);
158 4
            return;
159
        }
160 4
        $this->transactionLevel = $this->transactionLevel - 1;
161
        // do rollback or savepoint
162 4
        if (0 === $this->transactionLevel) {
163 4
            $this->commandTransactionRollback();
164
        } else {
165 4
            $this->commandRollbackToSavepoint("LEVEL_{$this->transactionLevel}");
166
        }
167
    }
168
169 293
    final public function transPreventCommit(bool $preventCommit = null): bool
170
    {
171 293
        if (null === $preventCommit) {
172 293
            return $this->preventCommit;
173
        }
174 8
        $previous = $this->preventCommit;
175 8
        $this->preventCommit = $preventCommit;
176 8
        return $previous;
177
    }
178
179 9
    final public function sqlTable(string $tableName, string $asTable = ''): string
180
    {
181 9
        return $this->sqlTableEscape($this->settings->get('prefix', '') . $tableName, $asTable);
182
    }
183
184 4
    final public function sqlField(string $fieldName, string $asFieldName = ''): string
185
    {
186 4
        return $fieldName . (('' !== $asFieldName) ? ' AS ' . $this->sqlFieldEscape($asFieldName) : '');
187
    }
188
189 325
    final public function sqlQuote(
190
        $variable,
191
        string $commonType = CommonTypes::TTEXT,
192
        bool $includeNull = false
193
    ): string {
194 325
        if (is_object($variable)) {
195 8
            $variable = $this->convertObjectToString($variable);
196
        }
197 325
        if ($includeNull && null === $variable) {
198 16
            return 'NULL';
199
        }
200 325
        if (! is_scalar($variable) && ! is_null($variable)) {
201
            throw new InvalidArgumentException('Value is something that cannot be parsed as scalar');
202
        }
203
        // CommonTypes::TTEXT is here because is the most common used type
204 325
        if (CommonTypes::TTEXT === $commonType) {
205 301
            return "'" . $this->sqlString($variable ?? '') . "'";
206
        }
207 321
        if (CommonTypes::TINT === $commonType) {
208 317
            return $this->sqlQuoteParseNumber($variable, true);
209
        }
210 301
        if (CommonTypes::TNUMBER === $commonType) {
211 297
            return $this->sqlQuoteParseNumber($variable, false);
212
        }
213 301
        if (CommonTypes::TBOOL === $commonType) {
214 297
            return ($variable) ? '1' : '0';
215
        }
216 301
        if (CommonTypes::TDATE === $commonType) {
217 8
            return "'" . date('Y-m-d', (int) $variable) . "'";
218
        }
219 301
        if (CommonTypes::TTIME === $commonType) {
220 8
            return "'" . date('H:i:s', (int) $variable) . "'";
221
        }
222 301
        if (CommonTypes::TDATETIME === $commonType) {
223 301
            return "'" . date('Y-m-d H:i:s', (int) $variable) . "'";
224
        }
225 8
        return "'" . $this->sqlString((string) $variable) . "'";
226
    }
227
228
    /**
229
     * @param scalar|null $value
230
     * @param bool $asInteger
231
     * @return string
232
     */
233 317
    private function sqlQuoteParseNumber($value, bool $asInteger): string
234
    {
235 317
        return (new NumericParser())->parseAsEnglish((string) $value, $asInteger);
236
    }
237
238 24
    final public function sqlQuoteIn(
239
        array $values,
240
        string $commonType = CommonTypes::TTEXT,
241
        bool $includeNull = false
242
    ): string {
243 24
        if ([] === $values) {
244 4
            throw new RuntimeException('The array of values passed to DBAL::sqlQuoteIn is empty');
245
        }
246 20
        return '('
247 20
            . implode(', ', array_map(function ($value) use ($commonType, $includeNull): string {
248 20
                return $this->sqlQuote($value, $commonType, $includeNull);
249 20
            }, array_unique($values)))
250 20
            . ')';
251
    }
252
253 12
    final public function sqlIn(
254
        string $field,
255
        array $values,
256
        string $commonType = CommonTypes::TTEXT,
257
        bool $positive = true,
258
        bool $includeNull = false
259
    ): string {
260 12
        if (! $positive) {
261 4
            trigger_error(
262 4
                __METHOD__ . ' with argument $positive = false is deprecated, use DBAL::sqlNotIn',
263 4
                E_USER_NOTICE
264 4
            );
265 4
            return $this->sqlNotIn($field, $values, $commonType, $includeNull);
266
        }
267 8
        if ([] === $values) {
268 4
            return '0 = 1';
269
        }
270 4
        return $field . ' IN ' . $this->sqlQuoteIn($values, $commonType, $includeNull);
271
    }
272
273 12
    final public function sqlNotIn(
274
        string $field,
275
        array $values,
276
        string $commonType = CommonTypes::TTEXT,
277
        bool $includeNull = false
278
    ): string {
279 12
        if ([] === $values) {
280 4
            return '1 = 1';
281
        }
282 8
        return $field . ' NOT IN ' . $this->sqlQuoteIn($values, $commonType, $includeNull);
283
    }
284
285 8
    final public function sqlIsNull(string $field, bool $positive = true): string
286
    {
287 8
        if (! $positive) {
288 4
            trigger_error(
289 4
                __METHOD__ . ' with argument $positive = false is deprecated, use DBAL::sqlIsNotNull',
290 4
                E_USER_NOTICE
291 4
            );
292 4
            return $this->sqlIsNotNull($field);
293
        }
294 4
        return $field . ' IS NULL';
295
    }
296
297 8
    final public function sqlIsNotNull(string $field): string
298
    {
299 8
        return $field . ' IS NOT NULL';
300
    }
301
302 4
    final public function sqlBetweenQuote(
303
        string $field,
304
        $lowerBound,
305
        $upperBound,
306
        string $commonType = CommonTypes::TTEXT
307
    ): string {
308 4
        return $field
309 4
            . ' BETWEEN ' . $this->sqlQuote($lowerBound, $commonType)
310 4
            . ' AND ' . $this->sqlQuote($upperBound, $commonType)
311 4
        ;
312
    }
313
314 4
    final public function sqlIfNull(string $fieldName, string $nullValue): string
315
    {
316 4
        return 'IFNULL(' . $fieldName . ', ' . $nullValue . ')';
317
    }
318
319 4
    final public function sqlLikeSearch(
320
        string $fieldName,
321
        string $searchTerms,
322
        bool $matchAnyTerm = true,
323
        string $termsSeparator = ' '
324
    ): string {
325 4
        if ('' === $termsSeparator) {
326
            throw new InvalidArgumentException('Arguments to explode terms must not be an empty string');
327
        }
328 4
        return implode(
329 4
            ($matchAnyTerm) ? ' OR ' : ' AND ',
330 4
            array_map(function (string $term) use ($fieldName): string {
331 4
                return '(' . $this->sqlLike($fieldName, $term) . ')';
332 4
            }, array_unique(array_filter(explode($termsSeparator, $searchTerms))))
333 4
        );
334
    }
335
336
    /**
337
     * Executes a query and return the number of affected rows
338
     *
339
     * @param string $query
340
     * @return int|false FALSE if the query fails
341
     */
342
    abstract protected function queryAffectedRows(string $query);
343
344
    /**
345
     * Return the last error message from the driver
346
     * @return string
347
     */
348
    abstract protected function getLastErrorMessage(): string;
349
350 293
    final public function execute(string $query, string $exceptionMessage = '')
351
    {
352 293
        $return = $this->queryAffectedRows($query);
353 293
        if (false === $return) {
354 4
            if ('' !== $exceptionMessage) {
355 4
                $previous = $this->getLastErrorMessage() ? new RuntimeException($this->getLastErrorMessage()) : null;
356 4
                throw new QueryException($exceptionMessage, $query, 0, $previous);
357
            }
358
            return false;
359
        }
360
361 293
        $this->logger->info("-- AffectedRows: $return");
362 293
        return $return;
363
    }
364
365 4
    final public function query(string $query)
366
    {
367 4
        trigger_error(__METHOD__ . ' is deprecated, use queryResult instead', E_USER_DEPRECATED);
368 4
        return $this->queryResult($query);
369
    }
370
371 70
    final public function queryOne(string $query, $default = false)
372
    {
373 70
        return current($this->queryRow($query) ?: [$default]);
374
    }
375
376 82
    final public function queryRow(string $query)
377
    {
378 82
        $result = $this->queryResult($query);
379 82
        if (false === $result) {
380 8
            return false;
381
        }
382
383 74
        $row = $result->fetchRow();
384 74
        if (false === $row) {
385 8
            return false;
386
        }
387
388 66
        return $row;
389
    }
390
391 12
    final public function queryValues(string $query, array $overrideTypes = [])
392
    {
393 12
        $recordset = $this->queryRecordset($query, '', [], $overrideTypes);
394 12
        if (false === $recordset) {
395 4
            return false;
396
        }
397 8
        if ($recordset->eof()) {
398 4
            return false;
399
        }
400 4
        return $recordset->values;
401
    }
402
403 24
    final public function queryArray(string $query)
404
    {
405 24
        $result = $this->queryResult($query);
406 24
        if (false === $result) {
407 8
            return false;
408
        }
409
410 16
        $return = [];
411 16
        while (false !== $row = $result->fetchRow()) {
412 12
            $return[] = $row;
413
        }
414 16
        return $return;
415
    }
416
417 12
    final public function queryArrayValues(string $query, array $overrideTypes = [])
418
    {
419 12
        $recordset = $this->queryRecordset($query, '', [], $overrideTypes);
420 12
        if (false === $recordset) {
421 4
            return false;
422
        }
423
424 8
        $return = [];
425 8
        while (! $recordset->eof()) {
426 4
            $return[] = $recordset->values;
427 4
            $recordset->moveNext();
428
        }
429 8
        return $return;
430
    }
431
432 16
    final public function queryArrayKey(string $query, string $keyField, string $keyPrefix = '')
433
    {
434 16
        $result = $this->queryResult($query);
435 16
        if (false === $result) {
436 4
            return false;
437
        }
438
439 12
        $return = [];
440
        /** @noinspection PhpAssignmentInConditionInspection */
441 12
        while ($row = $result->fetchRow()) {
442 8
            if (! array_key_exists($keyField, $row)) {
443 4
                return false;
444
            }
445 4
            $return[$keyPrefix . $row[$keyField]] = $row;
446
        }
447 8
        return $return;
448
    }
449
450 12
    final public function queryPairs(
451
        string $query,
452
        string $keyField,
453
        string $valueField,
454
        string $keyPrefix = '',
455
        $default = false
456
    ): array {
457 12
        $array = $this->queryArray($query) ?: [];
458 12
        $return = [];
459 12
        foreach ($array as $row) {
460 8
            $return[$keyPrefix . ($row[$keyField] ?? '')] = $row[$valueField] ?? $default;
461
        }
462 12
        return $return;
463
    }
464
465 28
    final public function queryArrayOne(string $query, string $field = '')
466
    {
467 28
        $result = $this->queryResult($query);
468 28
        if (false === $result) {
469 8
            return false;
470
        }
471
472 20
        $return = [];
473 20
        $verifiedFieldName = false;
474
        /** @noinspection PhpAssignmentInConditionInspection */
475 20
        while ($row = $result->fetchRow()) {
476 20
            if ('' === $field) {
477 12
                $keys = array_keys($row);
478 12
                $field = $keys[0];
479 12
                $verifiedFieldName = true;
480
            }
481 20
            if (! $verifiedFieldName) {
482 8
                if (! array_key_exists($field, $row)) {
483 4
                    return false;
484
                }
485 4
                $verifiedFieldName = true;
486
            }
487 16
            $return[] = $row[$field] ?? null;
488
        }
489 16
        return $return;
490
    }
491
492 12
    final public function queryOnString(string $query, string $default = '', string $separator = ', '): string
493
    {
494 12
        $array = $this->queryArrayOne($query);
495 12
        if (false === $array) {
496 4
            return $default;
497
        }
498 8
        return implode($separator, $array);
499
    }
500
501 27
    final public function queryRecordset(
502
        string $query,
503
        string $overrideEntity = '',
504
        array $overrideKeys = [],
505
        array $overrideTypes = []
506
    ) {
507
        try {
508 27
            $recordset = $this->createRecordset($query, $overrideEntity, $overrideKeys, $overrideTypes);
509 8
        } catch (Throwable $exception) {
510 8
            $this->logger->error("DBAL::queryRecordset failure running $query");
511 8
            return false;
512
        }
513 19
        return $recordset;
514
    }
515
516 42
    final public function createRecordset(
517
        string $query,
518
        string $overrideEntity = '',
519
        array $overrideKeys = [],
520
        array $overrideTypes = []
521
    ): Recordset {
522
        try {
523 42
            $recordset = new Recordset($this);
524 42
            $recordset->query($query, $overrideEntity, $overrideKeys, $overrideTypes);
525 12
        } catch (Throwable $exception) {
526 12
            throw new QueryException('Unable to create a valid Recordset', $query, 0, $exception);
527
        }
528 30
        return $recordset;
529
    }
530
531
    final public function queryPager(
532
        string $querySelect,
533
        string $queryCount = '',
534
        int $page = 1,
535
        int $recordsPerPage = 20
536
    ) {
537
        try {
538
            return $this->createPager($querySelect, $queryCount, $page, $recordsPerPage);
539
        } catch (Throwable $exception) {
540
            $this->logger->error("DBAL::queryPager failure running $querySelect");
541
            return false;
542
        }
543
    }
544
545 4
    final public function createPager(
546
        string $querySelect,
547
        string $queryCount = '',
548
        int $page = 1,
549
        int $recordsPerPage = 20
550
    ): Pager {
551 4
        $previous = null;
552
        try {
553 4
            $pager = new Pager($this, $querySelect, $queryCount);
554 4
            $pager->setPageSize($recordsPerPage);
555 4
            $success = (-1 == $page) ? $pager->queryAll() : $pager->queryPage($page);
556
            if (! $success) {
557
                $pager = false;
558
            }
559 4
        } catch (Throwable $exception) {
560 4
            $previous = $exception;
561 4
            $pager = false;
562
        }
563 4
        if (! ($pager instanceof Pager)) {
564 4
            throw new QueryException('Unable to create a valid Pager', $querySelect, 0, $previous);
565
        }
566
567
        return $pager;
568
    }
569
570 33
    final public function getLastMessage(): string
571
    {
572 33
        if ($this->isConnected()) {
573 33
            return $this->getLastErrorMessage();
574
        }
575
        return '';
576
    }
577
578
    final public function createQueryException(string $query, Throwable $previous = null): QueryException
579
    {
580
        return new QueryException($this->getLastMessage() ?: 'Database error', $query, 0, $previous);
581
    }
582
583 7
    final public function getLogger(): LoggerInterface
584
    {
585 7
        return $this->logger;
586
    }
587
588 388
    final public function setLogger(LoggerInterface $logger): void
589
    {
590 388
        $this->logger = $logger;
591
    }
592
}
593