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

codeigniter4 / CodeIgniter4 / 22225212157

20 Feb 2026 01:04PM UTC coverage: 85.718% (+0.001%) from 85.717%
22225212157

push

github

web-flow
fix: prevent extra query and invalid size in Model::chunk() (#9961)

3 of 3 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

22243 of 25949 relevant lines covered (85.72%)

206.68 hits per line

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

98.41
/system/Model.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;
15

16
use Closure;
17
use CodeIgniter\Database\BaseBuilder;
18
use CodeIgniter\Database\BaseConnection;
19
use CodeIgniter\Database\ConnectionInterface;
20
use CodeIgniter\Database\Exceptions\DatabaseException;
21
use CodeIgniter\Database\Exceptions\DataException;
22
use CodeIgniter\Entity\Entity;
23
use CodeIgniter\Exceptions\BadMethodCallException;
24
use CodeIgniter\Exceptions\InvalidArgumentException;
25
use CodeIgniter\Exceptions\ModelException;
26
use CodeIgniter\Validation\ValidationInterface;
27
use Config\Database;
28
use Config\Feature;
29
use stdClass;
30

31
/**
32
 * The Model class extends BaseModel and provides additional
33
 * convenient features that makes working with a SQL database
34
 * table less painful.
35
 *
36
 * It will:
37
 *      - automatically connect to database
38
 *      - allow intermingling calls to the builder
39
 *      - removes the need to use Result object directly in most cases
40
 *
41
 * @property-read BaseConnection $db
42
 *
43
 * @method $this groupBy($by, ?bool $escape = null)
44
 * @method $this groupEnd()
45
 * @method $this groupStart()
46
 * @method $this having($key, $value = null, ?bool $escape = null)
47
 * @method $this havingGroupEnd()
48
 * @method $this havingGroupStart()
49
 * @method $this havingIn(?string $key = null, $values = null, ?bool $escape = null)
50
 * @method $this havingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
51
 * @method $this havingNotIn(?string $key = null, $values = null, ?bool $escape = null)
52
 * @method $this join(string $table, string $cond, string $type = '', ?bool $escape = null)
53
 * @method $this like($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
54
 * @method $this limit(?int $value = null, ?int $offset = 0)
55
 * @method $this notGroupStart()
56
 * @method $this notHavingGroupStart()
57
 * @method $this notHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
58
 * @method $this notLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
59
 * @method $this offset(int $offset)
60
 * @method $this orderBy(string $orderBy, string $direction = '', ?bool $escape = null)
61
 * @method $this orGroupStart()
62
 * @method $this orHaving($key, $value = null, ?bool $escape = null)
63
 * @method $this orHavingGroupStart()
64
 * @method $this orHavingIn(?string $key = null, $values = null, ?bool $escape = null)
65
 * @method $this orHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
66
 * @method $this orHavingNotIn(?string $key = null, $values = null, ?bool $escape = null)
67
 * @method $this orLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
68
 * @method $this orNotGroupStart()
69
 * @method $this orNotHavingGroupStart()
70
 * @method $this orNotHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
71
 * @method $this orNotLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
72
 * @method $this orWhere($key, $value = null, ?bool $escape = null)
73
 * @method $this orWhereIn(?string $key = null, $values = null, ?bool $escape = null)
74
 * @method $this orWhereNotIn(?string $key = null, $values = null, ?bool $escape = null)
75
 * @method $this select($select = '*', ?bool $escape = null)
76
 * @method $this selectAvg(string $select = '', string $alias = '')
77
 * @method $this selectCount(string $select = '', string $alias = '')
78
 * @method $this selectMax(string $select = '', string $alias = '')
79
 * @method $this selectMin(string $select = '', string $alias = '')
80
 * @method $this selectSum(string $select = '', string $alias = '')
81
 * @method $this when($condition, callable $callback, ?callable $defaultCallback = null)
82
 * @method $this whenNot($condition, callable $callback, ?callable $defaultCallback = null)
83
 * @method $this where($key, $value = null, ?bool $escape = null)
84
 * @method $this whereIn(?string $key = null, $values = null, ?bool $escape = null)
85
 * @method $this whereNotIn(?string $key = null, $values = null, ?bool $escape = null)
86
 *
87
 * @phpstan-import-type row_array from BaseModel
88
 */
89
class Model extends BaseModel
90
{
91
    /**
92
     * Name of database table.
93
     *
94
     * @var string
95
     */
96
    protected $table;
97

98
    /**
99
     * The table's primary key.
100
     *
101
     * @var string
102
     */
103
    protected $primaryKey = 'id';
104

105
    /**
106
     * Whether primary key uses auto increment.
107
     *
108
     * @var bool
109
     */
110
    protected $useAutoIncrement = true;
111

112
    /**
113
     * Query Builder object.
114
     *
115
     * @var BaseBuilder|null
116
     */
117
    protected $builder;
118

119
    /**
120
     * Holds information passed in via 'set'
121
     * so that we can capture it (not the builder)
122
     * and ensure it gets validated first.
123
     *
124
     * @var array{escape: array<int|string, bool|null>, data: row_array}|array{}
125
     */
126
    protected $tempData = [];
127

128
    /**
129
     * Escape array that maps usage of escape
130
     * flag for every parameter.
131
     *
132
     * @var array<int|string, bool|null>
133
     */
134
    protected $escape = [];
135

136
    /**
137
     * Builder method names that should not be used in the Model.
138
     *
139
     * @var list<string>
140
     */
141
    private array $builderMethodsNotAvailable = [
142
        'getCompiledInsert',
143
        'getCompiledSelect',
144
        'getCompiledUpdate',
145
    ];
146

147
    public function __construct(?ConnectionInterface $db = null, ?ValidationInterface $validation = null)
148
    {
149
        /**
150
         * @var BaseConnection|null $db
151
         */
152
        $db ??= Database::connect($this->DBGroup);
397✔
153

154
        $this->db = $db;
397✔
155

156
        parent::__construct($validation);
397✔
157
    }
158

159
    /**
160
     * Specify the table associated with a model.
161
     *
162
     * @return $this
163
     */
164
    public function setTable(string $table)
165
    {
166
        $this->table = $table;
1✔
167

168
        return $this;
1✔
169
    }
170

171
    protected function doFind(bool $singleton, $id = null)
172
    {
173
        $builder = $this->builder();
56✔
174
        $useCast = $this->useCasts();
55✔
175

176
        if ($useCast) {
55✔
177
            $returnType = $this->tempReturnType;
18✔
178
            $this->asArray();
18✔
179
        }
180

181
        if ($this->tempUseSoftDeletes) {
55✔
182
            $builder->where($this->table . '.' . $this->deletedField, null);
31✔
183
        }
184

185
        $row  = null;
55✔
186
        $rows = [];
55✔
187

188
        if (is_array($id)) {
55✔
189
            $rows = $builder->whereIn($this->table . '.' . $this->primaryKey, $id)
3✔
190
                ->get()
3✔
191
                ->getResult($this->tempReturnType);
3✔
192
        } elseif ($singleton) {
52✔
193
            $row = $builder->where($this->table . '.' . $this->primaryKey, $id)
44✔
194
                ->get()
44✔
195
                ->getFirstRow($this->tempReturnType);
44✔
196
        } else {
197
            $rows = $builder->get()->getResult($this->tempReturnType);
9✔
198
        }
199

200
        if ($useCast) {
55✔
201
            $this->tempReturnType = $returnType;
18✔
202

203
            if ($singleton) {
18✔
204
                if ($row === null) {
15✔
205
                    return null;
1✔
206
                }
207

208
                return $this->convertToReturnType($row, $returnType);
14✔
209
            }
210

211
            foreach ($rows as $i => $row) {
3✔
212
                $rows[$i] = $this->convertToReturnType($row, $returnType);
3✔
213
            }
214

215
            return $rows;
3✔
216
        }
217

218
        if ($singleton) {
37✔
219
            return $row;
29✔
220
        }
221

222
        return $rows;
9✔
223
    }
224

225
    protected function doFindColumn(string $columnName)
226
    {
227
        return $this->select($columnName)->asArray()->find();
2✔
228
    }
229

230
    /**
231
     * {@inheritDoc}
232
     *
233
     * Works with the current Query Builder instance.
234
     */
235
    protected function doFindAll(?int $limit = null, int $offset = 0)
236
    {
237
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
22✔
238
        if ($limitZeroAsAll) {
22✔
239
            $limit ??= 0;
22✔
240
        }
241

242
        $builder = $this->builder();
22✔
243

244
        $useCast = $this->useCasts();
22✔
245
        if ($useCast) {
22✔
246
            $returnType = $this->tempReturnType;
5✔
247
            $this->asArray();
5✔
248
        }
249

250
        if ($this->tempUseSoftDeletes) {
22✔
251
            $builder->where($this->table . '.' . $this->deletedField, null);
10✔
252
        }
253

254
        $results = $builder->limit($limit, $offset)
22✔
255
            ->get()
22✔
256
            ->getResult($this->tempReturnType);
22✔
257

258
        if ($useCast) {
22✔
259
            foreach ($results as $i => $row) {
5✔
260
                $results[$i] = $this->convertToReturnType($row, $returnType);
4✔
261
            }
262

263
            $this->tempReturnType = $returnType;
5✔
264
        }
265

266
        return $results;
22✔
267
    }
268

269
    /**
270
     * {@inheritDoc}
271
     *
272
     * Will take any previous Query Builder calls into account
273
     * when determining the result set.
274
     */
275
    protected function doFirst()
276
    {
277
        $builder = $this->builder();
24✔
278

279
        $useCast = $this->useCasts();
24✔
280
        if ($useCast) {
24✔
281
            $returnType = $this->tempReturnType;
5✔
282
            $this->asArray();
5✔
283
        }
284

285
        if ($this->tempUseSoftDeletes) {
24✔
286
            $builder->where($this->table . '.' . $this->deletedField, null);
19✔
287
        } elseif ($this->useSoftDeletes && ($builder->QBGroupBy === []) && $this->primaryKey !== '') {
13✔
288
            $builder->groupBy($this->table . '.' . $this->primaryKey);
6✔
289
        }
290

291
        // Some databases, like PostgreSQL, need order
292
        // information to consistently return correct results.
293
        if ($builder->QBGroupBy !== [] && ($builder->QBOrderBy === []) && $this->primaryKey !== '') {
24✔
294
            $builder->orderBy($this->table . '.' . $this->primaryKey, 'asc');
9✔
295
        }
296

297
        $row = $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType);
24✔
298

299
        if ($useCast && $row !== null) {
24✔
300
            $row = $this->convertToReturnType($row, $returnType);
4✔
301

302
            $this->tempReturnType = $returnType;
4✔
303
        }
304

305
        return $row;
24✔
306
    }
307

308
    protected function doInsert(array $row)
309
    {
310
        $escape       = $this->escape;
96✔
311
        $this->escape = [];
96✔
312

313
        // Require non-empty primaryKey when
314
        // not using auto-increment feature
315
        if (! $this->useAutoIncrement) {
96✔
316
            if (! isset($row[$this->primaryKey])) {
15✔
317
                throw DataException::forEmptyPrimaryKey('insert');
2✔
318
            }
319

320
            // Validate the primary key value (arrays not allowed for insert)
321
            $this->validateID($row[$this->primaryKey], false);
13✔
322
        }
323

324
        $builder = $this->builder();
85✔
325

326
        // Must use the set() method to ensure to set the correct escape flag
327
        foreach ($row as $key => $val) {
85✔
328
            $builder->set($key, $val, $escape[$key] ?? null);
84✔
329
        }
330

331
        if ($this->allowEmptyInserts && $row === []) {
85✔
332
            $table = $this->db->protectIdentifiers($this->table, true, null, false);
1✔
333
            if ($this->db->getPlatform() === 'MySQLi') {
1✔
334
                $sql = 'INSERT INTO ' . $table . ' VALUES ()';
1✔
335
            } elseif ($this->db->getPlatform() === 'OCI8') {
1✔
336
                $allFields = $this->db->protectIdentifiers(
1✔
337
                    array_map(
1✔
338
                        static fn ($row) => $row->name,
1✔
339
                        $this->db->getFieldData($this->table),
1✔
340
                    ),
1✔
341
                    false,
1✔
342
                    true,
1✔
343
                );
1✔
344

345
                $sql = sprintf(
1✔
346
                    'INSERT INTO %s (%s) VALUES (%s)',
1✔
347
                    $table,
1✔
348
                    implode(',', $allFields),
1✔
349
                    substr(str_repeat(',DEFAULT', count($allFields)), 1),
1✔
350
                );
1✔
351
            } else {
352
                $sql = 'INSERT INTO ' . $table . ' DEFAULT VALUES';
1✔
353
            }
354

355
            $result = $this->db->query($sql);
1✔
356
        } else {
357
            $result = $builder->insert();
84✔
358
        }
359

360
        // If insertion succeeded then save the insert ID
361
        if ($result) {
85✔
362
            $this->insertID = $this->useAutoIncrement ? $this->db->insertID() : $row[$this->primaryKey];
83✔
363
        }
364

365
        return $result;
85✔
366
    }
367

368
    protected function doInsertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100, bool $testing = false)
