Completed
Push — master ( 54db38...e6245f )
by Lars
01:29
created

Result::fetchCallable()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.0178

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 13
cts 14
cp 0.9286
rs 8.5066
c 0
b 0
f 0
cc 7
nc 9
nop 2
crap 7.0178
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\db;
6
7
use Arrayy\Arrayy;
8
use Symfony\Component\PropertyAccess\PropertyAccess;
9
use voku\helper\UTF8;
10
11
/**
12
 * Result: This class can handle the results from the "DB"-class.
13
 */
14
final class Result implements \Countable, \SeekableIterator, \ArrayAccess
15
{
16
    const MYSQL_TYPE_BIT = 16;
17
18
    const MYSQL_TYPE_BLOB = 252;
19
20
    const MYSQL_TYPE_DATE = 10;
21
22
    const MYSQL_TYPE_DATETIME = 12;
23
24
    const MYSQL_TYPE_DECIMAL = 0;
25
26
    const MYSQL_TYPE_DOUBLE = 5;
27
28
    const MYSQL_TYPE_ENUM = 247;
29
30
    const MYSQL_TYPE_FLOAT = 4;
31
32
    const MYSQL_TYPE_GEOMETRY = 255;
33
34
    const MYSQL_TYPE_INT24 = 9;
35
36
    const MYSQL_TYPE_JSON = 245;
37
38
    const MYSQL_TYPE_LONG = 3;
39
40
    const MYSQL_TYPE_LONGLONG = 8;
41
42
    const MYSQL_TYPE_LONG_BLOB = 251;
43
44
    const MYSQL_TYPE_MEDIUM_BLOB = 250;
45
46
    const MYSQL_TYPE_NEWDATE = 14;
47
48
    const MYSQL_TYPE_NEWDECIMAL = 246;
49
50
    const MYSQL_TYPE_NULL = 6;
51
52
    const MYSQL_TYPE_SET = 248;
53
54
    const MYSQL_TYPE_SHORT = 2;
55
56
    const MYSQL_TYPE_STRING = 254;
57
58
    const MYSQL_TYPE_TIME = 11;
59
60
    const MYSQL_TYPE_TIMESTAMP = 7;
61
62
    const MYSQL_TYPE_TINY = 1;
63
64
    const MYSQL_TYPE_TINY_BLOB = 249;
65
66
    const MYSQL_TYPE_VARCHAR = 15;
67
68
    const MYSQL_TYPE_VAR_STRING = 253;
69
70
    const MYSQL_TYPE_YEAR = 13;
71
72
    const RESULT_TYPE_ARRAY = 'array';
73
74
    const RESULT_TYPE_ARRAYY = 'Arrayy';
75
76
    const RESULT_TYPE_OBJECT = 'object';
77
78
    const RESULT_TYPE_YIELD = 'yield';
79
80
    /**
81
     * @var int
82
     */
83
    public $num_rows;
84
85
    /**
86
     * @var string
87
     */
88
    public $sql;
89
90
    /**
91
     * @var \Doctrine\DBAL\Statement|\mysqli_result
92
     */
93
    private $_result;
94
95
    /**
96
     * @var int
97
     */
98
    private $current_row;
99
100
    /**
101
     * @var \Closure|null
102
     */
103
    private $_mapper;
104
105
    /**
106
     * @var string
107
     */
108
    private $_default_result_type = self::RESULT_TYPE_OBJECT;
109
110
    /**
111
     * @var \mysqli_stmt|null
112
     */
113
    private $doctrineMySQLiStmt;
114
115
    /**
116
     * @var \Doctrine\DBAL\Driver\PDOStatement|null
117
     */
118
    private $doctrinePdoStmt;
119
120
    /**
121
     * @var int
122
     */
123
    private $doctrinePdoStmtDataSeekFake = 0;
124
125
    /**
126
     * @var bool
127
     */
128
    private $doctrinePdoStmtDataSeekInit = false;
129
130
    /**
131
     * @var array
132
     */
133
    private $doctrinePdoStmtDataSeekFakeCache = [];
134
135
    /**
136
     * Result constructor.
137
     *
138
     * @param string                                  $sql
139
     * @param \Doctrine\DBAL\Statement|\mysqli_result $result
140
     * @param \Closure                                $mapper Optional callback mapper for the "fetchCallable()" method
141
     */
142 101
    public function __construct(string $sql, $result, \Closure $mapper = null)
143
    {
144 101
        $this->sql = $sql;
145
146
        if (
147 101
            !$result instanceof \mysqli_result
148
            &&
149 101
            !$result instanceof \Doctrine\DBAL\Statement
150
        ) {
151
            throw new \InvalidArgumentException('$result must be ' . \mysqli_result::class . ' or ' . \Doctrine\DBAL\Statement::class . ' !');
152
        }
153
154 101
        $this->_result = $result;
155
156 101
        if ($this->_result instanceof \Doctrine\DBAL\Statement) {
157
            $doctrineDriver = $this->_result->getWrappedStatement();
158
159
            if ($doctrineDriver instanceof \Doctrine\DBAL\Driver\PDOStatement) {
160
                $this->doctrinePdoStmt = $doctrineDriver;
161
            }
162
163
            if ($doctrineDriver instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliStatement) {
164
                // try to get the mysqli driver from doctrine
165
                $reflectionTmp = new \ReflectionClass($doctrineDriver);
166
                $propertyTmp = $reflectionTmp->getProperty('_stmt');
167
                $propertyTmp->setAccessible(true);
168
                $this->doctrineMySQLiStmt = $propertyTmp->getValue($doctrineDriver);
169
            }
170
171
            $this->num_rows = $this->_result->rowCount();
172
        } else {
173 101
            $this->num_rows = (int) $this->_result->num_rows;
174
        }
175
176 101
        $this->current_row = 0;
177
178 101
        $this->_mapper = $mapper;
179 101
    }
180
181
    /**
182
     * __destruct
183
     */
184 100
    public function __destruct()
185
    {
186 100
        $this->free();
187 100
    }
188
189
    /**
190
     * Runs a user-provided callback with the MySQLi_Result object given as
191
     * argument and returns the result, or returns the MySQLi_Result object if
192
     * called without an argument.
193
     *
194
     * @param callable $callback User-provided callback (optional)
195
     *
196
     * @return \Doctrine\DBAL\Statement|mixed|\mysqli_result
197
     */
198 2
    public function __invoke(callable $callback = null)
199
    {
200 2
        if ($callback !== null) {
201 2
            return $callback($this->_result);
202
        }
203
204 1
        return $this->_result;
205
    }
206
207
    /**
208
     * Get the current "num_rows" as string.
209
     *
210
     * @return string
211
     */
212
    public function __toString()
213
    {
214
        return (string) $this->num_rows;
215
    }
216
217
    /**
218
     * Cast data into int, float or string.
219
     *
220
     * <p>
221
     *   <br />
222
     *   INFO: install / use "mysqlnd"-driver for better performance
223
     * </p>
224
     *
225
     * @param array|object $data
226
     *
227
     * @return array|false|object
228
     *                            <p><strong>false</strong> on error</p>
229
     */
230 68
    private function cast(&$data)
231
    {
232
        if (
233 68
            !$this->doctrinePdoStmt // pdo only have limited support for types, so we try to improve it
234
            &&
235 68
            Helper::isMysqlndIsUsed()
236
        ) {
237 68
            return $data;
238
        }
239
240
        // init
241
        static $FIELDS_CACHE = [];
242
        static $TYPES_CACHE = [];
243
244
        $result_hash = \spl_object_hash($this->_result);
245
246
        if (!isset($FIELDS_CACHE[$result_hash])) {
247
            $FIELDS_CACHE[$result_hash] = $this->fetch_fields();
248
        }
249
250
        if (
251
            !isset($FIELDS_CACHE[$result_hash])
252
            ||
253
            $FIELDS_CACHE[$result_hash] === false
254
        ) {
255
            return false;
256
        }
257
258
        if (!isset($TYPES_CACHE[$result_hash])) {
259
            foreach ($FIELDS_CACHE[$result_hash] as $field) {
260
                switch ($field->type) {
261
                    case self::MYSQL_TYPE_BIT:
262
                        $TYPES_CACHE[$result_hash][$field->name] = 'boolean';
263
264
                        break;
265
                    case self::MYSQL_TYPE_TINY:
266
                    case self::MYSQL_TYPE_SHORT:
267
                    case self::MYSQL_TYPE_LONG:
268
                    case self::MYSQL_TYPE_LONGLONG:
269
                    case self::MYSQL_TYPE_INT24:
270
                        $TYPES_CACHE[$result_hash][$field->name] = 'integer';
271
272
                        break;
273
                    case self::MYSQL_TYPE_DOUBLE:
274
                    case self::MYSQL_TYPE_DECIMAL:
275
                    case self::MYSQL_TYPE_NEWDECIMAL:
276
                    case self::MYSQL_TYPE_FLOAT:
277
                        $TYPES_CACHE[$result_hash][$field->name] = 'float';
278
279
                        break;
280
                    default:
281
                        $TYPES_CACHE[$result_hash][$field->name] = 'string';
282
283
                        break;
284
                }
285
            }
286
        }
287
288
        if (\is_array($data)) {
289 View Code Duplication
            foreach ($TYPES_CACHE[$result_hash] as $type_name => $type) {
290
                if (isset($data[$type_name])) {
291
                    \settype($data[$type_name], $type);
292
                }
293
            }
294
        } elseif (\is_object($data)) {
295 View Code Duplication
            foreach ($TYPES_CACHE[$result_hash] as $type_name => $type) {
296
                if (isset($data->{$type_name})) {
297
                    \settype($data->{$type_name}, $type);
298
                }
299
            }
300
        }
301
302
        return $data;
303
    }
304
305
    /**
306
     * Countable interface implementation.
307
     *
308
     * @return int The number of rows in the result
309
     */
310 2
    public function count(): int
311
    {
312 2
        return $this->num_rows;
313
    }
314
315
    /**
316
     * Iterator interface implementation.
317
     *
318
     * @return mixed The current element
319
     */
320 8
    public function current()
321
    {
322 8
        return $this->fetchCallable($this->current_row);
323
    }
324
325
    /**
326
     * Iterator interface implementation.
327
     *
328
     * @return int The current element key (row index; zero-based)
329
     */
330 1
    public function key(): int
331
    {
332 1
        return $this->current_row;
333
    }
334
335
    /**
336
     * Iterator interface implementation.
337
     *
338
     * @return void
339
     */
340 8
    public function next()
341
    {
342 8
        $this->current_row++;
343 8
    }
344
345
    /**
346
     * Iterator interface implementation.
347
     *
348
     * @param int $row Row position to rewind to; defaults to 0
349
     *
350
     * @return void
351
     */
352 12
    public function rewind($row = 0)
353
    {
354 12
        if ($this->seek($row)) {
355 10
            $this->current_row = $row;
356
        }
357 12
    }
358
359
    /**
360
     * Moves the internal pointer to the specified row position.
361
     *
362
     * @param int $row <p>Row position; zero-based and set to 0 by default</p>
363
     *
364
     * @return bool
365
     *              <p>true on success, false otherwise</p>
366
     */
367 20
    public function seek($row = 0): bool
368
    {
369 20
        if (\is_int($row) && $row >= 0 && $row < $this->num_rows) {
370
            if (
371 16
                $this->doctrineMySQLiStmt
372
                &&
373 16
                $this->doctrineMySQLiStmt instanceof \mysqli_stmt
374
            ) {
375
                $this->doctrineMySQLiStmt->data_seek($row);
376
377
                return true;
378
            }
379
380
            if (
381 16
                $this->doctrinePdoStmt
382
                &&
383 16
                $this->doctrinePdoStmt instanceof \Doctrine\DBAL\Driver\PDOStatement
384
            ) {
385
                return true;
386
            }
387
388 16
            if ($this->_result instanceof \mysqli_result) {
389 16
                return \mysqli_data_seek($this->_result, $row);
390
            }
391
        }
392
393 4
        return false;
394
    }
395
396
    /**
397
     * Iterator interface implementation.
398
     *
399
     * @return bool
400
     *              <p>true if the current index is valid, false otherwise</p>
401
     */
402 8
    public function valid(): bool
403
    {
404 8
        return $this->current_row < $this->num_rows;
405
    }
406
407
    /**
408
     * Fetch.
409
     *
410
     * <p>
411
     *   <br />
412
     *   INFO: this will return an object by default, not an array<br />
413
     *   and you can change the behaviour via "Result->setDefaultResultType()"
414
     * </p>
415
     *
416
     * @param bool $reset optional <p>Reset the \mysqli_result counter.</p>
417
     *
418
     * @return array|false|object
419
     *                            <p><strong>false</strong> on error</p>
420
     */
421 6
    public function fetch(bool $reset = false)
422
    {
423 6
        $return = false;
424
425 6
        if ($this->_default_result_type === self::RESULT_TYPE_OBJECT) {
426 6
            $return = $this->fetchObject('', null, $reset);
427 6
        } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAY) {
428 6
            $return = $this->fetchArray($reset);
429
        } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAYY) {
430
            $return = $this->fetchArrayy($reset);
431
        } elseif ($this->_default_result_type === self::RESULT_TYPE_YIELD) {
432
            $return = $this->fetchYield('', null, $reset);
433
        }
