• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dg / dibi / 22282875037

22 Feb 2026 06:32PM UTC coverage: 76.552% (-0.5%) from 77.004%
22282875037

push

github

dg
phpstan

55 of 96 new or added lines in 23 files covered. (57.29%)

1 existing line in 1 file now uncovered.

1763 of 2303 relevant lines covered (76.55%)

0.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.35
/src/Dibi/Result.php
1
<?php declare(strict_types=1);
1✔
2

3
/**
4
 * This file is part of the Dibi, smart database abstraction layer (https://dibi.nette.org)
5
 * Copyright (c) 2005 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Dibi;
9

10
use function array_keys, array_pop, count, explode, is_float, is_string, json_decode, ltrim, preg_match, preg_split, property_exists, reset, rtrim, str_contains, str_replace, str_starts_with, strpos;
11
use const PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_NO_EMPTY;
12

13

14
/**
15
 * Query result.
16
 *
17
 * @property-read int $rowCount
18
 */
19
class Result implements IDataSource
20
{
21
        private ?ResultDriver $driver;
22

23
        /** @var array<?string>  column name => Type constant */
24
        private array $types = [];
25
        private ?Reflection\Result $meta;
26

27
        /** Already fetched? Used for allowance for first seek(0) */
28
        private bool $fetched = false;
29

30
        /** @var ?class-string<Row> */
31
        private ?string $rowClass = Row::class;
32

33
        /** @var ?\Closure(mixed[]): mixed */
34
        private ?\Closure $rowFactory = null;
35

36
        /** @var array<string, ?string>  Type constant => format string */
37
        private array $formats = [];
38

39

40
        public function __construct(ResultDriver $driver, bool $normalize = true)
1✔
41
        {
42
                $this->driver = $driver;
1✔
43
                if ($normalize) {
1✔
44
                        $this->detectTypes();
1✔
45
                }
46
        }
1✔
47

48

49
        /**
50
         * Frees the resources allocated for this result set.
51
         */
52
        final public function free(): void
53
        {
54
                if ($this->driver !== null) {
×
55
                        $this->driver->free();
×
56
                        $this->driver = $this->meta = null;
×
57
                }
58
        }
59

60

61
        /**
62
         * Safe access to property $driver.
63
         * @throws \RuntimeException
64
         */
65
        final public function getResultDriver(): ResultDriver
66
        {
67
                if ($this->driver === null) {
1✔
68
                        throw new \RuntimeException('Result-set was released from memory.');
×
69
                }
70

71
                return $this->driver;
1✔
72
        }
73

74

75
        /********************* rows ****************d*g**/
76

77

78
        /**
79
         * Moves cursor position without fetching row.
80
         * @throws Exception
81
         */
82
        final public function seek(int $row): bool
1✔
83
        {
84
                return ($row !== 0 || $this->fetched)
1✔
85
                        ? $this->getResultDriver()->seek($row)
×
86
                        : true;
1✔
87
        }
88

89

90
        /**
91
         * Required by the Countable interface.
92
         */
93
        final public function count(): int
94
        {
NEW
95
                return max(0, $this->getResultDriver()->getRowCount());
×
96
        }
97

98

99
        /**
100
         * Returns the number of rows in a result set.
101
         */
102
        final public function getRowCount(): int
103
        {
104
                return $this->getResultDriver()->getRowCount();
1✔
105
        }
106

107

108
        /**
109
         * Required by the IteratorAggregate interface.
110
         */
111
        final public function getIterator(): ResultIterator
112
        {
113
                return new ResultIterator($this);
1✔
114
        }
115

116

117
        /**
118
         * Returns the number of columns in a result set.
119
         */
120
        final public function getColumnCount(): int
121
        {
122
                return count($this->types);
1✔
123
        }
124

125

126
        /********************* fetching rows ****************d*g**/
127

128

129
        /**
130
         * Set fetched object class. This class should extend the Row class.
131
         * @param ?class-string<Row>  $class
132
         */
133
        public function setRowClass(?string $class): static
1✔
134
        {
135
                $this->rowClass = $class;
1✔
136
                return $this;
1✔
137
        }
138

139

140
        /**
141
         * Returns fetched object class name.
142
         * @return ?class-string<Row>
143
         */
144
        public function getRowClass(): ?string
145
        {
146
                return $this->rowClass;
×
147
        }
148

149

150
        /**
151
         * Set a factory to create fetched object instances. These should extend the Row class.
152
         * @param  callable(mixed[]): mixed  $callback
153
         */
154
        public function setRowFactory(callable $callback): static
155
        {
156
                $this->rowFactory = $callback(...);
×
157
                return $this;
×
158
        }
159

160

161
        /**
162
         * Fetches the row at current position, process optional type conversion.
163
         * and moves the internal cursor to the next position
164
         */
165
        final public function fetch(): mixed
166
        {
167
                $row = $this->getResultDriver()->fetch(true);
1✔
168
                if ($row === null) {
1✔
169
                        return null;
1✔
170
                }
171

172
                $this->fetched = true;
1✔
173
                $this->normalize($row);
1✔
174
                if ($this->rowFactory) {
1✔
175
                        return ($this->rowFactory)($row);
×
176
                } elseif ($this->rowClass) {
1✔
177
                        return new $this->rowClass($row);
1✔
178
                }
179

180
                return $row;
1✔
181
        }
182

183

184
        /**
185
         * Like fetch(), but returns only first field.
186
         * Returns value on success, null if no next record
187
         */
188
        final public function fetchSingle(): mixed
189
        {
190
                $row = $this->getResultDriver()->fetch(true);
1✔
191
                if ($row === null) {
1✔
192
                        return null;
1✔
193
                }
194

195
                $this->fetched = true;
1✔
196
                $this->normalize($row);
1✔
197
                return reset($row);
1✔
198
        }
199

200

201
        /**
202
         * Fetches all records from table.
203
         * @return list<Row|mixed[]>
204
         */
205
        final public function fetchAll(?int $offset = null, ?int $limit = null): array
1✔
206
        {
207
                $limit ??= -1;
1✔
208
                $this->seek($offset ?? 0);
1✔
209
                $row = $this->fetch();
1✔
210
                if (!$row) {
1✔
211
                        return [];  // empty result set
1✔
212
                }
213

214
                $data = [];
1✔
215
                do {
216
                        if ($limit === 0) {
1✔
217
                                break;
×
218
                        }
219

220
                        $limit--;
1✔
221
                        $data[] = $row;
1✔
222
                } while ($row = $this->fetch());
1✔
223

224
                return $data;
1✔
225
        }
226

227

228
        /**
229
         * Fetches all records from table and returns associative tree.
230
         * Examples:
231
         * - associative descriptor: col1[]col2->col3
232
         *   builds a tree:          $tree[$val1][$index][$val2]->col3[$val3] = {record}
233
         * - associative descriptor: col1|col2->col3=col4
234
         *   builds a tree:          $tree[$val1][$val2]->col3[$val3] = val4
235
         * @return array<mixed>
236
         * @throws \InvalidArgumentException
237
         */
238
        final public function fetchAssoc(string $assoc): array
1✔
239
        {
240
                if (str_contains($assoc, ',')) {
1✔
241
                        return $this->oldFetchAssoc($assoc);
1✔
242
                }
243

244
                $this->seek(0);
1✔
245
                $row = $this->fetch();
1✔
246
                if (!$row) {
1✔
247
                        return [];  // empty result set
×
248
                }
249

250
                $data = null;
1✔
251
                $origAssoc = $assoc;
1✔
252
                $assoc = preg_split('#(\[\]|->|=|\|)#', $assoc, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
1✔
253
                if (!$assoc) {
1✔
NEW
254
                        throw new \InvalidArgumentException("Invalid descriptor '$origAssoc'.");
×
255
                }
256

257
                // check columns
258
                foreach ($assoc as $as) {
1✔
259
                        // offsetExists ignores null in PHP 5.2.1, isset() surprisingly null accepts
260
                        if ($as !== '[]' && $as !== '=' && $as !== '->' && $as !== '|' && !property_exists($row, $as)) {
1✔
261
                                throw new \InvalidArgumentException("Unknown column '$as' in associative descriptor.");
×
262
                        }
263
                }
264

265
                if ($as === '->') { // must not be last
1✔
266
                        array_pop($assoc);
1✔
267
                }
268

269
                if (empty($assoc)) {
1✔
270
                        $assoc[] = '[]';
×
271
                }
272

273
                // make associative tree
274
                do {
275
                        $x = &$data;
1✔
276

277
                        // iterative deepening
278
                        foreach ($assoc as $i => $as) {
1✔
279
                                if ($as === '[]') { // indexed-array node
1✔
280
                                        $x = &$x[];
1✔
281

282
                                } elseif ($as === '=') { // "value" node
1✔
283
                                        $x = $row->{$assoc[$i + 1]};
1✔
284
                                        continue 2;
1✔
285

286
                                } elseif ($as === '->') { // "object" node
1✔
287
                                        if ($x === null) {
1✔
288
                                                $x = clone $row;
1✔
289
                                                $x = &$x->{$assoc[$i + 1]};
1✔
290
                                                $x = null; // prepare child node
1✔
291
                                        } else {
292
                                                $x = &$x->{$assoc[$i + 1]};
1✔
293
                                        }
294
                                } elseif ($as !== '|') { // associative-array node
1✔
295
                                        $x = &$x[(string) $row->$as];
1✔
296
                                }
297
                        }
298

299
                        if ($x === null) { // build leaf
1✔
300
                                $x = $row;
1✔
301
                        }
302
                } while ($row = $this->fetch());
1✔
303

304
                unset($x);
1✔
305
                return $data;
1✔
306
        }
307

308

309
        /**
310
         * @deprecated
311
         * @return array<mixed>
312
         */
313
        private function oldFetchAssoc(string $assoc): array
1✔
314
        {
315
                $this->seek(0);
1✔
316
                $row = $this->fetch();
1✔
317
                if (!$row) {
1✔
318
                        return [];  // empty result set
×
319
                }
320

321
                $data = null;
1✔
322
                $assoc = explode(',', $assoc);
1✔
323

324
                // strip leading = and @
325
                $leaf = '@';  // gap
1✔
326
                $last = count($assoc) - 1;
1✔
327
                while ($assoc[$last] === '=' || $assoc[$last] === '@') {
1✔
328
                        $leaf = $assoc[$last];
1✔
329
                        unset($assoc[$last]);
1✔
330
                        $last--;
1✔
331

332
                        if ($last < 0) {
1✔
333
                                $assoc[] = '#';
1✔
334
                                break;
1✔
335
                        }
336
                }
337

338
                do {
339
                        $x = &$data;
1✔
340

341
                        foreach ($assoc as $i => $as) {
1✔
342
                                if ($as === '#') { // indexed-array node
1✔
343
                                        $x = &$x[];
1✔
344

345
                                } elseif ($as === '=') { // "record" node
1✔
346
                                        if ($x === null) {
1✔
347
                                                $x = $row->toArray();
1✔
348
                                                $x = &$x[$assoc[$i + 1]];
1✔
349
                                                $x = null; // prepare child node
1✔
350
                                        } else {
351
                                                $x = &$x[$assoc[$i + 1]];
1✔
352
                                        }
353
                                } elseif ($as === '@') { // "object" node
1✔
354
                                        if ($x === null) {
1✔
355
                                                $x = clone $row;
1✔
356
                                                $x = &$x->{$assoc[$i + 1]};
1✔
357
                                                $x = null; // prepare child node
1✔
358
                                        } else {
359
                                                $x = &$x->{$assoc[$i + 1]};
1✔
360
                                        }
361
                                } else { // associative-array node
362
                                        $x = &$x[(string) $row->$as];
1✔
363
                                }
364
                        }
365

366
                        if ($x === null) { // build leaf
1✔
367
                                $x = $leaf === '='
1✔
368
                                        ? $row->toArray()
×
369
                                        : $row;
1✔
370
                        }
371
                } while ($row = $this->fetch());
1✔
372

373
                unset($x);
1✔
374
                return $data ?? [];
1✔
375
        }
376

377

378
        /**
379
         * Fetches all records from table like $key => $value pairs.
380
         * @return mixed[]
381
         * @throws \InvalidArgumentException
382
         */
383
        final public function fetchPairs(?string $key = null, ?string $value = null): array
1✔
384
        {
385
                $this->seek(0);
1✔
386
                $row = $this->fetch();
1✔
387
                if (!$row) {
1✔
388
                        return [];  // empty result set
×
389
                }
390

391
                $data = [];
1✔
392

393
                if ($value === null) {
1✔
394
                        if ($key !== null) {
1✔
395
                                throw new \InvalidArgumentException('Either none or both columns must be specified.');
×
396
                        }
397

398
                        // autodetect
399
                        $tmp = array_keys($row->toArray());
1✔
400
                        $key = $tmp[0];
1✔
401
                        if (count($row) < 2) { // indexed-array
1✔
402
                                do {
403
                                        $data[] = $row[$key];
×
404
                                } while ($row = $this->fetch());
×
405

406
                                return $data;
×
407
                        }
408

409
                        $value = $tmp[1];
1✔
410

411
                } else {
412
                        if (!property_exists($row, $value)) {
1✔
413
                                throw new \InvalidArgumentException("Unknown value column '$value'.");
×
414
                        }
415

416
                        if ($key === null) { // indexed-array
1✔
417
                                do {
418
                                        $data[] = $row[$value];
×
419
                                } while ($row = $this->fetch());
×
420

421
                                return $data;
×
422
                        }
423

424
                        if (!property_exists($row, $key)) {
1✔
425
                                throw new \InvalidArgumentException("Unknown key column '$key'.");
×
426
                        }
427
                }
428

429
                do {
430
                        $data[(string) $row[$key]] = $row[$value];
1✔
431
                } while ($row = $this->fetch());
1✔
432

433
                return $data;
1✔
434
        }
435

436

437
        /********************* column types ****************d*g**/
438

439

440
        /**
441
         * Autodetect column types.
442
         */
443
        private function detectTypes(): void
444
        {
445
                $cache = Helpers::getTypeCache();
1✔
446
                try {
447
                        foreach ($this->getResultDriver()->getResultColumns() as $col) {
1✔
448
                                $this->types[$col['name']] = $col['type'] ?? $cache->{$col['nativetype']};
1✔
449
                        }
450
                } catch (NotSupportedException) {
×
451
                }
452
        }
1✔
453

454

455
        /**
456
         * Converts values to specified type and format.
457
         * @param  mixed[]  $row
458
         */
459
        private function normalize(array &$row): void
1✔
460
        {
461
                foreach ($this->types as $key => $type) {
1✔
462
                        if (!isset($row[$key])) { // null
1✔
463
                                continue;
1✔
464
                        }
465

466
                        $value = $row[$key];
1✔
467
                        $format = $this->formats[$type ?? ''] ?? null;
1✔
468

469
                        if ($type === null || $format === 'native') {
1✔
470
                                $row[$key] = $value;
1✔
471

472
                        } elseif ($type === Type::Text) {
1✔
473
                                $row[$key] = (string) $value;
1✔
474

475
                        } elseif ($type === Type::Integer) {
1✔
476
                                $row[$key] = is_float($tmp = $value * 1)
1✔
477
                                        ? (is_string($value) ? $value : (int) $value)
1✔
478
                                        : $tmp;
1✔
479

480
                        } elseif ($type === Type::Float) {
1✔
481
                                if (!is_string($value)) {
1✔
482
                                        $row[$key] = (float) $value;
1✔
483
                                        continue;
1✔
484
                                }
485

486
                                $negative = ($value[0] ?? null) === '-';
1✔
487
                                $value = ltrim($value, '0-');
1✔
488
                                $p = strpos($value, '.');
1✔
489
                                $e = strpos($value, 'e');
1✔
490
                                if ($p !== false && $e === false) {
1✔
491
                                        $value = rtrim(rtrim($value, '0'), '.');
1✔
492
                                } elseif ($p !== false && $e !== false) {
1✔
493
                                        $value = rtrim($value, '.');
1✔
494
                                }
495

496
                                if ($value === '' || $value[0] === '.') {
1✔
497
                                        $value = '0' . $value;
1✔
498
                                }
499

500
                                if ($negative) {
1✔
501
                                        $value = '-' . $value;
1✔
502
                                }
503

504
                                $row[$key] = $value === str_replace(',', '.', (string) ($float = (float) $value))
1✔
505
                                        ? $float
1✔
506
                                        : $value;
1✔
507

508
                        } elseif ($type === Type::Bool) {
1✔
509
                                $row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F';
1✔
510

511
                        } elseif ($type === Type::DateTime || $type === Type::Date || $type === Type::Time) {
1✔
512
                                if ($value && !str_starts_with((string) $value, '0000-00')) { // '', null, false, '0000-00-00', ...
1✔
513
                                        $value = new DateTime($value);
1✔
514
                                        $row[$key] = $format ? $value->format($format) : $value;
1✔
515
                                } else {
516
                                        $row[$key] = null;
1✔
517
                                }
518
                        } elseif ($type === Type::TimeInterval) {
1✔
519
                                preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)\z#', $value, $m);
1✔
520
                                $value = new \DateInterval("PT$m[2]H$m[3]M$m[4]S");
1✔
521
                                $value->invert = (int) (bool) $m[1];
1✔
522
                                $row[$key] = $format ? $value->format($format) : $value;
1✔
523

524
                        } elseif ($type === Type::Binary) {
×
525
                                $row[$key] = is_string($value)
×
526
                                        ? $this->getResultDriver()->unescapeBinary($value)
×
527
                                        : $value;
×
528

529
                        } elseif ($type === Type::JSON) {
×
530
                                if ($format === 'string') { // back compatibility with 'native'
×
531
                                        $row[$key] = $value;
×
532
                                } else {
533
                                        $row[$key] = json_decode($value, $format === 'array');
×
534
                                }
535
                        } else {
536
                                throw new \RuntimeException('Unexpected type ' . $type);
×
537
                        }
538
                }
539
        }
1✔
540

541

542
        /**
543
         * Define column type.
544
         * @param  ?string  $type  use constant Type::*
545
         */
546
        final public function setType(string $column, ?string $type): static
1✔
547
        {
548
                $this->types[$column] = $type;
1✔
549
                return $this;
1✔
550
        }
551

552

553
        /**
554
         * Returns column type.
555
         */
556
        final public function getType(string $column): ?string
557
        {
558
                return $this->types[$column] ?? null;
×
559
        }
560

561

562
        /**
563
         * Returns columns type.
564
         * @return array<?string>
565
         */
566
        final public function getTypes(): array
567
        {
568
                return $this->types;
×
569
        }
570

571

572
        /**
573
         * Sets type format.
574
         */
575
        final public function setFormat(string $type, ?string $format): static
1✔
576
        {
577
                $this->formats[$type] = $format;
1✔
578
                return $this;
1✔
579
        }
580

581

582
        /**
583
         * Sets type formats.
584
         * @param  array<string, ?string>  $formats
585
         */
586
        final public function setFormats(array $formats): static
1✔
587
        {
588
                $this->formats = $formats;
1✔
589
                return $this;
1✔
590
        }
591

592

593
        /**
594
         * Returns data format.
595
         */
596
        final public function getFormat(string $type): ?string
597
        {
598
                return $this->formats[$type] ?? null;
×
599
        }
600

601

602
        /********************* meta info ****************d*g**/
603

604

605
        /**
606
         * Returns a meta information about the current result set.
607
         */
608
        public function getInfo(): Reflection\Result
609
        {
610
                if (!isset($this->meta)) {
1✔
611
                        $this->meta = new Reflection\Result($this->getResultDriver());
1✔
612
                }
613

614
                return $this->meta;
1✔
615
        }
616

617

618
        /** @return Reflection\Column[] */
619
        final public function getColumns(): array
620
        {
621
                return $this->getInfo()->getColumns();
×
622
        }
623

624

625
        /********************* misc tools ****************d*g**/
626

627

628
        /**
629
         * Displays complete result set as HTML or text table for debug purposes.
630
         */
631
        final public function dump(): void
632
        {
633
                echo Helpers::dump($this);
×
634
        }
635
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc