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

codeigniter4 / CodeIgniter4 / 26978559516

04 Jun 2026 08:46PM UTC coverage: 88.238%. First build
26978559516

Pull #10257

github

web-flow
Merge 86a545a91 into d5c27fb92
Pull Request #10257: fix(database): use currentRow instead of customResultObject in getRowObject and add null fallback

8 of 9 new or added lines in 1 file covered. (88.89%)

22146 of 25098 relevant lines covered (88.24%)

210.7 hits per line

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

75.97
/system/Database/BaseResult.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Database;
15

16
use CodeIgniter\Entity\Entity;
17
use stdClass;
18

19
/**
20
 * @template TConnection
21
 * @template TResult
22
 *
23
 * @implements ResultInterface<TConnection, TResult>
24
 */
25
abstract class BaseResult implements ResultInterface
26
{
27
    /**
28
     * Connection ID
29
     *
30
     * @var TConnection
31
     */
32
    public $connID;
33

34
    /**
35
     * Result ID
36
     *
37
     * @var false|TResult
38
     */
39
    public $resultID;
40

41
    /**
42
     * Result Array
43
     *
44
     * @var list<array>
45
     */
46
    public $resultArray = [];
47

48
    /**
49
     * Result Object
50
     *
51
     * @var list<object>
52
     */
53
    public $resultObject = [];
54

55
    /**
56
     * Custom Result Object
57
     *
58
     * @var array
59
     */
60
    public $customResultObject = [];
61

62
    /**
63
     * Current Row index
64
     *
65
     * @var int
66
     */
67
    public $currentRow = 0;
68

69
    /**
70
     * The number of records in the query result
71
     *
72
     * @var int|null
73
     */
74
    protected $numRows;
75

76
    /**
77
     * Row data
78
     *
79
     * @var array|null
80
     */
81
    public $rowData;
82

83
    /**
84
     * Constructor
85
     *
86
     * @param TConnection $connID
87
     * @param TResult     $resultID
88
     */
89
    public function __construct(&$connID, &$resultID)
90
    {
91
        $this->connID   = $connID;
852✔
92
        $this->resultID = $resultID;
852✔
93
    }
94

95
    /**
96
     * Retrieve the results of the query. Typically an array of
97
     * individual data rows, which can be either an 'array', an
98
     * 'object', or a custom class name.
99
     *
100
     * @param string $type The row type. Either 'array', 'object', or a class name to use
101
     */
102
    public function getResult(string $type = 'object'): array
103
    {
104
        if ($type === 'array') {
215✔
105
            return $this->getResultArray();
43✔
106
        }
107

108
        if ($type === 'object') {
172✔
109
            return $this->getResultObject();
164✔
110
        }
111

112
        return $this->getCustomResultObject($type);
8✔
113
    }
114

115
    /**
116
     * Returns the results as an array of custom objects.
117
     *
118
     * @param class-string $className
119
     *
120
     * @return array
121
     */
122
    public function getCustomResultObject(string $className)
123
    {
124
        if (isset($this->customResultObject[$className])) {
11✔
125
            return $this->customResultObject[$className];
×
126
        }
127

128
        if (! $this->isValidResultId()) {
11✔
129
            return [];
2✔
130
        }
131

132
        // Don't fetch the result set again if we already have it
133
        $_data = null;
9✔
134
        if (($c = count($this->resultArray)) > 0) {
9✔
135
            $_data = 'resultArray';
×
136
        } elseif (($c = count($this->resultObject)) > 0) {
9✔
137
            $_data = 'resultObject';
×
138
        }
139

140
        if ($_data !== null) {
9✔
141
            for ($i = 0; $i < $c; $i++) {
×
142
                $this->customResultObject[$className][$i] = new $className();
×
143

144
                foreach ($this->{$_data}[$i] as $key => $value) {
×
145
                    $this->customResultObject[$className][$i]->{$key} = $value;
×
146
                }
147
            }
148

149
            return $this->customResultObject[$className];
×
150
        }
151

152
        if ($this->rowData !== null) {
9✔
153
            $this->dataSeek();
×
154
        }
155
        $this->customResultObject[$className] = [];
9✔
156

157
        while ($row = $this->fetchObject($className)) {
9✔
158
            if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) {
9✔
159
                $row->syncOriginal();
×
160
            }
161

162
            $this->customResultObject[$className][] = $row;
9✔
163
        }
164

165
        return $this->customResultObject[$className];
9✔
166
    }