369
    {
370
        if (is_array($set) && ! $this->useAutoIncrement) {
21✔
371
            foreach ($set as $row) {
11✔
372
                // Require non-empty $primaryKey when
373
                // not using auto-increment feature
374
                if (! isset($row[$this->primaryKey])) {
11✔
375
                    throw DataException::forEmptyPrimaryKey('insertBatch');
1✔
376
                }
377

378
                // Validate the primary key value
379
                $this->validateID($row[$this->primaryKey], false);
11✔
380
            }
381
        }
382

383
        return $this->builder()->testMode($testing)->insertBatch($set, $escape, $batchSize);
11✔
384
    }
385

386
    protected function doUpdate($id = null, $row = null): bool
387
    {
388
        $escape       = $this->escape;
38✔
389
        $this->escape = [];
38✔
390

391
        $builder = $this->builder();
38✔
392

393
        if (is_array($id) && $id !== []) {
38✔
394
            $builder = $builder->whereIn($this->table . '.' . $this->primaryKey, $id);
32✔
395
        }
396

397
        // Must use the set() method to ensure to set the correct escape flag
398
        foreach ($row as $key => $val) {
38✔
399
            $builder->set($key, $val, $escape[$key] ?? null);
38✔
400
        }
401

402
        if ($builder->getCompiledQBWhere() === []) {
38✔
403
            throw new DatabaseException(
1✔
404
                'Updates are not allowed unless they contain a "where" or "like" clause.',
1✔
405
            );
1✔
406
        }
407

408
        return $builder->update();
37✔
409
    }
