Completed
Push — master ( 4e98a1...a4a40b )
by Lars
05:38 queued 10s
created

Result::__destruct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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(2, 0, $row); // FETCH_ASSOC + FETCH_ORI_NEXT
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 20
  public function fetchAllArray(): array
413
  {
414 20
    if ($this->is_empty()) {
415
      return [];
416
    }
417
418 20
    $this->reset();
419
420 20
    $data = [];
421
    /** @noinspection PhpAssignmentInConditionInspection */
422 20
    while ($row = $this->fetch_assoc()) {
423 20
      $data[] = $this->cast($row);
424
    }
425
426 20
    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
      /** @noinspection ClassConstantCanBeUsedInspection */
489 6
      $class = '\stdClass';
490
    }
491
492
    // init
493 13
    $data = [];
494 13
    $this->reset();
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 13
    $propertyAccessor = PropertyAccess::createPropertyAccessor();
512
    /** @noinspection PhpAssignmentInConditionInspection */
513 13 View Code Duplication
    while ($row = $this->fetch_assoc()) {
514 13
      $classTmp = clone $classTmpOrig;
515 13
      $row = $this->cast($row);
516 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...
517 13
        if ($class === '\stdClass') {
518 6
          $classTmp->{$key} = $value;
519
        } else {
520 13
          $propertyAccessor->setValue($classTmp, $key, $value);
521
        }
522
      }
523 13
      $data[] = $classTmp;
524
    }
525
526 13
    return $data;
527
  }
528
529
  /**
530
   * Fetch all results as "\Generator" via yield.
531
   *
532
   * @param object|string $class  <p>
533
   *                              <strong>string</strong>: create a new object (with optional constructor
534
   *                              parameter)<br>
535
   *                              <strong>object</strong>: use a object and fill the the data into
536
   *                              </p>
537
   * @param null|array    $params optional
538
   *                              <p>
539
   *                              An array of parameters to pass to the constructor, used if $class is a
540
   *                              string.
541
   *                              </p>
542
   *
543
   * @return \Generator
544
   */
545 6
  public function fetchAllYield($class = '', array $params = null): \Generator
546
  {
547 6
    if ($this->is_empty()) {
548
      return;
549
    }
550
551
    // init
552 6
    $this->reset();
553
554
    // fallback
555 6
    if (!$class || $class === 'stdClass') {
556
      /** @noinspection ClassConstantCanBeUsedInspection */
557 6
      $class = '\stdClass';
558
    }
559
560 6
    if (\is_object($class)) {
561
562
      $classTmpOrig = $class;
563
564 6
    } elseif ($class && $params) {
565
566
      $reflectorTmp = new \ReflectionClass($class);
567
      $classTmpOrig = $reflectorTmp->newInstanceArgs($params);
568
569
    } else {
570
571 6
      $classTmpOrig = new $class;
572
573
    }
574
575 6
    $propertyAccessor = PropertyAccess::createPropertyAccessor();
576
    /** @noinspection PhpAssignmentInConditionInspection */
577 6 View Code Duplication
    while ($row = $this->fetch_assoc()) {
578 6
      $classTmp = clone $classTmpOrig;
579 6
      $row = $this->cast($row);
580 6
      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...
581 6
        if ($class === '\stdClass') {
582 6
          $classTmp->{$key} = $value;
583
        } else {
584 6
          $propertyAccessor->setValue($classTmp, $key, $value);
585
        }
586
      }
587 6
      yield $classTmp;
588
    }
589 6
  }
590
591
  /**
592
   * Fetch as array.
593
   *
594
   * @param bool $reset
595
   *
596
   * @return array|false <p><strong>false</strong> on error</p>
597
   */
598 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...
599
  {
600 23
    if ($reset === true) {
601 2
      $this->reset();
602
    }
603
604 23
    $row = $this->fetch_assoc();
605 23
    if ($row) {
606 21
      return $this->cast($row);
607
    }
608
609 4
    if ($row === null || $row === false) {
610 4
      return [];
611
    }
612
613
    return false;
614
  }
615
616
  /**
617
   * Fetch data as a key/value pair array.
618
   *
619
   * <p>
620
   *   <br />
621
   *   INFO: both "key" and "value" must exists in the fetched data
622
   *   the key will be the new key of the result-array
623
   *   <br /><br />
624
   * </p>
625
   *
626
   * e.g.:
627
   * <code>
628
   *    fetchArrayPair('some_id', 'some_value');
629
   *    // array(127 => 'some value', 128 => 'some other value')
630
   * </code>
631
   *
632
   * @param string $key
633
   * @param string $value
634
   *
635
   * @return array
636
   */
637 2
  public function fetchArrayPair(string $key, string $value): array
638
  {
639 2
    $arrayPair = [];
640 2
    $data = $this->fetchAllArray();
641
642 2
    foreach ($data as &$_row) {
643
      if (
644 2
          \array_key_exists($key, $_row) === true
645
          &&
646 2
          \array_key_exists($value, $_row) === true
647
      ) {
648 2
        $_key = $_row[$key];
649 2
        $_value = $_row[$value];
650 2
        $arrayPair[$_key] = $_value;
651
      }
652
    }
653
654 2
    return $arrayPair;
655
  }
656
657
  /**
658
   * Fetch as "Arrayy"-object.
659
   *
660
   * @param bool $reset optional <p>Reset the \mysqli_result counter.</p>
661
   *
662
   * @return Arrayy|false <p><strong>false</strong> on error</p>
663
   */
664 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...
665
  {
666 4
    if ($reset === true) {
667
      $this->reset();
668
    }
669
670 4
    $row = $this->fetch_assoc();
671 4
    if ($row) {
672 2
      return Arrayy::create($this->cast($row));
673
    }
674
675 2
    if ($row === null || $row === false) {
676 2
      return Arrayy::create();
677
    }
678
679
    return false;
680
  }
681
682
  /**
683
   * Fetches a row or a single column within a row. Returns null if there are
684
   * no more rows in the result.
685
   *
686
   * @param int    $row    The row number (optional)
687
   * @param string $column The column name (optional)
688
   *
689
   * @return mixed An associative array or a scalar value
690
   */
691 16
  public function fetchCallable(int $row = null, string $column = null)
692
  {
693 16
    if (!$this->num_rows) {
694 2
      return null;
695
    }
696
697 14
    if (null !== $row) {
698 13
      $this->seek($row);
699
    }
700
701 14
    $rows = $this->fetch_assoc();
702
703 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...
704 5
      return \is_array($rows) && isset($rows[$column]) ? $rows[$column] : null;
705
    }
706
707 13
    return \is_callable($this->_mapper) ? \call_user_func($this->_mapper, $rows) : $rows;
708
  }
709
710
  /**
711
   * Fetch a single column as string (or as 1-dimension array).
712
   *
713
   * @param string $column
714
   * @param bool   $skipNullValues <p>Skip "NULL"-values. | default: true</p>
715
   * @param bool   $asArray        <p>Get all values and not only the last one. | default: false</p>
716
   *
717
   * @return string|array <p>Return a empty string or an empty array if the "$column" wasn't found, depend on
718
   *                      "$asArray"</p>
719
   */
720 7
  public function fetchColumn(string $column = '', bool $skipNullValues = true, bool $asArray = false)
721
  {
722 7
    if ($asArray === false) {
723 5
      $columnData = '';
724
725 5
      $data = $this->fetchAllArrayy()->reverse();
726 5 View Code Duplication
      foreach ($data as $_row) {
727
728 5
        if ($skipNullValues === true) {
729 5
          if (isset($_row[$column]) === false) {
730 5
            continue;
731
          }
732 2
        } else if (\array_key_exists($column, $_row) === false) {
733 2
          break;
734
        }
735
736 5
        $columnData = $_row[$column];
737 5
        break;
738
      }
739
740 5
      return $columnData;
741
    }
742
743
    // -- return as array -->
744
745 4
    $columnData = [];
746
747 4 View Code Duplication
    foreach ($this->fetchAllYield() as $_row) {
748
749 4
      if ($skipNullValues === true) {
750 2
        if (isset($_row->{$column}) === false) {
751 2
          continue;
752
        }
753 4
      } else if (\array_key_exists($column, $_row) === false) {
754 2
        break;
755
      }
756
757 4
      $columnData[] = $_row->{$column};
758
    }
759
760 4
    return $columnData;
761
  }
762
763
  /**
764
   * Return rows of field information in a result set.
765
   *
766
   * @param bool $as_array Return each field info as array; defaults to false
767
   *
768
   * @return array Array of field information each as an associative array
769
   */
770 1
  public function fetchFields(bool $as_array = false): array
771
  {
772 1
    if ($as_array) {
773 1
      return \array_map(
774 1
          function ($object) {
775 1
            return (array)$object;
776 1
          },
777 1
          $this->fetch_fields()
778
      );
779
    }
780
781
    return $this->fetch_fields();
782
  }
783
784
  /**
785
   * Returns all rows at once as a grouped array of scalar values or arrays.
786
   *
787
   * @param string $group  The column name to use for grouping
788
   * @param string $column The column name to use as values (optional)
789
   *
790
   * @return array A grouped array of scalar values or arrays
791
   */
792 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...
793
  {
794
    // init
795 1
    $groups = [];
796 1
    $pos = $this->current_row;
797
798 1
    foreach ($this as $row) {
799
800 1
      if (!\array_key_exists($group, $row)) {
801
        continue;
802
      }
803
804 1
      if (null !== $column) {
805
806 1
        if (!\array_key_exists($column, $row)) {
807
          continue;
808
        }
809
810 1
        $groups[$row[$group]][] = $row[$column];
811
      } else {
812 1
        $groups[$row[$group]][] = $row;
813
      }
814
    }
815
816 1
    $this->rewind($pos);
817
818 1
    return $groups;
819
  }
820
821
  /**
822
   * Fetch as object.
823
   *
824
   * @param object|string $class  <p>
825
   *                              <strong>string</strong>: create a new object (with optional constructor
826
   *                              parameter)<br>
827
   *                              <strong>object</strong>: use a object and fill the the data into
828
   *                              </p>
829
   * @param null|array    $params optional
830
   *                              <p>
831
   *                              An array of parameters to pass to the constructor, used if $class is a
832
   *                              string.
833
   *                              </p>
834
   * @param bool          $reset  optional <p>Reset the \mysqli_result counter.</p>
835
   *
836
   * @return object|false <p><strong>false</strong> on error</p>
837
   */
838 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...
839
  {
840 25
    if ($reset === true) {
841 17
      $this->reset();
842
    }
843
844
    // fallback
845 25
    if (!$class || $class === 'stdClass') {
846
      /** @noinspection ClassConstantCanBeUsedInspection */
847 14
      $class = '\stdClass';
848
    }
849
850 25
    $row = $this->fetch_assoc();
851 25
    $row = $row ? $this->cast($row) : false;
852
853 25
    if (!$row) {
854 5
      return false;
855
    }
856
857 23
    if (\is_object($class)) {
858
859 11
      $classTmp = $class;
860
861 14
    } elseif ($class && $params) {
862
863 2
      $reflectorTmp = new \ReflectionClass($class);
864 2
      $classTmp = $reflectorTmp->newInstanceArgs($params);
865
866
    } else {
867
868 14
      $classTmp = new $class;
869
870
    }
871
872 23
    $propertyAccessor = PropertyAccess::createPropertyAccessor();
873 23
    foreach ($row as $key => $value) {
874 23
      if ($class === '\stdClass') {
875 14
        $classTmp->{$key} = $value;
876
      } else {
877 23
        $propertyAccessor->setValue($classTmp, $key, $value);
878
      }
879
    }
880
881 23
    return $classTmp;
882
  }
883
884
  /**
885
   * Returns all rows at once as key-value pairs.
886
   *
887
   * @param string $key    The column name to use as keys
888
   * @param string $column The column name to use as values (optional)
889
   *
890
   * @return array An array of key-value pairs
891
   */