167

168
    /**
169
     * Returns the results as an array of arrays.
170
     *
171
     * If no results, an empty array is returned.
172
     */
173
    public function getResultArray(): array
174
    {
175
        if ($this->resultArray !== []) {
822✔
176
            return $this->resultArray;
7✔
177
        }
178

179
        // In the event that query caching is on, the result_id variable
180
        // will not be a valid resource so we'll simply return an empty
181
        // array.
182
        if (! $this->isValidResultId()) {
815✔
183
            return [];
1✔
184
        }
185

186
        if ($this->resultObject !== []) {
814✔
187
            foreach ($this->resultObject as $row) {
×
188
                $this->resultArray[] = (array) $row;
×
189
            }
190

191
            return $this->resultArray;
×
192
        }
193

194
        if ($this->rowData !== null) {
814✔
195
            $this->dataSeek();
×
196
        }
197

198
        while ($row = $this->fetchAssoc()) {
814✔
199
            $this->resultArray[] = $row;
791✔
200
        }
201

202
        return $this->resultArray;
814✔
203
    }
204

205
    /**
206
     * Returns the results as an array of objects.
207
     *
208
     * If no results, an empty array is returned.
209
     *
210
     * @return list<stdClass>
211
     */
212
    public function getResultObject(): array
213
    {
214
        if ($this->resultObject !== []) {
820✔
215
            return $this->resultObject;
13✔
216
        }
217

218
        // In the event that query caching is on, the result_id variable
219
        // will not be a valid resource so we'll simply return an empty
220
        // array.
221
        if (! $this->isValidResultId()) {
814✔
222
            return [];
2✔
223
        }
224

225
        if ($this->resultArray !== []) {
812✔
226
            foreach ($this->resultArray as $row) {
1✔
227
                $this->resultObject[] = (object) $row;
1✔
228
            }
229

230
            return $this->resultObject;
1✔
231
        }
232

233
        if ($this->rowData !== null) {
812✔
234
            $this->dataSeek();
×
235
        }
236

237
        while ($row = $this->fetchObject()) {
812✔
238
            if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) {
811✔
239
                $row->syncOriginal();
×
240
            }
241

242
            $this->resultObject[] = $row;
811✔
243
        }
244

245
        return $this->resultObject;
812✔
246
    }
247

248
    /**
249
     * Wrapper object to return a row as either an array, an object, or
250
     * a custom class.
251
     *
252
     * If the row doesn't exist, returns null.
253
     *
254
     * @template T of object
255
     *
256
     * @param int|string                       $n    The index of the results to return, or column name.
257
     * @param 'array'|'object'|class-string<T> $type The type of result object. 'array', 'object' or class name.
258
     *
259
     * @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)))
260
     */
261
    public function getRow($n = 0, string $type = 'object')
262
    {
263
        // $n is a column name.
264
        if (! is_numeric($n)) {
359✔
265
            // We cache the row data for subsequent uses
266
            if (! is_array($this->rowData)) {
2✔
267
                $this->rowData = $this->getRowArray();
2✔
268
            }
269

270
            // array_key_exists() instead of isset() to allow for NULL values
271
            if (empty($this->rowData) || ! array_key_exists($n, $this->rowData)) {
2✔
272
                return null;
1✔
273
            }
274

275
            return $this->rowData[$n];
1✔
276
        }
277

278
        if ($type === 'object') {
357✔
279
            return $this->getRowObject($n);
354✔
280
        }
281

282
        if ($type === 'array') {
3✔
283
            return $this->getRowArray($n);
2✔
284
        }
285

286
        return $this->getCustomRowObject($n, $type);
1✔
287
    }