410

411
    protected function doUpdateBatch(?array $set = null, ?string $index = null, int $batchSize = 100, bool $returnSQL = false)
412
    {
413
        return $this->builder()->testMode($returnSQL)->updateBatch($set, $index, $batchSize);
5✔
414
    }
415

416
    protected function doDelete($id = null, bool $purge = false)
417
    {
418
        $set     = [];
36✔
419
        $builder = $this->builder();
36✔
420

421
        if (is_array($id) && $id !== []) {
36✔
422
            $builder = $builder->whereIn($this->primaryKey, $id);
22✔
423
        }
424

425
        if ($this->useSoftDeletes && ! $purge) {
36✔
426
            if ($builder->getCompiledQBWhere() === []) {
25✔
427
                throw new DatabaseException(
3✔
428
                    'Deletes are not allowed unless they contain a "where" or "like" clause.',
3✔
429
                );
3✔
430
            }
431

432
            $builder->where($this->deletedField);
22✔
433

434
            $set[$this->deletedField] = $this->setDate();
22✔
435

436
            if ($this->useTimestamps && $this->updatedField !== '') {
21✔
437
                $set[$this->updatedField] = $this->setDate();
1✔
438
            }
439

440
            return $builder->update($set);
21✔
441
        }
442

443
        return $builder->delete();
11✔
444
    }
