Completed
Push — master ( ace86f...569799 )
by Lars
02:21
created

Result::setDefaultResultType()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.0729

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 14
ccs 6
cts 7
cp 0.8571
rs 8.8571
c 1
b 0
f 0
cc 5
eloc 10
nc 2
nop 1
crap 5.0729
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
 * @package   voku\db
15
 */
16
final class Result implements \Countable, \SeekableIterator, \ArrayAccess
17
{
18
19
  const MYSQL_TYPE_BIT         = 16;
20
  const MYSQL_TYPE_BLOB        = 252;
21
  const MYSQL_TYPE_DATE        = 10;
22
  const MYSQL_TYPE_DATETIME    = 12;
23
  const MYSQL_TYPE_DECIMAL     = 0;
24
  const MYSQL_TYPE_DOUBLE      = 5;
25
  const MYSQL_TYPE_ENUM        = 247;
26
  const MYSQL_TYPE_FLOAT       = 4;
27
  const MYSQL_TYPE_GEOMETRY    = 255;
28
  const MYSQL_TYPE_INT24       = 9;
29
  const MYSQL_TYPE_JSON        = 245;
30
  const MYSQL_TYPE_LONG        = 3;
31
  const MYSQL_TYPE_LONGLONG    = 8;
32
  const MYSQL_TYPE_LONG_BLOB   = 251;
33
  const MYSQL_TYPE_MEDIUM_BLOB = 250;
34
  const MYSQL_TYPE_NEWDATE     = 14;
35
  const MYSQL_TYPE_NEWDECIMAL  = 246;
36
  const MYSQL_TYPE_NULL        = 6;
37
  const MYSQL_TYPE_SET         = 248;
38
  const MYSQL_TYPE_SHORT       = 2;
39
  const MYSQL_TYPE_STRING      = 254;
40
  const MYSQL_TYPE_TIME        = 11;
41
  const MYSQL_TYPE_TIMESTAMP   = 7;
42
  const MYSQL_TYPE_TINY        = 1;
43
  const MYSQL_TYPE_TINY_BLOB   = 249;
44
  const MYSQL_TYPE_VARCHAR     = 15;
45
  const MYSQL_TYPE_VAR_STRING  = 253;
46
  const MYSQL_TYPE_YEAR        = 13;
47
48
  const RESULT_TYPE_ARRAY  = 'array';
49
  const RESULT_TYPE_ARRAYY = 'Arrayy';
50
  const RESULT_TYPE_OBJECT = 'object';
51
  const RESULT_TYPE_YIELD  = 'yield';
52
53
  /**
54
   * @var int
55
   */
56
  public $num_rows;
57
58
  /**
59
   * @var string
60
   */
61
  public $sql;
62
63
  /**
64
   * @var \mysqli_result|\Doctrine\DBAL\Statement
65
   */
66
  private $_result;
67
68
  /**
69
   * @var int
70
   */
71
  private $current_row;
72
73
  /**
74
   * @var \Closure|null
75
   */
76
  private $_mapper;
77
78
  /**
79
   * @var string
80
   */
81
  private $_default_result_type = self::RESULT_TYPE_OBJECT;
82
83
  /**
84
   * @var \mysqli_stmt|null
85
   */
86
  private $doctrineMySQLiStmt;
87
88
  /**
89
   * @var \Doctrine\DBAL\Driver\PDOStatement|null
90
   */
91
  private $doctrinePdoStmt;
92
93
  /**
94
   * Result constructor.
95
   *
96
   * @param string         $sql
97
   * @param \mysqli_result $result
98
   * @param \Closure       $mapper Optional callback mapper for the "fetchCallable()" method
99
   */
100 91
  public function __construct(string $sql, $result, \Closure $mapper = null)
101
  {
102 91
    $this->sql = $sql;
103
104
    if (
105 91
        !$result instanceof \mysqli_result
106
        &&
107 91
        !$result instanceof \Doctrine\DBAL\Statement
108
    ) {
109
      throw new \InvalidArgumentException('$result must be ' . \mysqli_result::class . ' or ' . \Doctrine\DBAL\Statement::class . ' !');
110
    }
111
112 91
    $this->_result = $result;
113
114 91
    if ($this->_result instanceof \Doctrine\DBAL\Statement) {
115
116 24
      $doctrineDriver = $this->_result->getWrappedStatement();
117
118 24
      if ($doctrineDriver instanceof \Doctrine\DBAL\Driver\PDOStatement) {
119
        $this->doctrinePdoStmt = $doctrineDriver;
120
      } // try to get the mysqli driver from doctrine
121 24
      elseif ($doctrineDriver instanceof \Doctrine\DBAL\Driver\Mysqli\MysqliStatement) {
122 24
        $reflectionTmp = new \ReflectionClass($doctrineDriver);
123 24
        $propertyTmp = $reflectionTmp->getProperty('_stmt');
124 24
        $propertyTmp->setAccessible(true);
125 24
        $this->doctrineMySQLiStmt = $propertyTmp->getValue($doctrineDriver);
126
      }
127
128 24
      $this->num_rows = $this->_result->rowCount();
129
    } else {
130 67
      $this->num_rows = (int)$this->_result->num_rows;
131
    }
132
133 91
    $this->current_row = 0;
134
135
136 91
    $this->_mapper = $mapper;
137 91
  }
138
139
  /**
140
   * __destruct
141
   */
142 90
  public function __destruct()
143
  {
144 90
    $this->free();
145 90
  }
146
147
  /**
148
   * Runs a user-provided callback with the MySQLi_Result object given as
149
   * argument and returns the result, or returns the MySQLi_Result object if
150
   * called without an argument.
151
   *
152
   * @param callable $callback User-provided callback (optional)
153
   *
154
   * @return mixed|\Doctrine\DBAL\Statement|\mysqli_result
155
   */
156 2
  public function __invoke(callable $callback = null)
157
  {
158 2
    if (null !== $callback) {
159 2
      return $callback($this->_result);
160
    }
161
162 1
    return $this->_result;
163
  }
164
165
  /**
166
   * Get the current "num_rows" as string.
167
   *
168
   * @return string
169
   */
170
  public function __toString()
171
  {
172
    return (string)$this->num_rows;
173
  }
174
175
  /**
176
   * Cast data into int, float or string.
177
   *
178
   * <p>
179
   *   <br />
180
   *   INFO: install / use "mysqlnd"-driver for better performance
181
   * </p>
182
   *
183
   * @param array|object $data
184
   *
185
   * @return array|object|false <p><strong>false</strong> on error</p>
186
   */
187 64
  private function cast(&$data)
188
  {
189
    if (
190 64
        !$this->doctrinePdoStmt // pdo only have limited support for types, so we try to improve it
191
        &&
192 64
        Helper::isMysqlndIsUsed() === true
193
    ) {
194 64
      return $data;
195
    }
196
197
    // init
198
    static $FIELDS_CACHE = [];
199
    static $TYPES_CACHE = [];
200
201
    $result_hash = \spl_object_hash($this->_result);
202
203
    if (!isset($FIELDS_CACHE[$result_hash])) {
204
      $FIELDS_CACHE[$result_hash] = $this->fetch_fields();
205
    }
206
207
    if (
208
        !isset($FIELDS_CACHE[$result_hash])
209
        ||
210
        $FIELDS_CACHE[$result_hash] === false
211
    ) {
212
      return false;
213
    }
214
215
    if (!isset($TYPES_CACHE[$result_hash])) {
216
      foreach ($FIELDS_CACHE[$result_hash] as $field) {
217
        switch ($field->type) {
218
          case self::MYSQL_TYPE_BIT:
219
            $TYPES_CACHE[$result_hash][$field->name] = 'boolean';
220
            break;
221
          case self::MYSQL_TYPE_TINY:
222
          case self::MYSQL_TYPE_SHORT:
223
          case self::MYSQL_TYPE_LONG:
224
          case self::MYSQL_TYPE_LONGLONG:
225
          case self::MYSQL_TYPE_INT24:
226
            $TYPES_CACHE[$result_hash][$field->name] = 'integer';
227
            break;
228
          case self::MYSQL_TYPE_DOUBLE:
229
          case self::MYSQL_TYPE_DECIMAL:
230
          case self::MYSQL_TYPE_NEWDECIMAL:
231
          case self::MYSQL_TYPE_FLOAT:
232
            $TYPES_CACHE[$result_hash][$field->name] = 'float';
233
            break;
234
          default:
235
            $TYPES_CACHE[$result_hash][$field->name] = 'string';
236
            break;
237
        }
238
      }
239
    }
240
241
    if (\is_array($data) === true) {
242 View Code Duplication
      foreach ($TYPES_CACHE[$result_hash] as $type_name => $type) {
243
        if (isset($data[$type_name])) {
244
          \settype($data[$type_name], $type);
245
        }
246
      }
247
    } elseif (\is_object($data)) {
248 View Code Duplication
      foreach ($TYPES_CACHE[$result_hash] as $type_name => $type) {
249
        if (isset($data->{$type_name})) {
250
          \settype($data->{$type_name}, $type);
251
        }
252
      }
253
    }
254
255
    return $data;
256
  }
257
258
  /**
259
   * Countable interface implementation.
260
   *
261
   * @return int The number of rows in the result
262
   */
263 2
  public function count(): int
264
  {
265 2
    return $this->num_rows;
266
  }
267
268
  /**
269
   * Iterator interface implementation.
270
   *
271
   * @return mixed The current element
272
   */
273 7
  public function current()
274
  {
275 7
    return $this->fetchCallable($this->current_row);
276
  }
277
278
  /**
279
   * Iterator interface implementation.
280
   *
281
   * @return int The current element key (row index; zero-based)
282
   */
283 1
  public function key(): int
284
  {
285 1
    return $this->current_row;
286
  }
287
288
  /**
289
   * Iterator interface implementation.
290
   *
291
   * @return void
292
   */
293 7
  public function next()
294
  {
295 7
    $this->current_row++;
296 7
  }
297
298
  /**
299
   * Iterator interface implementation.
300
   *
301
   * @param int $row Row position to rewind to; defaults to 0
302
   *
303
   * @return void
304
   */
305 11
  public function rewind($row = 0)
306
  {
307 11
    if ($this->seek($row)) {
308 9
      $this->current_row = $row;
309
    }
310 11
  }
311
312
  /**
313
   * Moves the internal pointer to the specified row position.
314
   *
315
   * @param int $row <p>Row position; zero-based and set to 0 by default</p>
316
   *
317
   * @return bool <p>true on success, false otherwise</p>
318
   */
319 19
  public function seek($row = 0): bool
320
  {
321 19
    if (\is_int($row) && $row >= 0 && $row < $this->num_rows) {
322
323 15
      if ($this->doctrineMySQLiStmt) {
324 1
        $this->doctrineMySQLiStmt->data_seek($row);
325
326 1
        return true;
327
      }
328
329 14
      if ($this->doctrinePdoStmt) {
330
        return (bool)$this->doctrinePdoStmt->fetch(\PDO::FETCH_ASSOC, \PDO::FETCH_ORI_NEXT, $row);
331
      }
332
333 14
      return \mysqli_data_seek($this->_result, $row);
334
    }
335
336 4
    return false;
337
  }
338
339
  /**
340
   * Iterator interface implementation.
341
   *
342
   * @return bool <p>true if the current index is valid, false otherwise</p>
343
   */
344 7
  public function valid(): bool
345
  {
346 7
    return $this->current_row < $this->num_rows;
347
  }
348
349
  /**
350
   * Fetch.
351
   *
352
   * <p>
353
   *   <br />
354
   *   INFO: this will return an object by default, not an array<br />
355
   *   and you can change the behaviour via "Result->setDefaultResultType()"
356
   * </p>
357
   *
358
   * @param bool $reset optional <p>Reset the \mysqli_result counter.</p>
359
   *
360
   * @return array|object|false <p><strong>false</strong> on error</p>
361
   */
362 4
  public function fetch(bool $reset = false)
363
  {
364 4
    $return = false;
365
366 4
    if ($this->_default_result_type === self::RESULT_TYPE_OBJECT) {
367 4
      $return = $this->fetchObject('', null, $reset);
368 4
    } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAY) {
369 4
      $return = $this->fetchArray($reset);
370
    } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAYY) {
371
      $return = $this->fetchArrayy($reset);
372
    } elseif ($this->_default_result_type === self::RESULT_TYPE_YIELD) {
373
      $return = $this->fetchYield($reset);
0 ignored issues
show
Documentation introduced by
$reset is of type boolean, but the function expects a string|object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
374
    }