892 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...
893
  {
894
    // init
895 1
    $pairs = [];
896 1
    $pos = $this->current_row;
897
898 1
    foreach ($this as $row) {
899
900 1
      if (!\array_key_exists($key, $row)) {
901
        continue;
902
      }
903
904 1
      if (null !== $column) {
905
906 1
        if (!\array_key_exists($column, $row)) {
907
          continue;
908
        }
909
910 1
        $pairs[$row[$key]] = $row[$column];
911
      } else {
912 1
        $pairs[$row[$key]] = $row;
913
      }
914
    }
915
916 1
    $this->rewind($pos);
917
918 1
    return $pairs;
919
  }
920
921
  /**
922
   * Returns all rows at once, transposed as an array of arrays. Instead of
923
   * returning rows of columns, this method returns columns of rows.
924
   *
925
   * @param string $column The column name to use as keys (optional)
926
   *
927
   * @return mixed A transposed array of arrays
928
   */
929 1
  public function fetchTranspose(string $column = null)
930
  {
931
    // init
932 1
    $keys = null !== $column ? $this->fetchAllColumn($column) : [];
933 1
    $rows = [];
934 1
    $pos = $this->current_row;
935
936 1
    foreach ($this as $row) {
937 1
      foreach ($row as $key => $value) {
938 1
        $rows[$key][] = $value;
939
      }
940
    }
941
942 1
    $this->rewind($pos);
943
944 1
    if (empty($keys)) {
945 1
      return $rows;
946
    }
947
948 1
    return \array_map(
949 1
        function ($values) use ($keys) {
950 1
          return \array_combine($keys, $values);
951 1
        }, $rows
952
    );
953
  }
954
955
  /**
956
   * Fetch as "\Generator" via yield.
957
   *
958
   * @param object|string $class  <p>
959
   *                              <strong>string</strong>: create a new object (with optional constructor
960
   *                              parameter)<br>
961
   *                              <strong>object</strong>: use a object and fill the the data into
962
   *                              </p>
963
   * @param null|array    $params optional
964
   *                              <p>
965
   *                              An array of parameters to pass to the constructor, used if $class is a
966
   *                              string.
967
   *                              </p>
968
   * @param bool          $reset  optional <p>Reset the \mysqli_result counter.</p>
969
   *
970
   * @return \Generator
971
   */
972 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...
973
  {
974 2
    if ($reset === true) {
975
      $this->reset();
976
    }
977
978
    // fallback
979 2
    if (!$class || $class === 'stdClass') {
980
      /** @noinspection ClassConstantCanBeUsedInspection */
981 2
      $class = '\stdClass';
982
    }
983
984 2
    if (\is_object($class)) {
985
986
      $classTmp = $class;
987
988 2
    } elseif ($class && $params) {
989
990
      $reflectorTmp = new \ReflectionClass($class);
991
      $classTmp = $reflectorTmp->newInstanceArgs($params);
992
993
    } else {
994
995 2
      $classTmp = new $class;
996
997
    }
998
999 2
    $row = $this->fetch_assoc();
1000 2
    $row = $row ? $this->cast($row) : false;
1001
1002 2
    if (!$row) {
1003
      return;
1004
    }
1005
1006 2
    $propertyAccessor = PropertyAccess::createPropertyAccessor();
1007 2
    foreach ($row as $key => $value) {
1008 2
      if ($class === '\stdClass') {
1009 2
        $classTmp->{$key} = $value;
1010
      } else {
1011 2
        $propertyAccessor->setValue($classTmp, $key, $value);
1012
      }
1013
    }
1014
1015 2
    yield $classTmp;
1016 2
  }
1017
1018
  /**
1019
   * @return mixed
1020
   */
1021 76
  private function fetch_assoc()
1022
  {
1023 76
    if ($this->_result instanceof \Doctrine\DBAL\Statement) {
1024 15
      $this->_result->setFetchMode(2); // FETCH_ASSOC
1025 15
      $object = $this->_result->fetch();
1026
1027 15
      return $object;
1028
    }
1029
1030 61
    return mysqli_fetch_assoc($this->_result);
1031
  }