288

289
    /**
290
     * Returns a row as a custom class instance.
291
     *
292
     * If the row doesn't exist, returns null.
293
     *
294
     * @template T of object
295
     *
296
     * @param int             $n         The index of the results to return.
297
     * @param class-string<T> $className
298
     *
299
     * @return T|null
300
     */
301
    public function getCustomRowObject(int $n, string $className)
302
    {
303
        if (! isset($this->customResultObject[$className])) {
3✔
304
            $this->getCustomResultObject($className);
3✔
305
        }
306

307
        if (empty($this->customResultObject[$className])) {
3✔
308
            return null;
2✔
309
        }
310

311
        // Return null if the requested index is out of bounds
312
        if (! isset($this->customResultObject[$className][$n])) {
1✔
NEW
313
            return null;
×
314
        }
315
        if ($n !== $this->currentRow && isset($this->customResultObject[$className][$n])) {
1✔
316
            $this->currentRow = $n;
×
317
        }
318

319
        return $this->customResultObject[$className][$this->currentRow];
1✔
320
    }
321

322
    /**
323
     * Returns a single row from the results as an array.
324
     *
325
     * If row doesn't exist, returns null.
326
     *
327
     * @return array|null
328
     */
329
    public function getRowArray(int $n = 0)
330
    {
331
        $result = $this->getResultArray();
16✔
332
        if ($result === []) {
16✔
333
            return null;
1✔
334
        }
335

336
        // If default call (n = 0) but currentRow was previously set to an invalid index,
337
        // return null instead of silently falling back to the first row.
338
        if ($n === 0 && $this->currentRow !== 0 && ! isset($result[$this->currentRow])) {
15✔
339
            return null;
1✔
340
        }
341
        if ($n !== $this->currentRow && isset($result[$n])) {
14✔
342
            $this->currentRow = $n;
2✔
343
        }
344

345
        return $result[$this->currentRow] ?? null;
14✔
346
    }
347

348
    /**
349
     * Returns a single row from the results as an object.
350
     *
351
     * If row doesn't exist, returns null.
352
     *
353
     * @return object|stdClass|null
354
     */
355
    public function getRowObject(int $n = 0)
356
    {
357
        $result = $this->getResultObject();
360✔
358
        if ($result === []) {
360✔
359
            return null;
18✔
360
        }
361

362
        // Similar safeguard for object rows
363
        if ($n === 0 && $this->currentRow !== 0 && ! isset($result[$this->currentRow])) {
349✔
364
            return null;
1✔
365
        }
366
        if ($n !== $this->currentRow && isset($result[$n])) {
348✔
367
            $this->currentRow = $n;
3✔
368
        }
369

370
        return $result[$this->currentRow] ?? null;
348✔
371
    }
372

373
    /**
374
     * Assigns an item into a particular column slot.
375
     *
376
     * @param array|string               $key
377
     * @param array|object|stdClass|null $value
378
     *
379
     * @return void
380
     */
381
    public function setRow($key, $value = null)
382
    {
383
        // We cache the row data for subsequent uses
384
        if (! is_array($this->rowData)) {
×
385
            $this->rowData = $this->getRowArray();
×
386
        }
387

388
        if (is_array($key)) {
×
389
            foreach ($key as $k => $v) {
×
390
                $this->rowData[$k] = $v;
×
391
            }
392

393
            return;
×
394
        }
395

396
        if ($key !== '' && $value !== null) {
×
397
            $this->rowData[$key] = $value;
×
398
        }
399
    }
400

401
    /**
402
     * Returns the "first" row of the current results.
403
     *
404
     * @return array|object|null
405
     */
406
    public function getFirstRow(string $type = 'object')