375
376 4
    return $return;
377
  }
378
379
  /**
380
   * Fetch all results.
381
   *
382
   * <p>
383
   *   <br />
384
   *   INFO: this will return an object by default, not an array<br />
385
   *   and you can change the behaviour via "Result->setDefaultResultType()"
386
   * </p>
387
   *
388
   * @return array
389
   */
390 4
  public function fetchAll(): array
391
  {
392 4
    $return = [];
393
394 4
    if ($this->_default_result_type === self::RESULT_TYPE_OBJECT) {
395 4
      $return = $this->fetchAllObject();
396 2
    } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAY) {
397 2
      $return = $this->fetchAllArray();
398
    } elseif ($this->_default_result_type === self::RESULT_TYPE_ARRAYY) {
399
      $return = $this->fetchAllArrayy();
400
    } elseif ($this->_default_result_type === self::RESULT_TYPE_YIELD) {
401
      $return = $this->fetchAllYield();
402
    }
403
404 4
    return $return;
405
  }
406
407
  /**
408
   * Fetch all results as array.
409
   *
410
   * @return array
411
   */
412 23
  public function fetchAllArray(): array
413
  {
414 23
    if ($this->is_empty()) {
415
      return [];
416
    }
417
418 23
    $this->reset();
419
420 23
    $data = [];
421
    /** @noinspection PhpAssignmentInConditionInspection */
422 23
    while ($row = $this->fetch_assoc()) {
423 23
      $data[] = $this->cast($row);
424
    }
425
426 23
    return $data;
427
  }