1032
1033
  /**
1034
   * @return array|bool
1035
   */
1036 1
  private function fetch_fields()
1037
  {
1038 1
    if ($this->_result instanceof \mysqli_result) {
1039 1
      return \mysqli_fetch_fields($this->_result);
1040
    }
1041
1042
    if ($this->doctrineMySQLiStmt) {
1043
      $metadataTmp = $this->doctrineMySQLiStmt->result_metadata();
1044
1045
      return $metadataTmp->fetch_fields();
1046
    }
1047
1048
    if ($this->doctrinePdoStmt) {
1049
      $fields = [];
1050
1051
      static $THIS_CLASS_TMP = null;
1052
      if ($THIS_CLASS_TMP === null) {
1053
        $THIS_CLASS_TMP = new \ReflectionClass(__CLASS__);
1054
      }
1055
1056
      $totalColumnsTmp = $this->doctrinePdoStmt->columnCount();
1057
      for ($counterTmp = 0; $counterTmp < $totalColumnsTmp; $counterTmp++) {
1058
        $metadataTmp = $this->doctrinePdoStmt->getColumnMeta($counterTmp);
1059
        $fieldTmp = new \stdClass();
1060
        foreach ($metadataTmp as $metadataTmpKey => $metadataTmpValue) {
1061
          $fieldTmp->{$metadataTmpKey} = $metadataTmpValue;
1062
        }
1063
1064
        $typeNativeTmp = 'MYSQL_TYPE_' . $metadataTmp['native_type'];
1065
        $typeTmp = $THIS_CLASS_TMP->getConstant($typeNativeTmp);
1066
        if ($typeTmp) {
1067
          $fieldTmp->type = $typeTmp;
1068
        } else {
1069
          $fieldTmp->type = '';
1070
        }
1071
1072
        $fields[] = $fieldTmp;
1073
      }
1074
1075
      return $fields;
1076
    }
1077
1078
    return false;
1079
  }
1080
1081
  /**
1082
   * Returns the first row element from the result.
1083
   *
1084
   * @param string $column The column name to use as value (optional)
1085
   *
1086
   * @return mixed A row array or a single scalar value
1087
   */
1088 3
  public function first(string $column = null)
1089
  {
1090 3
    $pos = $this->current_row;
1091 3
    $first = $this->fetchCallable(0, $column);
1092 3
    $this->rewind($pos);
1093
1094 3
    return $first;
1095
  }
1096
1097
  /**
1098
   * free the memory
1099
   */
1100 90
  public function free()
1101
  {
1102 90
    if ($this->_result instanceof \mysqli_result) {
1103
      /** @noinspection PhpUsageOfSilenceOperatorInspection */
1104 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...
1105 66
      $this->_result = null;
1106
1107 66
      return true;
1108
    }
1109
1110 25
    $this->_result = null;
1111
1112 25
    return false;
1113
  }
1114
1115
  /**
1116
   * alias for "Result->fetch()"
1117
   *
1118
   * @see Result::fetch()
1119
   *
1120
   * @return array|object|false <p><strong>false</strong> on error</p>
1121
   */
1122 2
  public function get()
1123
  {
1124 2
    return $this->fetch();
1125
  }
1126
1127
  /**
1128
   * alias for "Result->fetchAll()"
1129
   *
1130
   * @see Result::fetchAll()
1131
   *
1132
   * @return array
1133
   */
1134 2
  public function getAll(): array
1135
  {
1136 2
    return $this->fetchAll();
1137
  }
1138
1139
  /**
1140
   * alias for "Result->fetchAllColumn()"
1141
   *
1142
   * @see Result::fetchAllColumn()
1143
   *
1144
   * @param string $column
1145
   * @param bool   $skipNullValues
1146
   *
1147
   * @return array
1148
   */
1149
  public function getAllColumn(string $column, bool $skipNullValues = false): array