445

446
    protected function doPurgeDeleted()
447
    {
448
        return $this->builder()
1✔
449
            ->where($this->table . '.' . $this->deletedField . ' IS NOT NULL')
1✔
450
            ->delete();
1✔
451
    }
452

453
    protected function doOnlyDeleted()
454
    {
455
        $this->builder()->where($this->table . '.' . $this->deletedField . ' IS NOT NULL');
1✔
456
    }
457

458
    protected function doReplace(?array $row = null, bool $returnSQL = false)
459
    {
460
        return $this->builder()->testMode($returnSQL)->replace($row);
2✔
461
    }
462

463
    /**
464
     * {@inheritDoc}
465
     *
466
     * The return array should be in the following format:
467
     *  `['source' => 'message']`.
468
     * This method works only with dbCalls.
469
     */
470
    protected function doErrors()
471
    {
472
        // $error is always ['code' => string|int, 'message' => string]
473
        $error = $this->db->error();
2✔
474

475
        if ((int) $error['code'] === 0) {
2✔
476
            return [];
2✔
477
        }
478

479
        return [$this->db::class => $error['message']];
×
480
    }
481

482
    public function getIdValue($row)
483
    {
484
        if (is_object($row)) {
27✔
485
            // Get the raw or mapped primary key value of the Entity.
486
            if ($row instanceof Entity && $row->{$this->primaryKey} !== null) {
17✔
487
                $cast = $row->cast();
9✔
488

489
                // Disable Entity casting, because raw primary key value is needed for database.
490
                $row->cast(false);
9✔
491

492
                $primaryKey = $row->{$this->primaryKey};
9✔
493

494
                // Restore Entity casting setting.
495
                $row->cast($cast);
9✔
496

497
                return $primaryKey;
9✔
498
            }
499

500
            if (! $row instanceof Entity && isset($row->{$this->primaryKey})) {
9✔
501
                return $row->{$this->primaryKey};
5✔
502
            }
503
        }
504

505
        if (is_array($row) && isset($row[$this->primaryKey])) {
15✔
506
            return $row[$this->primaryKey];
4✔
507
        }
508

509
        return null;
11✔
510
    }