434
435 6
        return $return;
436
    }
437
438
    /**
439
     * Fetch all results.
440
     *
441
     * <p>
442
     *   <br />
443
     *   INFO: this will return an object by default, not an array<br />
444
     *   and you can change the behaviour via "Result->setDefaultResultType()"
445
     * </p>
446
     *
447
     * @return array
448
     */
449 6
    public function fetchAll(): array
450
    {
451 6
        $return = [];
452
453 6
        if ($this->_default_result_type === self::RESULT_TYPE_OBJECT) {
454 6
            $return = $this->fetchAllObject();
455 3
        } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAY) {
456 3
            $return = $this->fetchAllArray();
457
        } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAYY) {
458
            $return = $this->fetchAllArrayy();
459
        } elseif ($this->_default_result_type === self::RESULT_TYPE_YIELD) {
460
            $return = $this->fetchAllYield();
461
        }
462
463 6
        return $return;
464
    }
465
466
    /**
467
     * Fetch all results as array.
468
     *
469
     * @return array
470
     */
471 28
    public function fetchAllArray(): array
472
    {
473 28
        if ($this->is_empty()) {
474
            return [];
475
        }
476
477 28
        $this->reset();
478
479 28
        $data = [];
480
        /** @noinspection PhpAssignmentInConditionInspection */
481 28
        while ($row = $this->fetch_assoc()) {
482 28
            $data[] = $this->cast($row);
483
        }
484
485 28
        return $data;
486
    }