1150
  {
1151
    return $this->fetchAllColumn($column, $skipNullValues);
1152
  }
1153
1154
  /**
1155
   * alias for "Result->fetchAllArray()"
1156
   *
1157
   * @see Result::fetchAllArray()
1158
   *
1159
   * @return array
1160
   */
1161 2
  public function getArray(): array
1162
  {
1163 2
    return $this->fetchAllArray();
1164
  }
1165
1166
  /**
1167
   * alias for "Result->fetchAllArrayy()"
1168
   *
1169
   * @see Result::fetchAllArrayy()
1170
   *
1171
   * @return Arrayy
1172
   */
1173
  public function getArrayy(): Arrayy
1174
  {
1175
    return $this->fetchAllArrayy();
1176
  }
1177
1178
  /**
1179
   * alias for "Result->fetchColumn()"
1180
   *
1181
   * @see Result::fetchColumn()
1182
   *
1183
   * @param string $column
1184
   * @param bool   $asArray
1185
   * @param bool   $skipNullValues
1186
   *
1187
   * @return string|array <p>Return a empty string or an empty array if the "$column" wasn't found, depend on
1188
   *                      "$asArray"</p>
1189
   */
1190 2
  public function getColumn(string $column, bool $skipNullValues = true, bool $asArray = false)
1191
  {
1192 2
    return $this->fetchColumn($column, $skipNullValues, $asArray);
1193
  }
1194
1195
  /**
1196
   * @return string
1197
   */
1198 2
  public function getDefaultResultType(): string
1199
  {
1200 2
    return $this->_default_result_type;
1201
  }
1202
1203
  /**
1204
   * alias for "Result->fetchAllObject()"
1205
   *
1206
   * @see Result::fetchAllObject()
1207
   *
1208
   * @return array of mysql-objects
1209
   */
1210 2
  public function getObject(): array
1211
  {
1212 2
    return $this->fetchAllObject();
1213
  }
1214
1215
  /**
1216
   * alias for "Result->fetchAllYield()"
1217
   *
1218
   * @see Result::fetchAllYield()
1219
   *
1220
   * @param bool $asArray
1221
   *
1222
   * @return \Generator
1223
   */
1224
  public function getYield($asArray = false): \Generator
1225
  {
1226
    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...
1227
  }
1228
1229
  /**
1230
   * Check if the result is empty.
1231
   *
1232
   * @return bool
1233
   */
1234 46
  public function is_empty(): bool
1235
  {
1236 46
    return !($this->num_rows > 0);
1237
  }
1238
1239
  /**
1240
   * Fetch all results as "json"-string.
1241
   *
1242
   * @return string
1243
   */
1244 2
  public function json(): string
1245
  {
1246 2
    $data = $this->fetchAllArray();
1247
1248 2
    return UTF8::json_encode($data);
1249
  }
1250
1251
  /**
1252
   * Returns the last row element from the result.
1253
   *
1254
   * @param string $column The column name to use as value (optional)
1255
   *
1256
   * @return mixed A row array or a single scalar value
1257
   */
1258 3
  public function last(string $column = null)
1259
  {
1260 3
    $pos = $this->current_row;
1261 3
    $last = $this->fetchCallable($this->num_rows - 1, $column);
1262 3
    $this->rewind($pos);
1263
1264 3
    return $last;
1265
  }
1266
1267
  /**
1268
   * Set the mapper...
1269
   *
1270
   * @param \Closure $callable
1271
   *
1272
   * @return $this
1273
   */
1274 1
  public function map(\Closure $callable): self
1275
  {
1276 1
    $this->_mapper = $callable;
1277
1278 1
    return $this;
1279
  }
1280
1281
  /**
1282
   * Alias of count(). Deprecated.
1283
   *
1284
   * @return int The number of rows in the result
1285
   */
1286 1
  public function num_rows(): int
1287
  {
1288 1
    return $this->count();
1289
  }
1290
1291
  /**
1292
   * ArrayAccess interface implementation.
1293
   *
1294
   * @param int $offset <p>Offset number</p>
1295
   *
1296
   * @return bool <p>true if offset exists, false otherwise</p>
1297
   */