511

512
    public function countAllResults(bool $reset = true, bool $test = false)
513
    {
514
        if ($this->tempUseSoftDeletes) {
17✔
515
            $this->builder()->where($this->table . '.' . $this->deletedField, null);
6✔
516
        }
517

518
        // When $reset === false, the $tempUseSoftDeletes will be
519
        // dependent on $useSoftDeletes value because we don't
520
        // want to add the same "where" condition for the second time.
521
        $this->tempUseSoftDeletes = $reset
17✔
522
            ? $this->useSoftDeletes
10✔
523
            : ($this->useSoftDeletes ? false : $this->useSoftDeletes);
12✔
524

525
        return $this->builder()->testMode($test)->countAllResults($reset);
17✔
526
    }
527

528
    /**
529
     * {@inheritDoc}
530
     *
531
     * Works with `$this->builder` to get the Compiled select to
532
     * determine the rows to operate on.
533
     * This method works only with dbCalls.
534
     */
535
    public function chunk(int $size, Closure $userFunc)
536
    {
537
        if ($size <= 0) {
6✔
538
            throw new InvalidArgumentException('chunk() requires a positive integer for the $size argument.');
2✔
539
        }
540

541
        $total  = $this->builder()->countAllResults(false);
4✔
542
        $offset = 0;
4✔
543

544
        while ($offset < $total) {
4✔
545
            $builder = clone $this->builder();
3✔
546
            $rows    = $builder->get($size, $offset);
3✔
547

548
            if (! $rows) {
3✔
549
                throw DataException::forEmptyDataset('chunk');
×
550
            }
551

552
            $rows = $rows->getResult($this->tempReturnType);
3✔
553

554
            $offset += $size;
3✔
555

556
            if ($rows === []) {
3✔
UNCOV
557
                continue;
×
558
            }
559

560
            foreach ($rows as $row) {
3✔
561
                if ($userFunc($row) === false) {
3✔
562
                    return;
1✔
563
                }
564
            }
565
        }
566
    }