428
429
  /**
430
   * Fetch all results as "Arrayy"-object.
431
   *
432
   * @return Arrayy
433
   */
434 7
  public function fetchAllArrayy(): Arrayy
435
  {
436 7
    if ($this->is_empty()) {
437
      return Arrayy::create([]);
438
    }
439
440 7
    $this->reset();
441
442 7
    $data = [];
443
    /** @noinspection PhpAssignmentInConditionInspection */
444 7
    while ($row = $this->fetch_assoc()) {
445 7
      $data[] = $this->cast($row);
446
    }
447
448 7
    return Arrayy::create($data);
449
  }
450
451
  /**
452
   * Fetch a single column as an 1-dimension array.
453
   *
454
   * @param string $column
455
   * @param bool   $skipNullValues <p>Skip "NULL"-values. | default: false</p>
456
   *
457
   * @return array <p>Return an empty array if the "$column" wasn't found</p>
458
   */
459 4
  public function fetchAllColumn(string $column, bool $skipNullValues = false): array
460
  {
461 4
    return $this->fetchColumn($column, $skipNullValues, true);
462
  }
463
464
  /**
465
   * Fetch all results as array with objects.
466
   *
467
   * @param object|string $class  <p>
468
   *                              <strong>string</strong>: create a new object (with optional constructor
469
   *                              parameter)<br>
470
   *                              <strong>object</strong>: use a object and fill the the data into
471
   *                              </p>
472
   * @param null|array    $params optional
473
   *                              <p>
474
   *                              An array of parameters to pass to the constructor, used if $class is a
475
   *                              string.
476
   *                              </p>
477
   *
478
   * @return array
479
   */
480 14
  public function fetchAllObject($class = '', array $params = null): array