1298 1
  public function offsetExists($offset): bool
1299
  {
1300 1
    return \is_int($offset) && $offset >= 0 && $offset < $this->num_rows;
1301
  }
1302
1303
  /**
1304
   * ArrayAccess interface implementation.
1305
   *
1306
   * @param int $offset Offset number
1307
   *
1308
   * @return mixed
1309
   */
1310 1
  public function offsetGet($offset)
1311
  {
1312 1
    if ($this->offsetExists($offset)) {
1313 1
      return $this->fetchCallable($offset);
1314
    }
1315
1316
    throw new \OutOfBoundsException("undefined offset ($offset)");
1317
  }
1318
1319
  /**
1320
   * ArrayAccess interface implementation. Not implemented by design.
1321
   *
1322
   * @param mixed $offset
1323
   * @param mixed $value
1324
   */
1325
  public function offsetSet($offset, $value)
1326
  {
1327
    /** @noinspection UselessReturnInspection */
1328
    return;
1329
  }
1330
1331
  /**
1332
   * ArrayAccess interface implementation. Not implemented by design.
1333
   *
1334
   * @param mixed $offset
1335
   */
1336
  public function offsetUnset($offset)
1337
  {
1338
    /** @noinspection UselessReturnInspection */
1339
    return;
1340
  }
1341
1342
  /**
1343
   * Reset the offset (data_seek) for the results.
1344
   *
1345
   * @return Result
1346
   */
1347 43
  public function reset(): self
1348
  {
1349 43
    if (!$this->is_empty()) {
1350
1351 41
      if ($this->doctrineMySQLiStmt) {
1352 9
        $this->doctrineMySQLiStmt->data_seek(0);
1353
      }
1354
1355 41
      if ($this->_result instanceof \mysqli_result) {
1356 32
        \mysqli_data_seek($this->_result, 0);
1357
      }
1358
    }
1359
1360 43
    return $this;
1361
  }
1362
1363
  /**
1364
   * You can set the default result-type to Result::RESULT_TYPE_*.
1365
   *
1366
   * INFO: used for "fetch()" and "fetchAll()"
1367
   *
1368
   * @param string $default_result_type
1369
   */
1370 4
  public function setDefaultResultType(string $default_result_type = self::RESULT_TYPE_OBJECT)
1371
  {
1372
    if (
1373 4
        $default_result_type === self::RESULT_TYPE_OBJECT
1374
        ||
1375 4
        $default_result_type === self::RESULT_TYPE_ARRAY
1376
        ||
1377
        $default_result_type === self::RESULT_TYPE_ARRAYY
1378
        ||
1379 4
        $default_result_type === self::RESULT_TYPE_YIELD
1380
    ) {
1381 4
      $this->_default_result_type = $default_result_type;
1382
    }
1383 4
  }
1384
1385
  /**
1386
   * @param int      $offset
1387
   * @param null|int $length
1388
   * @param bool     $preserve_keys
1389
   *
1390
   * @return array
1391
   */
1392 1
  public function slice(int $offset = 0, int $length = null, bool $preserve_keys = false): array
1393
  {
1394
    // init
1395 1
    $slice = [];
1396
1397 1
    if ($offset < 0) {
1398 1
      if (\abs($offset) > $this->num_rows) {
1399 1
        $offset = 0;
1400
      } else {
1401 1
        $offset = $this->num_rows - (int)\abs($offset);
1402
      }
1403
    }
1404
1405 1
    $length = null !== $length ? (int)$length : $this->num_rows;
1406 1
    $n = 0;
1407 1
    for ($i = $offset; $i < $this->num_rows && $n < $length; $i++) {
1408 1
      if ($preserve_keys) {
1409 1
        $slice[$i] = $this->fetchCallable($i);
1410
      } else {
1411 1
        $slice[] = $this->fetchCallable($i);
1412
      }
1413 1
      ++$n;
1414
    }
1415
1416 1
    return $slice;
1417
  }
1418
}
1419