Completed
Push — master ( 39d9a4...0a738c )
by Lars
03:51
created

Result::fetchTranspose()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.4742

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 11
cts 15
cp 0.7332
rs 9.1928
c 0
b 0
f 0
cc 5
nc 12
nop 1
crap 5.4742
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
            $this->num_rows = (int) $this->_result->num_rows;
174
        }
175
176
        $this->current_row = 0;
177
178
        $this->_mapper = $mapper;
179
    }
180
181 101
    /**
182
     * __destruct
183
     */
184 101
    public function __destruct()
185
    {
186 101
        $this->free();
187 101
    }
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 100
     * called without an argument.
193
     *
194 100
     * @param callable $callback User-provided callback (optional)
195 100
     *
196
     * @return \Doctrine\DBAL\Statement|mixed|\mysqli_result
197
     */
198
    public function __invoke(callable $callback = null)
199
    {
200
        if ($callback !== null) {
201
            return $callback($this->_result);
202
        }
203
204
        return $this->_result;
205
    }
206 2
207
    /**
208 2
     * Get the current "num_rows" as string.
209 2
     *
210
     * @return string
211
     */
212 1
    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
    private function cast(&$data)
231
    {
232
        if (
233
            !$this->doctrinePdoStmt // pdo only have limited support for types, so we try to improve it
234
            &&
235
            Helper::isMysqlndIsUsed()
236
        ) {
237 68
            return $data;
238
        }
239
240 68
        // init
241
        static $FIELDS_CACHE = [];
242 68
        static $TYPES_CACHE = [];
243
244 68
        $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
    public function count(): int
311
    {
312
        return $this->num_rows;
313
    }
314
315
    /**
316
     * Iterator interface implementation.
317 2
     *
318
     * @return mixed The current element
319 2
     */
320
    public function current()
321
    {
322
        return $this->fetchCallable($this->current_row);
323
    }
324
325
    /**
326
     * Iterator interface implementation.
327 8
     *
328
     * @return int The current element key (row index; zero-based)
329 8
     */
330
    public function key(): int
331
    {
332
        return $this->current_row;
333
    }
334
335
    /**
336
     * Iterator interface implementation.
337 1
     *
338
     * @return void
339 1
     */
340
    public function next()
341
    {
342
        $this->current_row++;
343
    }
344
345
    /**
346
     * Iterator interface implementation.
347 8
     *
348
     * @param int $row Row position to rewind to; defaults to 0
349 8
     *
350 8
     * @return void
351
     */
352
    public function rewind($row = 0)
353
    {
354
        if ($this->seek($row)) {
355
            $this->current_row = $row;
356
        }
357
    }
358
359 12
    /**
360
     * Moves the internal pointer to the specified row position.
361 12
     *
362 10
     * @param int $row <p>Row position; zero-based and set to 0 by default</p>
363
     *
364 12
     * @return bool
365
     *              <p>true on success, false otherwise</p>
366
     */
367
    public function seek($row = 0): bool
368
    {
369
        if (\is_int($row) && $row >= 0 && $row < $this->num_rows) {
370
            if (
371
                $this->doctrineMySQLiStmt
372
                &&
373 20
                $this->doctrineMySQLiStmt instanceof \mysqli_stmt
374
            ) {
375 20
                $this->doctrineMySQLiStmt->data_seek($row);
376
377 16
                return true;
378
            }
379 16
380
            if (
381
                $this->doctrinePdoStmt
382
                &&
383
                $this->doctrinePdoStmt instanceof \Doctrine\DBAL\Driver\PDOStatement
384
            ) {
385
                return true;
386
            }
387 16
388
            if ($this->_result instanceof \mysqli_result) {
389 16
                return \mysqli_data_seek($this->_result, $row);
390
            }
391
        }
392
393
        return false;
394 16
    }
395
396
    /**
397 4
     * Iterator interface implementation.
398
     *
399
     * @return bool
400
     *              <p>true if the current index is valid, false otherwise</p>
401
     */
402
    public function valid(): bool
403
    {
404
        return $this->current_row < $this->num_rows;
405 8
    }
406
407 8
    /**
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
    public function fetch(bool $reset = false)
422
    {
423 6
        $return = false;
424
425 6
        if ($this->_default_result_type === self::RESULT_TYPE_OBJECT) {
426
            $return = $this->fetchObject('', null, $reset);
427 6
        } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAY) {
428 6
            $return = $this->fetchArray($reset);
429 6
        } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAYY) {
430 6
            $return = $this->fetchArrayy($reset);
431
        } elseif ($this->_default_result_type === self::RESULT_TYPE_YIELD) {
432
            $return = $this->fetchYield('', null, $reset);
433
        }
434
435
        return $return;
436
    }
437 6
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
    public function fetchAll(): array
450
    {
451 6
        $return = [];
452
453 6
        if ($this->_default_result_type === self::RESULT_TYPE_OBJECT) {
454
            $return = $this->fetchAllObject();
455 6
        } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAY) {
456 6
            $return = $this->fetchAllArray();
457 3
        } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAYY) {
458 3
            $return = $this->fetchAllArrayy();
459
        } elseif ($this->_default_result_type === self::RESULT_TYPE_YIELD) {
460
            $return = $this->fetchAllYield();
461
        }
462
463
        return $return;
464
    }
465 6
466
    /**
467
     * Fetch all results as array.
468
     *
469
     * @return array
470
     */
471
    public function fetchAllArray(): array
472
    {
473 28
        if ($this->is_empty()) {
474
            return [];
475 28
        }
476
477
        $this->reset();
478
479 28
        $data = [];
480
        /** @noinspection PhpAssignmentInConditionInspection */
481 28
        while ($row = $this->fetch_assoc()) {
482
            $data[] = $this->cast($row);
483 28
        }
484 28
485
        return $data;
486
    }
487 28
488
    /**
489
     * Fetch all results as "Arrayy"-object.
490
     *
491
     * @return Arrayy
492
     */
493
    public function fetchAllArrayy(): Arrayy
494
    {
495 10
        if ($this->is_empty()) {
496
            return Arrayy::create([]);
497 10
        }
498
499
        $this->reset();
500
501 10
        $data = [];
502
        /** @noinspection PhpAssignmentInConditionInspection */
503 10
        while ($row = $this->fetch_assoc()) {
504
            $data[] = $this->cast($row);
505 10
        }
506 10
507
        return Arrayy::create($data);
508
    }
509 10
510
    /**
511
     * Fetch all results as "Arrayy"-object.
512
     *
513
     * @return Arrayy
514
     */
515
    public function fetchAllArrayyYield(): Arrayy
516
    {
517
        if ($this->is_empty()) {
518
            return Arrayy::create([]);
519
        }
520 5
521
        $this->reset();
522 5
523
        return Arrayy::createFromGeneratorFunction(
524
            function () {
525
                /** @noinspection PhpAssignmentInConditionInspection */
526
                while ($row = $this->fetch_assoc()) {
527
                    yield $this->cast($row);
528
                }
529
            }
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 9
     */
542
    public function fetchAllColumn(string $column, bool $skipNullValues = false): array
543 9
    {
544
        $return = $this->fetchColumn($column, $skipNullValues, true);
545
546
        \assert(\is_array($return));
547
548 9
        return $return;
549
    }
550 9
551
    /**
552
     * Fetch all results as array with objects.
553
     *
554 9
     * @param object|string $class  <p>
555 9
     *                              <strong>string</strong>: create a new object (with optional constructor
556
     *                              parameter)<br>
557 9
     *                              <strong>object</strong>: use a object and fill the the data into
558
     *                              </p>
559 9
     * @param array|null    $params optional
560 3
     *                              <p>
561 3
     *                              An array of parameters to pass to the constructor, used if $class is a
562
     *                              string.
563 9
     *                              </p>
564
     *
565
     * @return array
566 9
     */
567
    public function fetchAllObject($class = '', array $params = null): array
568 9
    {
569 9
        if ($this->is_empty()) {
570 9
            return [];
571 9
        }
572 9
573 9
        // fallback
574
        if (!$class || $class === 'stdClass') {
575 9
            $class = \stdClass::class;
576
        }
577
578 9
        // init
579
        $data = [];
580
        $this->reset();
581 9
582
        if (\is_object($class)) {
583
            $classTmpOrig = new $class();
584
        } elseif ($class && $params) {
585
            $reflectorTmp = new \ReflectionClass($class);
586
            $classTmpOrig = $reflectorTmp->newInstanceArgs($params);
587
        } else {
588
            $classTmpOrig = new $class();
589
        }
590
591
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
592
        /** @noinspection PhpAssignmentInConditionInspection */
593 View Code Duplication
        while ($row = $this->fetch_assoc()) {
594
            $classTmp = clone $classTmpOrig;
595
            $row = $this->cast($row);
596
            if ($row !== false) {
597
                foreach ($row as $key => $value) {
598
                    if ($class === \stdClass::class) {
599
                        $classTmp->{$key} = $value;
600 8
                    } else {
601
                        $propertyAccessor->setValue($classTmp, $key, $value);
602 8
                    }
603
                }
604
            }
605
606
            $data[] = $classTmp;
607 8
        }
608
609
        return $data;
610 8
    }
611
612 8
    /**
613
     * Fetch all results as "\Generator" via yield.
614
     *
615 8
     * @param object|string $class  <p>
616
     *                              <strong>string</strong>: create a new object (with optional constructor
617 8
     *                              parameter)<br>
618
     *                              <strong>object</strong>: use a object and fill the the data into
619
     *                              </p>
620
     * @param array|null    $params optional
621 8
     *                              <p>
622
     *                              An array of parameters to pass to the constructor, used if $class is a
623
     *                              string.
624 8
     *                              </p>
625
     *
626 8
     * @return \Generator
627 8
     */
628 8
    public function fetchAllYield($class = '', array $params = null): \Generator
629 8
    {
630 8
        if ($this->is_empty()) {
631 8
            return;
632
        }
633 8
634
        // init
635
        $this->reset();
636
637 8
        // fallback
638
        if (!$class || $class === 'stdClass') {
639 8
            $class = \stdClass::class;
640
        }
641
642
        if (\is_object($class)) {
643
            $classTmpOrig = $class;
644
        } elseif ($class && $params) {
645
            $reflectorTmp = new \ReflectionClass($class);
646
            $classTmpOrig = $reflectorTmp->newInstanceArgs($params);
647
        } else {
648 30
            $classTmpOrig = new $class();
649
        }
650 30
651 3
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
652
        /** @noinspection PhpAssignmentInConditionInspection */
653 View Code Duplication
        while ($row = $this->fetch_assoc()) {
654 30
            $classTmp = clone $classTmpOrig;
655 30
656 27
            $row = $this->cast($row);
657
            if ($row !== false) {
658
                foreach ($row as $key => $value) {
659 5
                    if ($class === \stdClass::class) {
660 5
                        $classTmp->{$key} = $value;
661
                    } else {
662
                        $propertyAccessor->setValue($classTmp, $key, $value);
663
                    }
664
                }
665
            }
666
667
            yield $classTmp;
668
        }
669
    }
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
    public function fetchArray(bool $reset = false)
680
    {
681
        if ($reset) {
682
            $this->reset();
683
        }
684
685
        $row = $this->fetch_assoc();
686
        if ($row) {
687 3
            $return = $this->cast($row);
688
689 3
            \assert(\is_array($return));
690 3
691
            return $return;
692 3
        }
693
694 3
        if ($row === null || $row === false) {
695
            return [];
696 3
        }
697
698 3
        return false;
699 3
    }
700 3
701
    /**
702
     * Fetch data as a key/value pair array.
703
     *
704 3
     * <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 6
     *    // array(127 => 'some value', 128 => 'some other value')
715
     * </code>
716 6
     *
717
     * @param string $key
718
     * @param string $value
719
     *
720 6
     * @return array
721 6
     */
722 3
    public function fetchArrayPair(string $key, string $value): array
723
    {
724
        $arrayPair = [];
725 3
        $data = $this->fetchAllArray();
726 3
727
        foreach ($data as &$_row) {
728
            if (
729
                \array_key_exists($key, $_row)
730
                &&
731
                \array_key_exists($value, $_row)
732
            ) {
733
                $_key = $_row[$key];
734
                $_value = $_row[$value];
735
                $arrayPair[$_key] = $_value;
736
            }
737
        }
738
739
        return $arrayPair;
740
    }
741 17
742
    /**
743 17
     * Fetch as "Arrayy"-object.
744 2
     *
745
     * @param bool $reset optional <p>Reset the \mysqli_result counter.</p>
746
     *
747 15
     * @return Arrayy|false
748 14
     *                      <p><strong>false</strong> on error</p>
749
     */
750
    public function fetchArrayy(bool $reset = false)
751 15
    {
752
        if ($reset) {
753 15
            $this->reset();
754
        }
755 5
756
        $row = $this->fetch_assoc();
757 5
        if ($row) {
758
            return Arrayy::create($this->cast($row));
759 5
        }
760
761
        if ($row === null || $row === false) {
762
            return Arrayy::create();
763
        }
764
765 14
        return false;
766 1
    }
767
768
    /**
769 14
     * 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
    public function fetchCallable(int $row = null, string $column = null)
778
    {
779
        if (!$this->num_rows) {
780
            return null;
781
        }
782 9
783
        if ($row !== null) {
784 9
            $this->seek($row);
785 7
        }
786
787 7
        $rows = $this->fetch_assoc();
788 7
789 7
        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 7
            if (
791 7
                \is_array($rows)
792
                &&
793 3
                isset($rows[$column])
794 3
            ) {
795
                return $rows[$column];
796
            }
797 7
798
            return null;
799 7
        }
800
801
        if (\is_callable($this->_mapper)) {
802 7
            return \call_user_func($this->_mapper, $rows);
803
        }
804
805
        return $rows;
806
    }
807 5
808
    /**
809 5
     * Fetch a single column as string (or as 1-dimension array).
810 5
     *
811 3
     * @param string $column
812 3
     * @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 5
     *
815 3
     * @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 5
     */
819
    public function fetchColumn(
820
        string $column = '',
821 5
        bool $skipNullValues = true,
822
        bool $asArray = false
823
    ) {
824
        if (!$asArray) {
825
            $columnData = '';
826
827
            $data = $this->fetchAllArrayy()->reverse()->getArray();
828 View Code Duplication
            foreach ($data as $_row) {
829
                if ($skipNullValues) {
830
                    if (!isset($_row[$column])) {
831 1
                        continue;
832
                    }
833 1
                } elseif (!\array_key_exists($column, $_row)) {
834 1
                    break;
835
                }
836 1
837 1
                $columnData = $_row[$column];
838 1
839
                break;
840
            }
841
842
            return $columnData;
843
        }
844
845
        // -- return as array -->
846
847
        $columnData = [];
848
849 View Code Duplication
        foreach ($this->fetchAllYield() as $_row) {
850
            if ($skipNullValues) {
851
                if (!isset($_row->{$column})) {
852
                    continue;
853 1
                }
854
            } elseif (!\array_key_exists($column, $_row)) {
855
                break;
856 1
            }
857 1
858
            $columnData[] = $_row->{$column};
859 1
        }
860 1
861
        return $columnData;
862
    }
863
864 1
    /**
865 1
     * 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 1
     * @return array
870
     *               <p>Array of field information each as an associative array.</p>
871 1
     */
872
    public function fetchFields(bool $as_array = false): array
873
    {
874
        $fields = $this->fetch_fields();
875 1
        if ($fields === false) {
876
            return [];
877 1
        }
878
879
        if ($as_array) {
880
            return \array_map(
881
                static function ($object) {
882
                    return (array) $object;
883
                },
884
                $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 21
     * @return array
898
     *               <p>A grouped array of scalar values or arrays.</p>
899 21
     */
900 9 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
        $groups = [];
904 21
        $pos = $this->current_row;
905
906 21
        foreach ($this as $row) {
907
            if (!\array_key_exists($group, $row)) {
908
                continue;
909 21
            }
910 21
911
            if ($column !== null) {
912 21
                if (!\array_key_exists($column, $row)) {
913 3
                    continue;
914
                }
915
916 21
                $groups[$row[$group]][] = $row[$column];
917 3
            } else {
918 21
                $groups[$row[$group]][] = $row;
919 3
            }
920 3
        }
921
922 21
        $this->rewind($pos);
923
924
        return $groups;
925 21
    }
926 21
927 21
    /**
928 21
     * Fetch as object.
929
     *
930 21
     * @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 21
     *                              </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 1 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
        if ($reset) {
948 1
            $this->reset();
949 1
        }
950
951 1
        // fallback
952 1
        if (!$class || $class === 'stdClass') {
953
            $class = \stdClass::class;
954
        }
955
956 1
        $row = $this->fetch_assoc();
957 1
        $row = $row ? $this->cast($row) : false;
958
959
        if (!$row) {
960
            return false;
961 1
        }
962
963 1
        if (\is_object($class)) {
964
            $classTmp = $class;
965
        } elseif ($class && $params) {
966
            $reflectorTmp = new \ReflectionClass($class);
967 1
            $classTmp = $reflectorTmp->newInstanceArgs($params);
968
        } else {
969 1
            $classTmp = new $class();
970
        }
971
972
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
973
        foreach ($row as $key => $value) {
974
            if ($class === \stdClass::class) {
975
                $classTmp->{$key} = $value;
976
            } else {
977
                $propertyAccessor->setValue($classTmp, $key, $value);
978
            }
979
        }
980 1
981
        return $classTmp;
982
    }
983 1
984 1
    /**
985 1
     * Returns all rows at once as key-value pairs.
986
     *
987 1
     * @param string $key    The column name to use as keys
988 1
     * @param string $column The column name to use as values (optional)
989 1
     *
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 1
        // init
996 1
        $pairs = [];
997
        $pos = $this->current_row;
998
999 1
        foreach ($this as $row) {
1000
            if (!\array_key_exists($key, $row)) {
1001 1
                continue;
1002 1
            }
1003 1
1004
            if ($column !== null) {
1005
                if (!\array_key_exists($column, $row)) {
1006
                    continue;
1007
                }
1008
1009
                $pairs[$row[$key]] = $row[$column];
1010
            } else {
1011
                $pairs[$row[$key]] = $row;
1012
            }
1013
        }
1014
1015
        $this->rewind($pos);
1016
1017
        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 4
     * @param string $column The column name to use as keys (optional)
1025
     *
1026 4
     * @return array
1027
     *               <p>A transposed array of arrays</p>
1028
     */
1029
    public function fetchTranspose(string $column = null)
1030
    {
1031 4
        // init
1032
        $keys = $column !== null ? $this->fetchAllColumn($column) : [];
1033 3
        $rows = [];
1034
        $pos = $this->current_row;
1035
1036 4
        foreach ($this as $row) {
1037
            foreach ($row as $key => $value) {
1038 4
                $rows[$key][] = $value;
1039
            }
1040
        }
1041
1042 4
        $this->rewind($pos);
1043
1044
        if (empty($keys)) {
1045 4
            return $rows;
1046 4
        }
1047
1048 4
        return \array_map(
1049
            static function ($values) use ($keys) {
1050
                return \array_combine($keys, $values);
1051
            },
1052 4
            $rows
1053 4
        );
1054 4
    }
1055 3
1056
    /**
1057 4
     * Fetch as "\Generator" via yield.
1058
     *
1059
     * @param object|string $class  <p>
1060
     *                              <strong>string</strong>: create a new object (with optional constructor
1061 4
     *                              parameter)<br>
1062 4
     *                              <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 78
     *                              string.
1068
     *                              </p>
1069 78
     * @param bool          $reset  optional <p>Reset the \mysqli_result counter.</p>
1070
     *
1071
     * @return \Generator
1072
     */
1073 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
        if ($reset) {
1076
            $this->reset();
1077
        }
1078
1079
        // fallback
1080
        if (!$class || $class === 'stdClass') {
1081
            $class = \stdClass::class;
1082
        }
1083
1084
        if (\is_object($class)) {
1085
            $classTmp = $class;
1086
        } elseif ($class && $params) {
1087
            $reflectorTmp = new \ReflectionClass($class);
1088
            $classTmp = $reflectorTmp->newInstanceArgs($params);
1089
        } else {
1090
            $classTmp = new $class();
1091
        }
1092
1093
        $row = $this->fetch_assoc();
1094
        $row = $row ? $this->cast($row) : false;
1095
1096
        if (!$row) {
1097
            return;
1098
        }
1099
1100
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
1101
        foreach ($row as $key => $value) {
1102 78
            if ($class === \stdClass::class) {
1103
                $classTmp->{$key} = $value;
1104
            } else {
1105
                $propertyAccessor->setValue($classTmp, $key, $value);
1106
            }
1107
        }
1108 1
1109
        yield $classTmp;
1110 1
    }
1111 1
1112
    /**
1113
     * @return mixed|null
1114
     */
1115
    private function fetch_assoc()
1116
    {
1117
        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
        return \mysqli_fetch_assoc($this->_result);
1151
    }
1152
1153
    /**
1154
     * @return array|false
1155
     */
1156
    private function fetch_fields()
1157
    {
1158
        if ($this->_result instanceof \mysqli_result) {
1159
            return \mysqli_fetch_fields($this->_result);
1160 3
        }
1161
1162 3
        if ($this->doctrineMySQLiStmt) {
1163 3
            $metadataTmp = $this->doctrineMySQLiStmt->result_metadata();
1164 3
            if ($metadataTmp === false) {
1165
                return [];
1166 3
            }
1167
1168
            return $metadataTmp->fetch_fields();
1169
        }
1170
1171
        if ($this->doctrinePdoStmt) {
1172 100
            $fields = [];
1173
1174
            static $THIS_CLASS_TMP = null;
1175 100
            if ($THIS_CLASS_TMP === null) {
1176
                $THIS_CLASS_TMP = new \ReflectionClass(__CLASS__);
1177 100
            }
1178
1179
            $totalColumnsTmp = $this->doctrinePdoStmt->columnCount();
1180 100
            for ($counterTmp = 0; $counterTmp < $totalColumnsTmp; $counterTmp++) {
1181 100
                $metadataTmp = $this->doctrinePdoStmt->getColumnMeta($counterTmp);
1182
                $fieldTmp = new \stdClass();
1183 100
                foreach ($metadataTmp as $metadataTmpKey => $metadataTmpValue) {
1184
                    $fieldTmp->{$metadataTmpKey} = $metadataTmpValue;
1185
                }
1186
1187 1
                $typeNativeTmp = 'MYSQL_TYPE_' . $metadataTmp['native_type'];
1188
                $typeTmp = $THIS_CLASS_TMP->getConstant($typeNativeTmp);
1189 1
                if ($typeTmp) {
1190
                    $fieldTmp->type = $typeTmp;
1191
                } else {
1192
                    $fieldTmp->type = '';
1193
                }
1194
1195
                $fields[] = $fieldTmp;
1196
            }
1197 1
1198
            return $fields;
1199 1
        }
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 3
     * @return mixed
1210
     *               <p>A row array or a single scalar value</p>
1211 3
     */
1212
    public function first(string $column = null)
1213
    {
1214
        $pos = $this->current_row;
1215
        $first = $this->fetchCallable(0, $column);
1216
        $this->rewind($pos);
1217
1218
        return $first;
1219
    }
1220
1221 3
    /**
1222
     * free the memory
1223 3
     */
1224
    public function free()
1225
    {
1226
        if ($this->_result instanceof \mysqli_result) {
1227
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
1228
            @\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
            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 3
     *
1249
     * @return array|false|object
1250 3
     *                            <p><strong>false</strong> on error</p>
1251
     *
1252
     * @see Result::fetch()
1253
     */
1254
    public function get()
1255
    {
1256
        return $this->fetch();
1257
    }
1258
1259
    /**
1260
     * alias for "Result->fetchAll()"
1261
     *
1262
     * @return array
1263
     *
1264
     * @see Result::fetchAll()
1265
     */
1266
    public function getAll(): array
1267
    {
1268
        return $this->fetchAll();
1269
    }
1270
1271
    /**
1272
     * alias for "Result->fetchAllColumn()"
1273
     *
1274
     * @param string $column
1275
     * @param bool   $skipNullValues
1276
     *
1277 3
     * @return array
1278
     *
1279 3
     * @see Result::fetchAllColumn()
1280
     */
1281
    public function getAllColumn(string $column, bool $skipNullValues = false): array
1282
    {
1283
        return $this->fetchAllColumn($column, $skipNullValues);
1284
    }
1285 3
1286
    /**
1287 3
     * alias for "Result->fetchAllArray()"
1288
     *
1289
     * @return array
1290
     *
1291
     * @see Result::fetchAllArray()
1292
     */
1293
    public function getArray(): array
1294
    {
1295
        return $this->fetchAllArray();
1296
    }
1297 3
1298
    /**
1299 3
     * 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 1
     * alias for "Result->fetchColumn()"
1312
     *
1313 1
     * @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 41
     * @see Result::fetchColumn()
1322
     */
1323 41
    public function getColumn(
1324
        string $column,
1325
        bool $skipNullValues = true,
1326
        bool $asArray = false
1327
    ) {
1328
        return $this->fetchColumn($column, $skipNullValues, $asArray);
1329
    }
1330
1331 3
    /**
1332
     * @return string
1333 3
     */
1334
    public function getDefaultResultType(): string
1335 3
    {
1336
        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 3
     */
1346
    public function getObject(): array
1347 3
    {
1348 3
        return $this->fetchAllObject();
1349 3
    }
1350
1351 3
    /**
1352
     * alias for "Result->fetchAllYield()"
1353
     *
1354
     * @return \Generator
1355
     *
1356
     * @see Result::fetchAllYield()
1357
     */
1358
    public function getYield(): \Generator
1359
    {
1360
        return $this->fetchAllYield();
1361 1
    }
1362
1363 1
    /**
1364
     * Check if the result is empty.
1365 1
     *
1366
     * @return bool
1367
     */
1368
    public function is_empty(): bool
1369
    {
1370
        return !($this->num_rows > 0);
1371
    }
1372
1373 1
    /**
1374
     * Fetch all results as "json"-string.
1375 1
     *
1376
     * @return false|string
1377
     */
1378
    public function json()
1379
    {
1380
        $data = $this->fetchAllArray();
1381
1382
        return UTF8::json_encode($data);
1383
    }
1384
1385 1
    /**
1386
     * Returns the last row element from the result.
1387 1
     *
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
    public function last(string $column = null)
1393
    {
1394
        $pos = $this->current_row;
1395
        $last = $this->fetchCallable($this->num_rows - 1, $column);
1396
        $this->rewind($pos);
1397 1
1398
        return $last;
1399 1
    }
1400 1
1401
    /**
1402
     * Set the mapper...
1403
     *
1404
     * @param \Closure $callable
1405
     *
1406
     * @return $this
1407
     */
1408
    public function map(\Closure $callable): self
1409
    {
1410
        $this->_mapper = $callable;
1411
1412
        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
    public function num_rows(): int
1422
    {
1423
        return $this->count();
1424
    }
1425
1426
    /**
1427
     * ArrayAccess interface implementation.
1428
     *
1429
     * @param int $offset <p>Offset number</p>
1430
     *
1431
     * @return bool
1432 38
     *              <p>true if offset exists, false otherwise.</p>
1433
     */
1434 38
    public function offsetExists($offset): bool
1435
    {
1436 38
        return \is_int($offset) && $offset >= 0 && $offset < $this->num_rows;
1437
    }
1438 38
1439
    /**
1440 38
     * ArrayAccess interface implementation.
1441
     *
1442
     * @param int $offset Offset number
1443
     *
1444
     * @return mixed
1445
     */
1446 38
    public function offsetGet($offset)
1447
    {
1448 38
        if ($this->offsetExists($offset)) {
1449
            return $this->fetchCallable($offset);
1450 38
        }
1451
1452
        throw new \OutOfBoundsException("undefined offset (${offset})");
1453
    }
1454 38
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 6
    }
1465
1466
    /**
1467 6
     * ArrayAccess interface implementation. Not implemented by design.
1468
     *
1469 6
     * @param mixed $offset
1470
     */
1471
    public function offsetUnset($offset)
1472
    {
1473 6
        /** @noinspection UselessReturnInspection */
1474
    }
1475 6
1476
    /**
1477 6
     * Reset the offset (data_seek) for the results.
1478
     *
1479
     * @return Result
1480
     */
1481
    public function reset(): self
1482
    {
1483
        $this->doctrinePdoStmtDataSeekFake = 0;
1484
1485
        if (!$this->is_empty()) {
1486 1
            if ($this->doctrineMySQLiStmt instanceof \mysqli_stmt) {
1487
                $this->doctrineMySQLiStmt->data_seek(0);
1488
            }
1489 1
1490
            if ($this->_result instanceof \mysqli_result) {
1491 1
                \mysqli_data_seek($this->_result, 0);
1492 1
            }
1493 1
        }
1494
1495 1
        return $this;
1496
    }
1497
1498
    /**
1499 1
     * You can set the default result-type to Result::RESULT_TYPE_*.
1500 1
     *
1501 1
     * INFO: used for "fetch()" and "fetchAll()"
1502 1
     *
1503 1
     * @param string $default_result_type
1504
     */
1505 1
    public function setDefaultResultType(string $default_result_type = self::RESULT_TYPE_OBJECT)
1506
    {
1507 1
        if (
1508
            $default_result_type === self::RESULT_TYPE_OBJECT
1509
            ||
1510 1
            $default_result_type === self::RESULT_TYPE_ARRAY
1511
            ||
1512
            $default_result_type === self::RESULT_TYPE_ARRAYY
1513
            ||
1514
            $default_result_type === self::RESULT_TYPE_YIELD
1515
        ) {
1516
            $this->_default_result_type = $default_result_type;
1517
        }
1518
    }
1519
1520
    /**
1521
     * @param int      $offset
1522
     * @param int|null $length
1523
     * @param bool     $preserve_keys
1524
     *
1525
     * @return array
1526
     */
1527
    public function slice(int $offset = 0, int $length = null, bool $preserve_keys = false): array
1528
    {
1529
        // init
1530
        $slice = [];
1531
1532
        if ($offset < 0) {
1533
            if (\abs($offset) > $this->num_rows) {
1534
                $offset = 0;
1535
            } else {
1536
                $offset = $this->num_rows - (int) \abs($offset);
1537
            }
1538
        }
1539
1540
        $length = $length !== null ? (int) $length : $this->num_rows;
1541
        $n = 0;
1542
        for ($i = $offset; $i < $this->num_rows && $n < $length; $i++) {
1543
            if ($preserve_keys) {
1544
                $slice[$i] = $this->fetchCallable($i);
1545
            } else {
1546
                $slice[] = $this->fetchCallable($i);
1547
            }
1548
            ++$n;
1549
        }
1550
1551
        return $slice;
1552
    }
1553
}
1554