407
    {
408
        $result = $this->getResult($type);
73✔
409

410
        return ($result === []) ? null : $result[0];
73✔
411
    }
412

413
    /**
414
     * Returns the "last" row of the current results.
415
     *
416
     * @return array|object|null
417
     */
418
    public function getLastRow(string $type = 'object')
419
    {
420
        $result = $this->getResult($type);
1✔
421

422
        return ($result === []) ? null : $result[count($result) - 1];
1✔
423
    }
424

425
    /**
426
     * Returns the "next" row of the current results.
427
     *
428
     * @return array|object|null
429
     */
430
    public function getNextRow(string $type = 'object')
431
    {
432
        $result = $this->getResult($type);
1✔
433
        if ($result === []) {
1✔
434
            return null;
×
435
        }
436

437
        return isset($result[$this->currentRow + 1]) ? $result[++$this->currentRow] : null;
1✔
438
    }
439

440
    /**
441
     * Returns the "previous" row of the current results.
442
     *
443
     * @return array|object|null
444
     */
445
    public function getPreviousRow(string $type = 'object')
446
    {
447
        $result = $this->getResult($type);
2✔
448
        if ($result === []) {
2✔
449
            return null;
1✔
450
        }
451

452
        if (isset($result[$this->currentRow - 1])) {
1✔
453
            $this->currentRow--;
1✔
454
        }
455

456
        return $result[$this->currentRow];
1✔
457
    }
458

459
    /**
460
     * Returns an unbuffered row and move the pointer to the next row.
461
     *
462
     * @return array|object|null
463
     */
464
    public function getUnbufferedRow(string $type = 'object')
465
    {
466
        if ($type === 'array') {
2✔
467
            return $this->fetchAssoc();
1✔
468
        }
469

470
        if ($type === 'object') {
1✔
471
            return $this->fetchObject();
1✔
472
        }
473

474
        return $this->fetchObject($type);
×
475
    }
476

477
    /**
478
     * Number of rows in the result set; checks for previous count, falls
479
     * back on counting resultArray or resultObject, finally fetching resultArray
480
     * if nothing was previously fetched
481
     */
482
    public function getNumRows(): int
483
    {
484
        if (is_int($this->numRows)) {
5✔
485
            return $this->numRows;
×
486
        }
487
        if ($this->resultArray !== []) {
5✔
488
            return $this->numRows = count($this->resultArray);
×
489
        }
490
        if ($this->resultObject !== []) {
5✔
491
            return $this->numRows = count($this->resultObject);
×
492
        }
493

494
        return $this->numRows = count($this->getResultArray());
5✔
495
    }
496

497
    private function isValidResultId(): bool
498
    {
499
        return is_resource($this->resultID) || is_object($this->resultID);
835✔
500
    }
501

502
    /**
503
     * Gets the number of fields in the result set.
504
     */
505
    abstract public function getFieldCount(): int;
506

507
    /**
508
     * Generates an array of column names in the result set.
509
     */
510
    abstract public function getFieldNames(): array;
511

512
    /**
513
     * Generates an array of objects representing field meta-data.
514
     */
515
    abstract public function getFieldData(): array;
516

517
    /**
518
     * Frees the current result.
519
     *
520
     * @return void
521
     */
522
    abstract public function freeResult();
523

524
    /**
525
     * Moves the internal pointer to the desired offset. This is called
526
     * internally before fetching results to make sure the result set
527
     * starts at zero.
528
     *
529
     * @return bool
530
     */
531
    abstract public function dataSeek(int $n = 0);
532

533
    /**
534
     * Returns the result set as an array.
535
     *
536
     * Overridden by driver classes.
537
     *
538
     * @return array|false|null
539
     */
540
    abstract protected function fetchAssoc();
541

542
    /**
543
     * Returns the result set as an object.
544
     *
545
     * @param class-string $className
546
     *
547
     * @return false|object
548
     */
549
    abstract protected function fetchObject(string $className = stdClass::class);
550
}
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