487
488
    /**
489
     * Fetch all results as "Arrayy"-object.
490
     *
491
     * @return Arrayy
492
     */
493 10
    public function fetchAllArrayy(): Arrayy
494
    {
495 10
        if ($this->is_empty()) {
496
            return Arrayy::create([]);
497
        }
498
499 10
        $this->reset();
500
501 10
        $data = [];
502
        /** @noinspection PhpAssignmentInConditionInspection */
503 10
        while ($row = $this->fetch_assoc()) {
504 10
            $data[] = $this->cast($row);
505
        }
506
507 10
        return Arrayy::create($data);
508
    }
509
510
    /**
511
     * Fetch all results as "Arrayy"-object.
512
     *
513
     * @return Arrayy
514
     */
515 1
    public function fetchAllArrayyYield(): Arrayy
516
    {
517 1
        if ($this->is_empty()) {
518
            return Arrayy::create([]);
519
        }
520
521 1
        $this->reset();
522
523 1
        return Arrayy::createFromGeneratorFunction(
524
            function () {
525
                /** @noinspection PhpAssignmentInConditionInspection */
526 1
                while ($row = $this->fetch_assoc()) {
527 1
                    yield $this->cast($row);
528
                }
529 1
            }
530
        );
531
    }
532
533
    /**
534
     * Fetch a single column as an 1-dimension array.
535
     *
536
     * @param string $column
537
     * @param bool   $skipNullValues <p>Skip "NULL"-values. | default: false</p>
538
     *
539
     * @return array
540
     *               <p>Return an empty array if the "$column" wasn't found</p>
541
     */
542 5
    public function fetchAllColumn(string $column, bool $skipNullValues = false): array
543
    {
544 5
        $return = $this->fetchColumn($column, $skipNullValues, true);
545
546 5
        \assert(\is_array($return));
547
548 5
        return $return;
549
    }
550
551
    /**
552
     * Fetch all results as array with objects.
553
     *
554
     * @param object|string $class  <p>
555
     *                              <strong>string</strong>: create a new object (with optional constructor
556
     *                              parameter)<br>
557
     *                              <strong>object</strong>: use a object and fill the the data into
558
     *                              </p>
559
     * @param array|null    $params optional
560
     *                              <p>
561
     *                              An array of parameters to pass to the constructor, used if $class is a
562
     *                              string.
563
     *                              </p>
564
     *
565
     * @return array
566
     */
567 9
    public function fetchAllObject($class = '', array $params = null): array
568
    {
569 9
        if ($this->is_empty()) {
570
            return [];
571
        }
572
573
        // fallback
574 9
        if (!$class || $class === 'stdClass') {
575 9
            $class = \stdClass::class;
576
        }
577
578
        // init
579 9
        $data = [];
580 9
        $this->reset();
581
582 9
        if (\is_object($class)) {
583
            $classTmpOrig = new $class();
584 9
        } elseif ($class && $params) {
585 3
            $reflectorTmp = new \ReflectionClass($class);
586 3
            $classTmpOrig = $reflectorTmp->newInstanceArgs($params);
587
        } else {
588 9
            $classTmpOrig = new $class();
589
        }
590
591 9
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
592
        /** @noinspection PhpAssignmentInConditionInspection */
593 9 View Code Duplication
        while ($row = $this->fetch_assoc()) {
594 9
            $classTmp = clone $classTmpOrig;
595 9
            $row = $this->cast($row);
596 9
            if ($row !== false) {
597 9
                foreach ($row as $key => $value) {
598 9
                    if ($class === \stdClass::class) {
599 9
                        $classTmp->{$key} = $value;
600
                    } else {
601 3
                        $propertyAccessor->setValue($classTmp, $key, $value);
602
                    }
603
                }
604
            }
605
606 9
            $data[] = $classTmp;
607
        }
608
609 9
        return $data;
610
    }
611
612
    /**
613
     * Fetch all results as "\Generator" via yield.
614
     *
615
     * @param object|string $class  <p>
616
     *                              <strong>string</strong>: create a new object (with optional constructor
617
     *                              parameter)<br>
618
     *                              <strong>object</strong>: use a object and fill the the data into
619
     *                              </p>
620
     * @param array|null    $params optional
621
     *                              <p>
622
     *                              An array of parameters to pass to the constructor, used if $class is a
623
     *                              string.
624
     *                              </p>
625
     *
626
     * @return \Generator
627
     */
628 8
    public function fetchAllYield($class = '', array $params = null): \Generator
629
    {
630 8
        if ($this->is_empty()) {
631
            return;
632
        }
633
634
        // init
635 8
        $this->reset();
636
637
        // fallback
638 8
        if (!$class || $class === 'stdClass') {
639 8
            $class = \stdClass::class;
640
        }
641
642 8
        if (\is_object($class)) {
643
            $classTmpOrig = $class;
644 8
        } elseif ($class && $params) {
645
            $reflectorTmp = new \ReflectionClass($class);
646
            $classTmpOrig = $reflectorTmp->newInstanceArgs($params);
647
        } else {
648 8
            $classTmpOrig = new $class();
649
        }
650
651 8
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
652
        /** @noinspection PhpAssignmentInConditionInspection */
653 8 View Code Duplication
        while ($row = $this->fetch_assoc()) {
654 8
            $classTmp = clone $classTmpOrig;
655
656 8
            $row = $this->cast($row);
657 8
            if ($row !== false) {
658 8
                foreach ($row as $key => $value) {
659 8
                    if ($class === \stdClass::class) {
660 8
                        $classTmp->{$key} = $value;
661
                    } else {
662 3
                        $propertyAccessor->setValue($classTmp, $key, $value);
663
                    }
664
                }
665
            }
666
667 8
            yield $classTmp;
668
        }
669 8
    }
670
671
    /**
672
     * Fetch as array.
673
     *
674
     * @param bool $reset
675
     *
676
     * @return array|false
677
     *                     <p><strong>false</strong> on error</p>
678
     */
679 30
    public function fetchArray(bool $reset = false)
680
    {
681 30
        if ($reset) {
682 3
            $this->reset();
683
        }
684
685 30
        $row = $this->fetch_assoc();
686 30
        if ($row) {
687 27
            $return = $this->cast($row);
688
689 27
            \assert(\is_array($return));
690
691 27
            return $return;
692
        }
693
694 5
        if ($row === null || $row === false) {
695 5
            return [];
696
        }
697
698
        return false;
699
    }
700
701
    /**
702
     * Fetch data as a key/value pair array.
703
     *
704
     * <p>
705
     *   <br />
706
     *   INFO: both "key" and "value" must exists in the fetched data
707
     *   the key will be the new key of the result-array
708
     *   <br /><br />
709
     * </p>
710
     *
711
     * e.g.:
712
     * <code>
713
     *    fetchArrayPair('some_id', 'some_value');
714
     *    // array(127 => 'some value', 128 => 'some other value')
715
     * </code>
716
     *
717
     * @param string $key
718
     * @param string $value
719
     *
720
     * @return array
721
     */
722 3
    public function fetchArrayPair(string $key, string $value): array
723
    {
724 3
        $arrayPair = [];
725 3
        $data = $this->fetchAllArray();
726
727 3
        foreach ($data as &$_row) {
728
            if (
729 3
                \array_key_exists($key, $_row)
730
                &&
731 3
                \array_key_exists($value, $_row)
732
            ) {
733 3
                $_key = $_row[$key];
734 3
                $_value = $_row[$value];
735 3
                $arrayPair[$_key] = $_value;
736
            }
737
        }
738
739 3
        return $arrayPair;
740
    }
741
742
    /**
743
     * Fetch as "Arrayy"-object.
744
     *
745
     * @param bool $reset optional <p>Reset the \mysqli_result counter.</p>
746
     *
747
     * @return Arrayy|false
748
     *                      <p><strong>false</strong> on error</p>
749
     */
750 6
    public function fetchArrayy(bool $reset = false)
751
    {
752 6
        if ($reset) {
753
            $this->reset();
754
        }
755
756 6
        $row = $this->fetch_assoc();
757 6
        if ($row) {
758 3
            return Arrayy::create($this->cast($row));
759
        }
760
761 3
        if ($row === null || $row === false) {
762 3
            return Arrayy::create();
763
        }
764
765
        return false;
766
    }
767
768
    /**
769
     * Fetches a row or a single column within a row. Returns null if there are
770
     * no more rows in the result.
771
     *
772
     * @param int    $row    The row number (optional)
773
     * @param string $column The column name (optional)
774
     *
775
     * @return mixed An associative array or a scalar value
776
     */
777 17
    public function fetchCallable(int $row = null, string $column = null)