481
  {
482 14
    if ($this->is_empty()) {
483 1
      return [];
484
    }
485
486
    // fallback
487 13
    if (!$class || $class === 'stdClass') {
488 6
      $class = '\stdClass';
489
    }
490
491
    // init
492 13
    $data = [];
493 13
    $this->reset();
494 13
    $propertyAccessor = PropertyAccess::createPropertyAccessor();
495
496 13
    if (\is_object($class)) {
497
498 7
      $classTmpOrig = new $class;
499
500 6
    } elseif ($class && $params) {
501
502 2
      $reflectorTmp = new \ReflectionClass($class);
503 2
      $classTmpOrig = $reflectorTmp->newInstanceArgs($params);
504
505
    } else {
506
507 6
      $classTmpOrig = new $class;
508
509
    }
510
511
    /** @noinspection PhpAssignmentInConditionInspection */
512 13 View Code Duplication
    while ($row = $this->fetch_assoc()) {
513 13
      $classTmp = clone $classTmpOrig;
514 13
      $row = $this->cast($row);
515 13
      foreach ($row as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $row of type array|object|false is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
516 13
        if ($class === '\stdClass') {
517 6
          $classTmp->{$key} = $value;
518
        } else {
519 13
          $propertyAccessor->setValue($classTmp, $key, $value);
520
        }
521
      }
522 13
      $data[] = $classTmp;
523
    }
524
525 13
    return $data;
526
  }
527
528
  /**
529
   * Fetch all results as "\Generator" via yield.
530
   *
531
   * @param object|string $class  <p>
532
   *                              <strong>string</strong>: create a new object (with optional constructor
533
   *                              parameter)<br>
534
   *                              <strong>object</strong>: use a object and fill the the data into
535
   *                              </p>
536
   * @param null|array    $params optional
537
   *                              <p>
538
   *                              An array of parameters to pass to the constructor, used if $class is a
539
   *                              string.
540
   *                              </p>
541
   *
542
   * @return \Generator
543
   */
544 2
  public function fetchAllYield($class = '', array $params = null): \Generator
545
  {
546 2
    if ($this->is_empty()) {
547
      return;
548
    }
549
550
    // init
551 2
    $this->reset();
552
553
    // fallback
554 2
    if (!$class || $class === 'stdClass') {
555 2
      $class = '\stdClass';
556
    }
557
558 2
    $propertyAccessor = PropertyAccess::createPropertyAccessor();
559
560 2
    if (\is_object($class)) {
561
562
      $classTmpOrig = $class;
563
564 2
    } else if ($class && $params) {
565
566
      $reflectorTmp = new \ReflectionClass($class);
567
      $classTmpOrig = $reflectorTmp->newInstanceArgs($params);
568
569
    } else {
570
571 2
      $classTmpOrig = new $class;
572
573
    }
574
575
    /** @noinspection PhpAssignmentInConditionInspection */
576 2 View Code Duplication
    while ($row = $this->fetch_assoc()) {
577 2
      $classTmp = clone $classTmpOrig;
578 2
      $row = $this->cast($row);
579 2
      foreach ($row as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $row of type array|object|false is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
580 2
        if ($class === '\stdClass') {
581 2
          $classTmp->{$key} = $value;
582
        } else {
583 2
          $propertyAccessor->setValue($classTmp, $key, $value);
584
        }
585
      }
586 2
      yield $classTmp;
587
    }
588 2
  }
589
590
  /**
591
   * Fetch as array.
592
   *
593
   * @param bool $reset
594
   *
595
   * @return array|false <p><strong>false</strong> on error</p>
596
   */