567

568
    /**
569
     * Provides a shared instance of the Query Builder.
570
     *
571
     * @param non-empty-string|null $table
572
     *
573
     * @return BaseBuilder
574
     *
575
     * @throws ModelException
576
     */
577
    public function builder(?string $table = null)
578
    {
579
        // Check for an existing Builder
580
        if ($this->builder instanceof BaseBuilder) {
201✔
581
            // Make sure the requested table matches the builder
582
            if ((string) $table !== '' && $this->builder->getTable() !== $table) {
118✔
583
                return $this->db->table($table);
1✔
584
            }
585

586
            return $this->builder;
118✔
587
        }
588

589
        // We're going to force a primary key to exist
590
        // so we don't have overly convoluted code,
591
        // and future features are likely to require them.
592
        if ($this->primaryKey === '') {
201✔
593
            throw ModelException::forNoPrimaryKey(static::class);
1✔
594
        }
595

596
        $table = ((string) $table === '') ? $this->table : $table;
200✔
597

598
        // Ensure we have a good db connection
599
        if (! $this->db instanceof BaseConnection) {
200✔
600
            $this->db = Database::connect($this->DBGroup);
×
601
        }
602

603
        $builder = $this->db->table($table);
200✔
604

605
        // Only consider it "shared" if the table is correct
606
        if ($table === $this->table) {
200✔
607
            $this->builder = $builder;
200✔
608
        }
609

610
        return $builder;
200✔
611
    }
612

613
    /**
614
     * Captures the builder's set() method so that we can validate the
615
     * data here. This allows it to be used with any of the other
616
     * builder methods and still get validated data, like replace.
617
     *
618
     * @param object|row_array|string           $key    Field name, or an array of field/value pairs, or an object
619
     * @param bool|float|int|object|string|null $value  Field value, if $key is a single field
620
     * @param bool|null                         $escape Whether to escape values
621
     *
622
     * @return $this
623
     */
624
    public function set($key, $value = '', ?bool $escape = null)
625
    {
626
        if (is_object($key)) {
10✔
627
            $key = $key instanceof stdClass ? (array) $key : $this->objectToArray($key);
2✔
628
        }
629

630
        $data = is_array($key) ? $key : [$key => $value];
10✔
631

632
        foreach (array_keys($data) as $k) {
10✔
633
            $this->tempData['escape'][$k] = $escape;
10✔
634
        }
635

636
        $this->tempData['data'] = array_merge($this->tempData['data'] ?? [], $data);
10✔
637

638
        return $this;
10✔
639
    }
640

641
    protected function shouldUpdate($row): bool
642
    {
643
        if (parent::shouldUpdate($row) === false) {
27✔
644
            return false;
12✔
645
        }
646

647
        if ($this->useAutoIncrement === true) {
17✔
648
            return true;
14✔
649
        }
650

651
        // When useAutoIncrement feature is disabled, check
652
        // in the database if given record already exists
653
        return $this->where($this->primaryKey, $this->getIdValue($row))->countAllResults() === 1;
3✔
654
    }
655

656
    public function insert($row = null, bool $returnID = true)
657
    {
658
        if (isset($this->tempData['data'])) {
119✔
659
            if ($row === null) {
2✔
660
                $row = $this->tempData['data'];
1✔
661
            } else {
662
                $row = $this->transformDataToArray($row, 'insert');
1✔
663
                $row = array_merge($this->tempData['data'], $row);
1✔
664
            }
665
        }
666

667
        $this->escape   = $this->tempData['escape'] ?? [];
119✔
668
        $this->tempData = [];
119✔
669

670
        return parent::insert($row, $returnID);
119✔
671
    }