778
    {
779 17
        if (!$this->num_rows) {
780 2
            return null;
781
        }
782
783 15
        if ($row !== null) {
784 14
            $this->seek($row);
785
        }
786
787 15
        $rows = $this->fetch_assoc();
788
789 15
        if ($column) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $column of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
790
            if (
791 5
                \is_array($rows)
792
                &&
793 5
                isset($rows[$column])
794
            ) {
795 5
                return $rows[$column];
796
            }
797
798
            return null;
799
        }
800
801 14
        if (\is_callable($this->_mapper)) {
802 1
            return \call_user_func($this->_mapper, $rows);
803
        }
804
805 14
        return $rows;
806
    }
807
808
    /**
809
     * Fetch a single column as string (or as 1-dimension array).
810
     *
811
     * @param string $column
812
     * @param bool   $skipNullValues <p>Skip "NULL"-values. | default: true</p>
813
     * @param bool   $asArray        <p>Get all values and not only the last one. | default: false</p>
814
     *
815
     * @return array|string
816
     *                      <p>Return a empty string or an empty array if the "$column" wasn't found, depend on
817
     *                      "$asArray"</p>
818
     */
819 9
    public function fetchColumn(
820
        string $column = '',
821
        bool $skipNullValues = true,
822
        bool $asArray = false
823
    ) {
824 9
        if (!$asArray) {
825 7
            $columnData = '';
826
827 7
            $data = $this->fetchAllArrayy()->reverse()->getArray();
828 7 View Code Duplication
            foreach ($data as $_row) {
829 7
                if ($skipNullValues) {
830 7
                    if (!isset($_row[$column])) {
831 7
                        continue;
832
                    }
833 3
                } elseif (!\array_key_exists($column, $_row)) {
834 3
                    break;
835
                }
836
837 7
                $columnData = $_row[$column];
838
839 7
                break;
840
            }
841
842 7
            return $columnData;
843
        }
844
845
        // -- return as array -->
846
847 5
        $columnData = [];
848
849 5 View Code Duplication
        foreach ($this->fetchAllYield() as $_row) {
850 5
            if ($skipNullValues) {
851 3
                if (!isset($_row->{$column})) {
852 3
                    continue;
853
                }
854 5
            } elseif (!\array_key_exists($column, $_row)) {
855 3
                break;
856
            }
857
858 5
            $columnData[] = $_row->{$column};
859
        }
860
861 5
        return $columnData;
862
    }
863
864
    /**
865
     * Return rows of field information in a result set.
866
     *
867
     * @param bool $as_array Return each field info as array; defaults to false
868
     *
869
     * @return array
870
     *               <p>Array of field information each as an associative array.</p>
871
     */
872 1
    public function fetchFields(bool $as_array = false): array
873
    {
874 1
        $fields = $this->fetch_fields();
875 1
        if ($fields === false) {
876
            return [];
877
        }
878
879 1
        if ($as_array) {
880 1
            return \array_map(
881
                static function ($object) {
882 1
                    return (array) $object;
883 1
                },
884 1
                $fields
885
            );
886
        }
887
888
        return $fields;
889
    }
890
891
    /**
892
     * Returns all rows at once as a grouped array of scalar values or arrays.
893
     *
894
     * @param string $group  The column name to use for grouping
895
     * @param string $column The column name to use as values (optional)
896
     *
897
     * @return array
898
     *               <p>A grouped array of scalar values or arrays.</p>
899
     */
900 1 View Code Duplication
    public function fetchGroups(string $group, string $column = null): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
901
    {
902
        // init
903 1
        $groups = [];
904 1
        $pos = $this->current_row;
905
906 1
        foreach ($this as $row) {
907 1
            if (!\array_key_exists($group, $row)) {
908
                continue;
909
            }
910
911 1
            if ($column !== null) {
912 1
                if (!\array_key_exists($column, $row)) {
913
                    continue;
914
                }
915
916 1
                $groups[$row[$group]][] = $row[$column];
917
            } else {
918 1
                $groups[$row[$group]][] = $row;
919
            }
920
        }
921
922 1
        $this->rewind($pos);
923
924 1
        return $groups;
925
    }
926
927
    /**
928
     * Fetch as object.
929
     *
930
     * @param object|string $class  <p>
931
     *                              <strong>string</strong>: create a new object (with optional constructor
932
     *                              parameter)<br>
933
     *                              <strong>object</strong>: use a object and fill the the data into
934
     *                              </p>
935
     * @param array|null    $params optional
936
     *                              <p>
937
     *                              An array of parameters to pass to the constructor, used if $class is a
938
     *                              string.
939
     *                              </p>
940
     * @param bool          $reset  optional <p>Reset the \mysqli_result counter.</p>
941
     *
942
     * @return false|object
943
     *                      <p><strong>false</strong> on error</p>
944
     */