597 23 View Code Duplication
  public function fetchArray(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...
598
  {
599 23
    if ($reset === true) {
600 2
      $this->reset();
601
    }
602
603 23
    $row = $this->fetch_assoc();
604 23
    if ($row) {
605 21
      return $this->cast($row);
606
    }
607
608 4
    if ($row === null || $row === false) {
609 4
      return [];
610
    }
611
612
    return false;
613
  }
614
615
  /**
616
   * Fetch data as a key/value pair array.
617
   *
618
   * <p>
619
   *   <br />
620
   *   INFO: both "key" and "value" must exists in the fetched data
621
   *   the key will be the new key of the result-array
622
   *   <br /><br />
623
   * </p>
624
   *
625
   * e.g.:
626
   * <code>
627
   *    fetchArrayPair('some_id', 'some_value');
628
   *    // array(127 => 'some value', 128 => 'some other value')
629
   * </code>
630
   *
631
   * @param string $key
632
   * @param string $value
633
   *
634
   * @return array
635
   */
636 2
  public function fetchArrayPair(string $key, string $value): array
637
  {
638 2
    $arrayPair = [];
639 2
    $data = $this->fetchAllArray();
640
641 2
    foreach ($data as &$_row) {
642
      if (
643 2
          \array_key_exists($key, $_row) === true
644
          &&
645 2
          \array_key_exists($value, $_row) === true
646
      ) {
647 2
        $_key = $_row[$key];
648 2
        $_value = $_row[$value];
649 2
        $arrayPair[$_key] = $_value;
650
      }
651
    }
652
653 2
    return $arrayPair;
654
  }
655
656
  /**
657
   * Fetch as "Arrayy"-object.
658
   *
659
   * @param bool $reset optional <p>Reset the \mysqli_result counter.</p>
660
   *
661
   * @return Arrayy|false <p><strong>false</strong> on error</p>
662
   */
663 4 View Code Duplication
  public function fetchArrayy(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...
664
  {
665 4
    if ($reset === true) {
666
      $this->reset();
667
    }
668
669 4
    $row = $this->fetch_assoc();
670 4
    if ($row) {
671 2
      return Arrayy::create($this->cast($row));
0 ignored issues
show
Bug introduced by
It seems like $this->cast($row) targeting voku\db\Result::cast() can also be of type false or object; however, Arrayy\Arrayy::create() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
672
    }
673
674 2
    if ($row === null || $row === false) {
675 2
      return Arrayy::create();
676
    }
677
678
    return false;
679
  }
680
681
  /**
682
   * Fetches a row or a single column within a row. Returns null if there are
683
   * no more rows in the result.
684
   *
685
   * @param int    $row    The row number (optional)
686
   * @param string $column The column name (optional)
687
   *
688
   * @return mixed An associative array or a scalar value
689
   */
690 16
  public function fetchCallable(int $row = null, string $column = null)
691
  {
692 16
    if (!$this->num_rows) {
693 2
      return null;
694
    }
695
696 14
    if (null !== $row) {
697 13
      $this->seek($row);
698
    }
699
700 14
    $rows = $this->fetch_assoc();
701
702 14
    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...
703 5
      return \is_array($rows) && isset($rows[$column]) ? $rows[$column] : null;
704
    }
705
706 13
    return \is_callable($this->_mapper) ? \call_user_func($this->_mapper, $rows) : $rows;
707
  }
708
709
  /**
710
   * Fetch a single column as string (or as 1-dimension array).
711
   *
712
   * @param string $column
713
   * @param bool   $skipNullValues <p>Skip "NULL"-values. | default: true</p>
714
   * @param bool   $asArray        <p>Get all values and not only the last one. | default: false</p>
715
   *
716
   * @return string|array <p>Return a empty string or an empty array if the "$column" wasn't found, depend on
717
   *                      "$asArray"</p>
718
   */
719 7
  public function fetchColumn(string $column = '', bool $skipNullValues = true, bool $asArray = false)
720
  {
721 7
    if ($asArray === false) {
722 5
      $columnData = '';
723
724 5
      $data = $this->fetchAllArrayy()->reverse();
725 5 View Code Duplication
      foreach ($data as $_row) {
726
727 5
        if ($skipNullValues === true) {
728 5
          if (isset($_row[$column]) === false) {
729 5
            continue;
730
          }
731
        } else {
732 2
          if (\array_key_exists($column, $_row) === false) {
733 2
            break;
734
          }
735
        }
736
737 5
        $columnData = $_row[$column];
738 5
        break;
739
      }
740
741 5
      return $columnData;
742
    }
743
744
    // -- return as array -->
745
746 4
    $columnData = [];
747
748 4
    $data = $this->fetchAllArray();
749
750 4 View Code Duplication
    foreach ($data as $_row) {
751
752 4
      if ($skipNullValues === true) {
753 2
        if (isset($_row[$column]) === false) {
754 2
          continue;
755
        }
756
      } else {
757 4
        if (\array_key_exists($column, $_row) === false) {
758 2
          break;
759
        }
760
      }
761
762 4
      $columnData[] = $_row[$column];
763
    }
764
765 4
    return $columnData;
766
  }
767
768
  /**
769
   * Return rows of field information in a result set.
770
   *
771
   * @param bool $as_array Return each field info as array; defaults to false
772
   *
773
   * @return array Array of field information each as an associative array
774
   */
775 1
  public function fetchFields(bool $as_array = false): array
776
  {
777 1
    if ($as_array) {
778 1
      return \array_map(
779 1
          function ($object) {
780 1
            return (array)$object;
781 1
          },
782 1
          $this->fetch_fields()
783
      );
784
    }
785
786
    return $this->fetch_fields();
787
  }
788
789
  /**
790
   * Returns all rows at once as a grouped array of scalar values or arrays.
791
   *
792
   * @param string $group  The column name to use for grouping
793
   * @param string $column The column name to use as values (optional)
794
   *
795
   * @return array A grouped array of scalar values or arrays
796
   */
797 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...
798
  {
799
    // init
800 1
    $groups = [];
801 1
    $pos = $this->current_row;
802
803 1
    foreach ($this as $row) {
804
805 1
      if (!\array_key_exists($group, $row)) {
806
        continue;
807
      }
808
809 1
      if (null !== $column) {
810
811 1
        if (!\array_key_exists($column, $row)) {
812
          continue;
813
        }
814
815 1
        $groups[$row[$group]][] = $row[$column];
816
      } else {
817 1
        $groups[$row[$group]][] = $row;
818
      }
819
    }
820
821 1
    $this->rewind($pos);
822
823 1
    return $groups;
824
  }
825
826
  /**
827
   * Fetch as object.
828
   *
829
   * @param object|string $class  <p>
830
   *                              <strong>string</strong>: create a new object (with optional constructor
831
   *                              parameter)<br>
832
   *                              <strong>object</strong>: use a object and fill the the data into
833
   *                              </p>
834
   * @param null|array    $params optional
835
   *                              <p>
836
   *                              An array of parameters to pass to the constructor, used if $class is a
837
   *                              string.
838
   *                              </p>
839
   * @param bool          $reset  optional <p>Reset the \mysqli_result counter.</p>
840
   *
841
   * @return object|false <p><strong>false</strong> on error</p>
842
   */
843 25 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...
844
  {
845 25
    if ($reset === true) {
846 17
      $this->reset();
847
    }
848
849
    // fallback
850 25
    if (!$class || $class === 'stdClass') {
851 14
      $class = '\stdClass';
852
    }
853
854 25
    $row = $this->fetch_assoc();
855 25
    $row = $row ? $this->cast($row) : false;
856
857 25
    if (!$row) {
858 5
      return false;
859
    }
860
861 23
    $propertyAccessor = PropertyAccess::createPropertyAccessor();
862
863 23
    if (\is_object($class)) {
864
865 11
      $classTmp = $class;
866
867 14
    } elseif ($class && $params) {
868
869 2
      $reflectorTmp = new \ReflectionClass($class);
870 2
      $classTmp = $reflectorTmp->newInstanceArgs($params);
871
872
    } else {
873
874 14
      $classTmp = new $class;
875
876
    }
877
878 23
    foreach ($row as $key => $value) {
879 23
      if ($class === '\stdClass') {
880 14
        $classTmp->{$key} = $value;
881
      } else {
882 23
        $propertyAccessor->setValue($classTmp, $key, $value);
883
      }
884
    }
885
886 23
    return $classTmp;
887
  }
888
889
  /**
890
   * Returns all rows at once as key-value pairs.
891
   *
892
   * @param string $key    The column name to use as keys
893
   * @param string $column The column name to use as values (optional)
894
   *
895
   * @return array An array of key-value pairs
896
   */
897 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...
898
  {
899
    // init
900 1
    $pairs = [];
901 1
    $pos = $this->current_row;
902
903 1
    foreach ($this as $row) {
904
905 1
      if (!\array_key_exists($key, $row)) {
906
        continue;
907
      }
908
909 1
      if (null !== $column) {
910
911 1
        if (!\array_key_exists($column, $row)) {
912
          continue;
913
        }
914
915 1
        $pairs[$row[$key]] = $row[$column];
916
      } else {
917 1
        $pairs[$row[$key]] = $row;
918
      }
919
    }
920
921 1
    $this->rewind($pos);
922
923 1
    return $pairs;
924
  }
925
926
  /**
927
   * Returns all rows at once, transposed as an array of arrays. Instead of
928
   * returning rows of columns, this method returns columns of rows.
929
   *
930
   * @param string $column The column name to use as keys (optional)
931
   *
932
   * @return mixed A transposed array of arrays
933
   */
934 1
  public function fetchTranspose(string $column = null)
935
  {
936
    // init
937 1
    $keys = null !== $column ? $this->fetchAllColumn($column) : [];
938 1
    $rows = [];
939 1
    $pos = $this->current_row;
940
941 1
    foreach ($this as $row) {
942 1
      foreach ($row as $key => $value) {
943 1
        $rows[$key][] = $value;
944
      }
945
    }
946
947 1
    $this->rewind($pos);
948
949 1
    if (empty($keys)) {
950 1
      return $rows;
951
    }
952
953 1
    return \array_map(
954 1
        function ($values) use ($keys) {
955 1
          return \array_combine($keys, $values);
956 1
        }, $rows
957
    );
958
  }
959
960
  /**
961
   * Fetch as "\Generator" via yield.
962
   *
963
   * @param object|string $class  <p>
964
   *                              <strong>string</strong>: create a new object (with optional constructor
965
   *                              parameter)<br>
966
   *                              <strong>object</strong>: use a object and fill the the data into
967
   *                              </p>
968
   * @param null|array    $params optional
969
   *                              <p>
970
   *                              An array of parameters to pass to the constructor, used if $class is a
971
   *                              string.
972
   *                              </p>
973
   * @param bool          $reset  optional <p>Reset the \mysqli_result counter.</p>
974
   *
975
   * @return \Generator
976
   */
977 2 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...
978
  {
979 2
    if ($reset === true) {
980
      $this->reset();
981
    }
982
983
    // fallback
984 2
    if (!$class || $class === 'stdClass') {
985 2
      $class = '\stdClass';
986
    }
987
988 2
    $propertyAccessor = PropertyAccess::createPropertyAccessor();
989
990 2
    if (\is_object($class)) {
991
992
      $classTmp = $class;
993
994 2
    } elseif ($class && $params) {
995
996
      $reflectorTmp = new \ReflectionClass($class);
997
      $classTmp = $reflectorTmp->newInstanceArgs($params);
998
999
    } else {
1000
1001 2
      $classTmp = new $class;
1002
1003
    }
1004
1005 2
    $row = $this->fetch_assoc();
1006 2
    $row = $row ? $this->cast($row) : false;
1007
1008 2
    if (!$row) {
1009
      return;
1010
    }
1011
1012 2
    foreach ($row as $key => $value) {
1013 2
      if ($class === '\stdClass') {
1014 2
        $classTmp->{$key} = $value;
1015
      } else {
1016 2
        $propertyAccessor->setValue($classTmp, $key, $value);
1017
      }
1018
    }
1019
1020 2
    yield $classTmp;
1021 2
  }
1022
1023
  /**
1024
   * @return mixed
1025
   */
1026 76
  private function fetch_assoc()
1027
  {
1028 76
    if ($this->_result instanceof \Doctrine\DBAL\Statement) {
1029 15
      $this->_result->setFetchMode(\PDO::FETCH_ASSOC);
1030 15
      $object = $this->_result->fetch();
1031
1032 15
      return $object;
1033
    }
1034
1035 61
    return mysqli_fetch_assoc($this->_result);
1036
  }
1037
1038
  /**
1039
   * @return array|bool
1040
   */
1041 1
  private function fetch_fields()
1042
  {
1043 1
    if ($this->_result instanceof \mysqli_result) {
1044 1
      return \mysqli_fetch_fields($this->_result);
1045
    }
1046
1047
    if ($this->doctrineMySQLiStmt) {
1048
      $metadataTmp = $this->doctrineMySQLiStmt->result_metadata();
1049
1050
      return $metadataTmp->fetch_fields();
1051
    }
1052
1053
    if ($this->doctrinePdoStmt) {
1054
      $fields = [];
1055
1056
      static $THIS_CLASS_TMP = null;
1057
      if ($THIS_CLASS_TMP === null) {
1058
        $THIS_CLASS_TMP = new \ReflectionClass(__CLASS__);
1059
      }
1060
1061
      $totalColumnsTmp = $this->doctrinePdoStmt->columnCount();
1062
      for ($counterTmp = 0; $counterTmp < $totalColumnsTmp; $counterTmp++) {
1063
        $metadataTmp = $this->doctrinePdoStmt->getColumnMeta($counterTmp);
1064
        $fieldTmp = new \stdClass();
1065
        foreach ($metadataTmp as $metadataTmpKey => $metadataTmpValue) {
1066
          $fieldTmp->{$metadataTmpKey} = $metadataTmpValue;
1067
        }
1068
1069
        $typeNativeTmp = 'MYSQL_TYPE_' . $metadataTmp['native_type'];
1070
        $typeTmp = $THIS_CLASS_TMP->getConstant($typeNativeTmp);
1071
        if ($typeTmp) {
1072
          $fieldTmp->type = $typeTmp;
1073
        } else {
1074
          $fieldTmp->type = '';
1075
        }
1076
1077
        $fields[] = $fieldTmp;
1078
      }
1079
1080
      return $fields;
1081
    }
1082
1083
    return false;
1084
  }
1085
1086
  /**
1087
   * Returns the first row element from the result.
1088
   *
1089
   * @param string $column The column name to use as value (optional)
1090
   *
1091
   * @return mixed A row array or a single scalar value
1092
   */
1093 3
  public function first(string $column = null)
1094
  {
1095 3
    $pos = $this->current_row;
1096 3
    $first = $this->fetchCallable(0, $column);
1097 3
    $this->rewind($pos);
1098
1099 3
    return $first;
1100
  }
1101
1102
  /**
1103
   * free the memory
1104
   */
1105 90
  public function free()
1106
  {
1107 90
    if ($this->_result instanceof \mysqli_result) {
1108
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1109 66
      @\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...
1110 66
      $this->_result = null;
1111
1112 66
      return true;
1113
    }
1114
1115 25
    $this->_result = null;
1116
1117 25
    return false;
1118
  }
1119
1120
  /**
1121
   * alias for "Result->fetch()"
1122
   *
1123
   * @see Result::fetch()
1124
   *
1125
   * @return array|object|false <p><strong>false</strong> on error</p>
1126
   */
1127 2
  public function get()
1128
  {
1129 2
    return $this->fetch();
1130
  }
1131
1132
  /**
1133
   * alias for "Result->fetchAll()"
1134
   *
1135
   * @see Result::fetchAll()
1136
   *
1137
   * @return array
1138
   */
1139 2
  public function getAll(): array
1140
  {
1141 2
    return $this->fetchAll();
1142
  }
1143
1144
  /**
1145
   * alias for "Result->fetchAllColumn()"
1146
   *
1147
   * @see Result::fetchAllColumn()
1148
   *
1149
   * @param string $column
1150
   * @param bool   $skipNullValues
1151
   *
1152
   * @return array
1153
   */
1154
  public function getAllColumn(string $column, bool $skipNullValues = false): array
1155
  {
1156
    return $this->fetchAllColumn($column, $skipNullValues);
1157
  }
1158
1159
  /**
1160
   * alias for "Result->fetchAllArray()"
1161
   *
1162
   * @see Result::fetchAllArray()
1163
   *
1164
   * @return array
1165
   */
1166 2
  public function getArray(): array
1167
  {
1168 2
    return $this->fetchAllArray();
1169
  }
1170
1171
  /**
1172
   * alias for "Result->fetchAllArrayy()"
1173
   *
1174
   * @see Result::fetchAllArrayy()
1175
   *
1176
   * @return Arrayy
1177
   */
1178
  public function getArrayy(): Arrayy
1179
  {
1180
    return $this->fetchAllArrayy();
1181
  }
1182
1183
  /**
1184
   * alias for "Result->fetchColumn()"
1185
   *
1186
   * @see Result::fetchColumn()
1187
   *
1188
   * @param string $column
1189
   * @param bool   $asArray
1190
   * @param bool   $skipNullValues
1191
   *
1192
   * @return string|array <p>Return a empty string or an empty array if the "$column" wasn't found, depend on
1193
   *                      "$asArray"</p>
1194
   */
1195 2
  public function getColumn(string $column, bool $skipNullValues = true, bool $asArray = false)
1196
  {
1197 2
    return $this->fetchColumn($column, $skipNullValues, $asArray);
1198
  }
1199
1200
  /**
1201
   * @return string
1202
   */
1203 2
  public function getDefaultResultType(): string
1204
  {
1205 2
    return $this->_default_result_type;
1206
  }
1207
1208
  /**
1209
   * alias for "Result->fetchAllObject()"
1210
   *
1211
   * @see Result::fetchAllObject()
1212
   *
1213
   * @return array of mysql-objects
1214
   */
1215 2
  public function getObject(): array
1216
  {
1217 2
    return $this->fetchAllObject();
1218
  }
1219
1220
  /**
1221
   * alias for "Result->fetchAllYield()"
1222
   *
1223
   * @see Result::fetchAllYield()
1224
   *
1225
   * @param bool $asArray
1226
   *
1227
   * @return \Generator
1228
   */
1229
  public function getYield($asArray = false): \Generator
1230
  {
1231
    yield $this->fetchAllYield($asArray);
0 ignored issues
show
Documentation introduced by
$asArray is of type boolean, but the function expects a string|object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1232
  }
1233
1234
  /**
1235
   * Check if the result is empty.
1236
   *
1237
   * @return bool
1238
   */
1239 46
  public function is_empty(): bool
1240
  {
1241 46
    return !($this->num_rows > 0);
1242
  }
1243
1244
  /**
1245
   * Fetch all results as "json"-string.
1246
   *
1247
   * @return string
1248
   */
1249 2
  public function json(): string
1250
  {
1251 2
    $data = $this->fetchAllArray();
1252
1253 2
    return UTF8::json_encode($data);
1254
  }
1255
1256
  /**
1257
   * Returns the last row element from the result.
1258
   *
1259
   * @param string $column The column name to use as value (optional)
1260
   *
1261
   * @return mixed A row array or a single scalar value
1262
   */
1263 3
  public function last(string $column = null)
1264
  {
1265 3
    $pos = $this->current_row;
1266 3
    $last = $this->fetchCallable($this->num_rows - 1, $column);
1267 3
    $this->rewind($pos);
1268
1269 3
    return $last;
1270
  }
1271
1272
  /**
1273
   * Set the mapper...
1274
   *
1275
   * @param \Closure $callable
1276
   *
1277
   * @return $this
1278
   */
1279 1
  public function map(\Closure $callable): self
1280
  {
1281 1
    $this->_mapper = $callable;
1282
1283 1
    return $this;
1284
  }
1285
1286
  /**
1287
   * Alias of count(). Deprecated.
1288
   *
1289
   * @return int The number of rows in the result
1290
   */
1291 1
  public function num_rows(): int
1292
  {
1293 1
    return $this->count();
1294
  }
1295
1296
  /**
1297
   * ArrayAccess interface implementation.
1298
   *
1299
   * @param int $offset <p>Offset number</p>
1300
   *
1301
   * @return bool <p>true if offset exists, false otherwise</p>
1302
   */
1303 1
  public function offsetExists($offset): bool
1304
  {
1305 1
    return \is_int($offset) && $offset >= 0 && $offset < $this->num_rows;
1306
  }
1307
1308
  /**
1309
   * ArrayAccess interface implementation.
1310
   *
1311
   * @param int $offset Offset number
1312
   *
1313
   * @return mixed
1314
   */
1315 1
  public function offsetGet($offset)
1316
  {
1317 1
    if ($this->offsetExists($offset)) {
1318 1
      return $this->fetchCallable($offset);
1319
    }
1320
1321
    throw new \OutOfBoundsException("undefined offset ($offset)");
1322
  }
1323
1324
  /**
1325
   * ArrayAccess interface implementation. Not implemented by design.
1326
   *
1327
   * @param mixed $offset
1328
   * @param mixed $value
1329
   */
1330
  public function offsetSet($offset, $value)
1331
  {
1332
    /** @noinspection UselessReturnInspection */
1333
    return;
1334
  }
1335
1336
  /**
1337
   * ArrayAccess interface implementation. Not implemented by design.
1338
   *
1339
   * @param mixed $offset
1340
   */
1341
  public function offsetUnset($offset)
1342
  {
1343
    /** @noinspection UselessReturnInspection */
1344
    return;
1345
  }
1346
1347
  /**
1348
   * Reset the offset (data_seek) for the results.
1349
   *
1350
   * @return Result
1351
   */
1352 43
  public function reset(): self
1353
  {
1354 43
    if (!$this->is_empty()) {
1355
1356 41
      if ($this->doctrineMySQLiStmt) {
1357 9
        $this->doctrineMySQLiStmt->data_seek(0);
1358
      }
1359
1360 41
      if ($this->_result instanceof \mysqli_result) {
1361 32
        \mysqli_data_seek($this->_result, 0);
1362
      }
1363
    }
1364
1365 43
    return $this;
1366
  }
1367
1368
  /**
1369
   * You can set the default result-type to Result::RESULT_TYPE_*.
1370
   *
1371
   * INFO: used for "fetch()" and "fetchAll()"
1372
   *
1373
   * @param string $default_result_type
1374
   */
1375 4
  public function setDefaultResultType(string $default_result_type = self::RESULT_TYPE_OBJECT)
1376
  {
1377
    if (
1378 4
        $default_result_type === self::RESULT_TYPE_OBJECT
1379
        ||
1380 4
        $default_result_type === self::RESULT_TYPE_ARRAY
1381
        ||
1382
        $default_result_type === self::RESULT_TYPE_ARRAYY
1383
        ||
1384 4
        $default_result_type === self::RESULT_TYPE_YIELD
1385
    ) {
1386 4
      $this->_default_result_type = $default_result_type;
1387
    }
1388 4
  }
1389
1390
  /**
1391
   * @param int      $offset
1392
   * @param null|int $length
1393
   * @param bool     $preserve_keys
1394
   *
1395
   * @return array
1396
   */
1397 1
  public function slice(int $offset = 0, int $length = null, bool $preserve_keys = false): array
1398
  {
1399
    // init
1400 1
    $slice = [];
1401
1402 1
    if ($offset < 0) {
1403 1
      if (\abs($offset) > $this->num_rows) {
1404 1
        $offset = 0;
1405
      } else {
1406 1
        $offset = $this->num_rows - (int)\abs($offset);
1407
      }
1408
    }
1409
1410 1
    $length = null !== $length ? (int)$length : $this->num_rows;
1411 1
    $n = 0;
1412 1
    for ($i = $offset; $i < $this->num_rows && $n < $length; $i++) {
1413 1
      if ($preserve_keys) {
1414 1
        $slice[$i] = $this->fetchCallable($i);
1415
      } else {
1416 1
        $slice[] = $this->fetchCallable($i);
1417
      }
1418 1
      ++$n;
1419
    }
1420
1421 1
    return $slice;
1422
  }
1423
}
1424