672

673
    protected function doProtectFieldsForInsert(array $row): array
674
    {
675
        if (! $this->protectFields) {
121✔
676
            return $row;
9✔
677
        }
678

679
        if ($this->allowedFields === []) {
112✔
680
            throw DataException::forInvalidAllowedFields(static::class);
1✔
681
        }
682

683
        foreach (array_keys($row) as $key) {
111✔
684
            // Do not remove the non-auto-incrementing primary key data.
685
            if ($this->useAutoIncrement === false && $key === $this->primaryKey) {
110✔
686
                continue;
25✔
687
            }
688

689
            if (! in_array($key, $this->allowedFields, true)) {
110✔
690
                unset($row[$key]);
25✔
691
            }
692
        }
693

694
        return $row;
111✔
695
    }
696

697
    public function update($id = null, $row = null): bool
698
    {
699
        if (isset($this->tempData['data'])) {
58✔
700
            if ($row === null) {
6✔
701
                $row = $this->tempData['data'];
5✔
702
            } else {
703
                $row = $this->transformDataToArray($row, 'update');
1✔
704
                $row = array_merge($this->tempData['data'], $row);
1✔
705
            }
706
        }
707

708
        $this->escape   = $this->tempData['escape'] ?? [];
58✔
709
        $this->tempData = [];
58✔
710

711
        return parent::update($id, $row);
58✔
712
    }
713

714
    protected function objectToRawArray($object, bool $onlyChanged = true, bool $recursive = false): array
715
    {
716
        return parent::objectToRawArray($object, $onlyChanged);
25✔
717
    }
718

719
    /**
720
     * Provides/instantiates the builder/db connection and model's table/primary key names and return type.
721
     *
722
     * @return array<int|string, mixed>|BaseBuilder|bool|float|int|object|string|null
723
     */
724
    public function __get(string $name)
725
    {
726
        if (parent::__isset($name)) {
50✔
727
            return parent::__get($name);
50✔
728
        }
729

730
        return $this->builder()->{$name} ?? null;
1✔
731
    }
732

733
    /**
734
     * Checks for the existence of properties across this model, builder, and db connection.
735
     */
736
    public function __isset(string $name): bool
737
    {
738
        if (parent::__isset($name)) {
47✔
739
            return true;
47✔
740
        }
741

742
        return isset($this->builder()->{$name});
1✔
743
    }
744

745
    /**
746
     * Provides direct access to method in the builder (if available)
747
     * and the database connection.
748
     *
749
     * @return $this|array<int|string, mixed>|BaseBuilder|bool|float|int|object|string|null
750
     */
751
    public function __call(string $name, array $params)
752
    {
753
        $builder = $this->builder();
48✔
754
        $result  = null;
48✔
755

756
        if (method_exists($this->db, $name)) {
48✔
757
            $result = $this->db->{$name}(...$params);
2✔
758
        } elseif (method_exists($builder, $name)) {
47✔
759
            $this->checkBuilderMethod($name);
46✔
760

761
            $result = $builder->{$name}(...$params);
44✔
762
        } else {
763
            throw new BadMethodCallException('Call to undefined method ' . static::class . '::' . $name);
1✔
764
        }
765

766
        if ($result instanceof BaseBuilder) {
45✔
767
            return $this;
44✔
768
        }
769

770
        return $result;
2✔
771
    }
772

773
    /**
774
     * Checks the Builder method name that should not be used in the Model.
775
     */
776
    private function checkBuilderMethod(string $name): void
777
    {
778
        if (in_array($name, $this->builderMethodsNotAvailable, true)) {
46✔
779
            throw ModelException::forMethodNotAvailable(static::class, $name . '()');
2✔
780
        }
781
    }
782
}
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