945 21 View Code Duplication
    public function fetchObject($class = '', array $params = null, bool $reset = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
946
    {
947 21
        if ($reset) {
948 9
            $this->reset();
949
        }
950
951
        // fallback
952 21
        if (!$class || $class === 'stdClass') {
953 21
            $class = \stdClass::class;
954
        }
955
956 21
        $row = $this->fetch_assoc();
957 21
        $row = $row ? $this->cast($row) : false;
958
959 21
        if (!$row) {
960 3
            return false;
961
        }
962
963 21
        if (\is_object($class)) {
964 3
            $classTmp = $class;
965 21
        } elseif ($class && $params) {
966 3
            $reflectorTmp = new \ReflectionClass($class);
967 3
            $classTmp = $reflectorTmp->newInstanceArgs($params);
968
        } else {
969 21
            $classTmp = new $class();
970
        }
971
972 21
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
973 21
        foreach ($row as $key => $value) {
974 21
            if ($class === \stdClass::class) {
975 21
                $classTmp->{$key} = $value;
976
            } else {
977 3
                $propertyAccessor->setValue($classTmp, $key, $value);
978
            }
979
        }
980
981 21
        return $classTmp;
982
    }
983
984
    /**
985
     * Returns all rows at once as key-value pairs.
986
     *
987
     * @param string $key    The column name to use as keys
988
     * @param string $column The column name to use as values (optional)
989
     *
990
     * @return array
991
     *               <p>An array of key-value pairs.</p>
992
     */
993 1 View Code Duplication
    public function fetchPairs(string $key, string $column = null): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
994
    {
995
        // init
996 1
        $pairs = [];
997 1
        $pos = $this->current_row;
998
999 1
        foreach ($this as $row) {
1000 1
            if (!\array_key_exists($key, $row)) {
1001
                continue;
1002
            }
1003
1004 1
            if ($column !== null) {
1005 1
                if (!\array_key_exists($column, $row)) {
1006
                    continue;
1007
                }
1008
1009 1
                $pairs[$row[$key]] = $row[$column];
1010
            } else {
1011 1
                $pairs[$row[$key]] = $row;
1012
            }
1013
        }
1014
1015 1
        $this->rewind($pos);
1016
1017 1
        return $pairs;
1018
    }
1019
1020
    /**
1021
     * Returns all rows at once, transposed as an array of arrays. Instead of
1022
     * returning rows of columns, this method returns columns of rows.
1023
     *
1024
     * @param string $column The column name to use as keys (optional)
1025
     *
1026
     * @return array
1027
     *               <p>A transposed array of arrays</p>
1028
     */
1029 1
    public function fetchTranspose(string $column = null)
1030
    {
1031
        // init
1032 1
        $keys = $column !== null ? $this->fetchAllColumn($column) : [];
1033 1
        $rows = [];
1034 1
        $pos = $this->current_row;
1035
1036 1
        foreach ($this as $row) {
1037 1
            foreach ($row as $key => $value) {
1038 1
                $rows[$key][] = $value;
1039
            }
1040
        }
1041
1042 1
        $this->rewind($pos);
1043
1044 1
        if (empty($keys)) {
1045 1
            return $rows;
1046
        }
1047
1048 1
        return \array_map(
1049
            static function ($values) use ($keys) {
1050 1
                return \array_combine($keys, $values);
1051 1
            },
1052 1
            $rows
1053
        );
1054
    }
1055
1056
    /**
1057
     * Fetch as "\Generator" via yield.
1058
     *
1059
     * @param object|string $class  <p>
1060
     *                              <strong>string</strong>: create a new object (with optional constructor
1061
     *                              parameter)<br>
1062
     *                              <strong>object</strong>: use a object and fill the the data into
1063
     *                              </p>
1064
     * @param array|null    $params optional
1065
     *                              <p>
1066
     *                              An array of parameters to pass to the constructor, used if $class is a
1067
     *                              string.
1068
     *                              </p>
1069
     * @param bool          $reset  optional <p>Reset the \mysqli_result counter.</p>
1070
     *
1071
     * @return \Generator
1072
     */
1073 4 View Code Duplication
    public function fetchYield($class = '', array $params = null, bool $reset = false): \Generator
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1074
    {
1075 4
        if ($reset) {
1076
            $this->reset();
1077
        }
1078
1079
        // fallback
1080 4
        if (!$class || $class === 'stdClass') {
1081 3
            $class = \stdClass::class;
1082
        }
1083
1084 4
        if (\is_object($class)) {
1085
            $classTmp = $class;
1086 4
        } elseif ($class && $params) {
1087
            $reflectorTmp = new \ReflectionClass($class);
1088
            $classTmp = $reflectorTmp->newInstanceArgs($params);
1089
        } else {
1090 4
            $classTmp = new $class();
1091
        }
1092
1093 4
        $row = $this->fetch_assoc();
1094 4
        $row = $row ? $this->cast($row) : false;
1095
1096 4
        if (!$row) {
1097
            return;
1098
        }
1099
1100 4
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
1101 4
        foreach ($row as $key => $value) {
1102 4
            if ($class === \stdClass::class) {
1103 3
                $classTmp->{$key} = $value;
1104
            } else {
1105 1
                $propertyAccessor->setValue($classTmp, $key, $value);
1106
            }
1107
        }
1108
1109 4
        yield $classTmp;
1110 4
    }
1111
1112
    /**
1113
     * @return mixed|null
1114
     */
1115 78
    private function fetch_assoc()
1116
    {
1117 78
        if ($this->_result instanceof \Doctrine\DBAL\Statement) {
1118
            if (
1119
                $this->doctrinePdoStmt
1120
                &&
1121
                $this->doctrinePdoStmt instanceof \Doctrine\DBAL\Driver\PDOStatement
1122
            ) {
1123
                if ($this->doctrinePdoStmtDataSeekInit === false) {
1124
                    $this->doctrinePdoStmtDataSeekInit = true;
1125
1126
                    $this->doctrinePdoStmtDataSeekFakeCache = $this->_result->fetchAll(\Doctrine\DBAL\FetchMode::ASSOCIATIVE);
1127
                }
1128
1129
                $return = ($this->doctrinePdoStmtDataSeekFakeCache[$this->doctrinePdoStmtDataSeekFake] ?? null);
1130
1131
                $this->doctrinePdoStmtDataSeekFake++;
1132
1133
                return $return;
1134
            }
1135
1136
            if (
1137
                $this->doctrineMySQLiStmt
1138
                &&
1139
                $this->doctrineMySQLiStmt instanceof \mysqli_stmt
1140
            ) {
1141
                return $this->_result->fetch(
1142
                    \Doctrine\DBAL\FetchMode::ASSOCIATIVE,
1143
                    0 // FETCH_ORI_NEXT
1144
                );
1145
            }
1146
1147
            return null;
1148
        }
1149
1150 78
        return \mysqli_fetch_assoc($this->_result);
1151
    }
1152
1153
    /**
1154
     * @return array|false
1155
     */
1156 1
    private function fetch_fields()
1157
    {
1158 1
        if ($this->_result instanceof \mysqli_result) {
1159 1
            return \mysqli_fetch_fields($this->_result);
1160
        }
1161
1162
        if ($this->doctrineMySQLiStmt) {
1163
            $metadataTmp = $this->doctrineMySQLiStmt->result_metadata();
1164
            if ($metadataTmp === false) {
1165
                return [];
1166
            }
1167
1168
            return $metadataTmp->fetch_fields();
1169
        }
1170
1171
        if ($this->doctrinePdoStmt) {
1172
            $fields = [];
1173
1174
            static $THIS_CLASS_TMP = null;
1175
            if ($THIS_CLASS_TMP === null) {
1176
                $THIS_CLASS_TMP = new \ReflectionClass(__CLASS__);
1177
            }
1178
1179
            $totalColumnsTmp = $this->doctrinePdoStmt->columnCount();
1180
            for ($counterTmp = 0; $counterTmp < $totalColumnsTmp; $counterTmp++) {
1181
                $metadataTmp = $this->doctrinePdoStmt->getColumnMeta($counterTmp);
1182
                $fieldTmp = new \stdClass();
1183
                foreach ($metadataTmp as $metadataTmpKey => $metadataTmpValue) {
1184
                    $fieldTmp->{$metadataTmpKey} = $metadataTmpValue;
1185
                }
1186
1187
                $typeNativeTmp = 'MYSQL_TYPE_' . $metadataTmp['native_type'];
1188
                $typeTmp = $THIS_CLASS_TMP->getConstant($typeNativeTmp);
1189
                if ($typeTmp) {
1190
                    $fieldTmp->type = $typeTmp;
1191
                } else {
1192
                    $fieldTmp->type = '';
1193
                }
1194
1195
                $fields[] = $fieldTmp;
1196
            }
1197
1198
            return $fields;
1199
        }
1200
1201
        return false;
1202
    }
1203
1204
    /**
1205
     * Returns the first row element from the result.
1206
     *
1207
     * @param string $column The column name to use as value (optional)
1208
     *
1209
     * @return mixed
1210
     *               <p>A row array or a single scalar value</p>
1211
     */
1212 3
    public function first(string $column = null)
1213
    {
1214 3
        $pos = $this->current_row;
1215 3
        $first = $this->fetchCallable(0, $column);
1216 3
        $this->rewind($pos);
1217
1218 3
        return $first;
1219
    }
1220
1221
    /**
1222
     * free the memory
1223
     */
1224 100
    public function free()
1225
    {
1226 100
        if ($this->_result instanceof \mysqli_result) {
1227
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
1228 100
            @\mysqli_free_result($this->_result);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1229
1230 100
            return true;
1231
        }
1232
1233
        if (
1234
            $this->doctrineMySQLiStmt
1235
            &&
1236
            $this->doctrineMySQLiStmt instanceof \mysqli_stmt
1237
        ) {
1238
            $this->doctrineMySQLiStmt->free_result();
1239
1240
            return true;
1241
        }
1242
1243
        return false;
1244
    }
1245
1246
    /**
1247
     * alias for "Result->fetch()"
1248
     *
1249
     * @return array|false|object
1250
     *                            <p><strong>false</strong> on error</p>
1251
     *
1252
     * @see Result::fetch()
1253
     */
1254 3
    public function get()
1255
    {
1256 3
        return $this->fetch();
1257
    }
1258
1259
    /**
1260
     * alias for "Result->fetchAll()"
1261
     *
1262
     * @return array
1263
     *
1264
     * @see Result::fetchAll()
1265
     */
1266 3
    public function getAll(): array
1267
    {
1268 3
        return $this->fetchAll();
1269
    }
1270
1271
    /**
1272
     * alias for "Result->fetchAllColumn()"
1273
     *
1274
     * @param string $column
1275
     * @param bool   $skipNullValues
1276
     *
1277
     * @return array
1278
     *
1279
     * @see Result::fetchAllColumn()
1280
     */
1281
    public function getAllColumn(string $column, bool $skipNullValues = false): array
1282
    {
1283
        return $this->fetchAllColumn($column, $skipNullValues);
1284
    }
1285
1286
    /**
1287
     * alias for "Result->fetchAllArray()"
1288
     *
1289
     * @return array
1290
     *
1291
     * @see Result::fetchAllArray()
1292
     */
1293 3
    public function getArray(): array
1294
    {
1295 3
        return $this->fetchAllArray();
1296
    }
1297
1298
    /**
1299
     * alias for "Result->fetchAllArrayy()"
1300
     *
1301
     * @return Arrayy
1302
     *
1303
     * @see Result::fetchAllArrayy()
1304
     */
1305
    public function getArrayy(): Arrayy
1306
    {
1307
        return $this->fetchAllArrayy();
1308
    }
1309
1310
    /**
1311
     * alias for "Result->fetchColumn()"
1312
     *
1313
     * @param string $column
1314
     * @param bool   $asArray
1315
     * @param bool   $skipNullValues
1316
     *
1317
     * @return array|string
1318
     *                      <p>Return a empty string or an empty array if the "$column" wasn't found, depend on
1319
     *                      "$asArray"</p>
1320
     *
1321
     * @see Result::fetchColumn()
1322
     */
1323 3
    public function getColumn(
1324
        string $column,
1325
        bool $skipNullValues = true,
1326
        bool $asArray = false
1327
    ) {
1328 3
        return $this->fetchColumn($column, $skipNullValues, $asArray);
1329
    }
1330
1331
    /**
1332
     * @return string
1333
     */
1334 3
    public function getDefaultResultType(): string
1335
    {
1336 3
        return $this->_default_result_type;
1337
    }
1338
1339
    /**
1340
     * alias for "Result->fetchAllObject()"
1341
     *
1342
     * @return array of mysql-objects
1343
     *
1344
     * @see Result::fetchAllObject()
1345
     */
1346 3
    public function getObject(): array
1347
    {
1348 3
        return $this->fetchAllObject();
1349
    }
1350
1351
    /**
1352
     * alias for "Result->fetchAllYield()"
1353
     *
1354
     * @return \Generator
1355
     *
1356
     * @see Result::fetchAllYield()
1357
     */
1358 1
    public function getYield(): \Generator
1359
    {
1360 1
        return $this->fetchAllYield();
1361
    }
1362
1363
    /**
1364
     * Check if the result is empty.
1365
     *
1366
     * @return bool
1367
     */
1368 41
    public function is_empty(): bool
1369
    {
1370 41
        return !($this->num_rows > 0);
1371
    }
1372
1373
    /**
1374
     * Fetch all results as "json"-string.
1375
     *
1376
     * @return false|string
1377
     */
1378 3
    public function json()
1379
    {
1380 3
        $data = $this->fetchAllArray();
1381
1382 3
        return UTF8::json_encode($data);
1383
    }
1384
1385
    /**
1386
     * Returns the last row element from the result.
1387
     *
1388
     * @param string $column The column name to use as value (optional)
1389
     *
1390
     * @return mixed A row array or a single scalar value
1391
     */
1392 3
    public function last(string $column = null)
1393
    {
1394 3
        $pos = $this->current_row;
1395 3
        $last = $this->fetchCallable($this->num_rows - 1, $column);
1396 3
        $this->rewind($pos);
1397
1398 3
        return $last;
1399
    }
1400
1401
    /**
1402
     * Set the mapper...
1403
     *
1404
     * @param \Closure $callable
1405
     *
1406
     * @return $this
1407
     */
1408 1
    public function map(\Closure $callable): self
1409
    {
1410 1
        $this->_mapper = $callable;
1411
1412 1
        return $this;
1413
    }
1414
1415
    /**
1416
     * Alias of count(). Deprecated.
1417
     *
1418
     * @return int
1419
     *             <p>The number of rows in the result.</p>
1420
     */
1421 1
    public function num_rows(): int
1422
    {
1423 1
        return $this->count();
1424
    }
1425
1426
    /**
1427
     * ArrayAccess interface implementation.
1428
     *
1429
     * @param int $offset <p>Offset number</p>
1430
     *
1431
     * @return bool
1432
     *              <p>true if offset exists, false otherwise.</p>
1433
     */
1434 1
    public function offsetExists($offset): bool
1435
    {
1436 1
        return \is_int($offset) && $offset >= 0 && $offset < $this->num_rows;
1437
    }
1438
1439
    /**
1440
     * ArrayAccess interface implementation.
1441
     *
1442
     * @param int $offset Offset number
1443
     *
1444
     * @return mixed
1445
     */
1446 1
    public function offsetGet($offset)
1447
    {
1448 1
        if ($this->offsetExists($offset)) {
1449 1
            return $this->fetchCallable($offset);
1450
        }
1451
1452
        throw new \OutOfBoundsException("undefined offset (${offset})");
1453
    }
1454
1455
    /**
1456
     * ArrayAccess interface implementation. Not implemented by design.
1457
     *
1458
     * @param mixed $offset
1459
     * @param mixed $value
1460
     */
1461
    public function offsetSet($offset, $value)
1462
    {
1463
        /** @noinspection UselessReturnInspection */
1464
    }
1465
1466
    /**
1467
     * ArrayAccess interface implementation. Not implemented by design.
1468
     *
1469
     * @param mixed $offset
1470
     */
1471
    public function offsetUnset($offset)
1472
    {
1473
        /** @noinspection UselessReturnInspection */
1474
    }
1475
1476
    /**
1477
     * Reset the offset (data_seek) for the results.
1478
     *
1479
     * @return Result
1480
     */
1481 38
    public function reset(): self
1482
    {
1483 38
        $this->doctrinePdoStmtDataSeekFake = 0;
1484
1485 38
        if (!$this->is_empty()) {
1486 38
            if ($this->doctrineMySQLiStmt instanceof \mysqli_stmt) {
1487
                $this->doctrineMySQLiStmt->data_seek(0);
1488
            }
1489
1490 38
            if ($this->_result instanceof \mysqli_result) {
1491 38
                \mysqli_data_seek($this->_result, 0);
1492
            }
1493
        }
1494
1495 38
        return $this;
1496
    }
1497
1498
    /**
1499
     * You can set the default result-type to Result::RESULT_TYPE_*.
1500
     *
1501
     * INFO: used for "fetch()" and "fetchAll()"
1502
     *
1503
     * @param string $default_result_type
1504
     */
1505 6
    public function setDefaultResultType(string $default_result_type = self::RESULT_TYPE_OBJECT)
1506
    {
1507
        if (
1508 6
            $default_result_type === self::RESULT_TYPE_OBJECT
1509
            ||
1510 6
            $default_result_type === self::RESULT_TYPE_ARRAY
1511
            ||
1512
            $default_result_type === self::RESULT_TYPE_ARRAYY
1513
            ||
1514 6
            $default_result_type === self::RESULT_TYPE_YIELD
1515
        ) {
1516 6
            $this->_default_result_type = $default_result_type;
1517
        }
1518 6
    }
1519
1520
    /**
1521
     * @param int      $offset
1522
     * @param int|null $length
1523
     * @param bool     $preserve_keys
1524
     *
1525
     * @return array
1526
     */
1527 1
    public function slice(int $offset = 0, int $length = null, bool $preserve_keys = false): array
1528
    {
1529
        // init
1530 1
        $slice = [];
1531
1532 1
        if ($offset < 0) {
1533 1
            if (\abs($offset) > $this->num_rows) {
1534 1
                $offset = 0;
1535
            } else {
1536 1
                $offset = $this->num_rows - (int) \abs($offset);
1537
            }
1538
        }
1539
1540 1
        $length = $length !== null ? (int) $length : $this->num_rows;
1541 1
        $n = 0;
1542 1
        for ($i = $offset; $i < $this->num_rows && $n < $length; $i++) {
1543 1
            if ($preserve_keys) {
1544 1
                $slice[$i] = $this->fetchCallable($i);
1545
            } else {
1546 1
                $slice[] = $this->fetchCallable($i);
1547
            }
1548 1
            ++$n;
1549
        }
1550
1551 1
        return $slice;
1552
    }
1553
}
1554