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

codeigniter4 / CodeIgniter4 / 22254151593

21 Feb 2026 09:09AM UTC coverage: 85.98% (+0.003%) from 85.977%
22254151593

Pull #9962

github

web-flow
Merge e5e63a3f4 into a7c9a2f89
Pull Request #9962: feat: Chunk array method in models

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

3 existing lines in 1 file now uncovered.

22267 of 25898 relevant lines covered (85.98%)

208.1 hits per line

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

98.44
/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 Generator;
30
use stdClass;
31

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

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

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

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

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

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

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

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

155
        $this->db = $db;
403✔
156

157
        parent::__construct($validation);
403✔
158
    }
159

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

169
        return $this;
1✔
170
    }
171

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

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

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

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

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

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

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

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

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

216
            return $rows;
3✔
217
        }
218

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

223
        return $rows;
9✔
224
    }
225

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

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

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

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

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

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

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

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

267
        return $results;
22✔
268
    }
269

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

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

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

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

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

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

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

306
        return $row;
24✔
307
    }
308

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

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

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

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

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

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

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

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

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

366
        return $result;
85✔
367
    }
368

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

498
                return $primaryKey;
9✔
499
            }
500

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

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

510
        return null;
11✔
511
    }
512

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

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

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

529
    private function iterateChunks(int $size): Generator
530
    {
531
        if ($size <= 0) {
12✔
532
            throw new InvalidArgumentException('$size must be a positive integer.');
4✔
533
        }
534

535
        $total  = $this->builder()->countAllResults(false);
8✔
536
        $offset = 0;
8✔
537

538
        while ($offset < $total) {
8✔
539
            $builder = clone $this->builder();
6✔
540
            $rows    = $builder->get($size, $offset);
6✔
541

542
            if (! $rows) {
6✔
UNCOV
543
                throw DataException::forEmptyDataset('chunk');
×
544
            }
545

546
            $rows = $rows->getResult($this->tempReturnType);
6✔
547

548
            $offset += $size;
6✔
549

550
            if ($rows === []) {
6✔
UNCOV
551
                continue;
×
552
            }
553

554
            yield $rows;
6✔
555
        }
556
    }
557

558
    /**
559
     * {@inheritDoc}
560
     *
561
     * Works with `$this->builder` to get the Compiled select to
562
     * determine the rows to operate on.
563
     * This method works only with dbCalls.
564
     */
565
    public function chunk(int $size, Closure $userFunc)
566
    {
567
        foreach ($this->iterateChunks($size) as $rows) {
6✔
568
            foreach ($rows as $row) {
3✔
569
                if ($userFunc($row) === false) {
3✔
570
                    return;
1✔
571
                }
572
            }
573
        }
574
    }
575

576
    /**
577
     * {@inheritDoc}
578
     *
579
     * Works with `$this->builder` to get the Compiled select to
580
     * determine the rows to operate on.
581
     * This method works only with dbCalls.
582
     */
583
    public function chunkRows(int $size, Closure $userFunc)
584
    {
585
        foreach ($this->iterateChunks($size) as $rows) {
6✔
586
            if ($userFunc($rows) === false) {
3✔
587
                return;
1✔
588
            }
589
        }
590
    }
591

592
    /**
593
     * Provides a shared instance of the Query Builder.
594
     *
595
     * @param non-empty-string|null $table
596
     *
597
     * @return BaseBuilder
598
     *
599
     * @throws ModelException
600
     */
601
    public function builder(?string $table = null)
602
    {
603
        // Check for an existing Builder
604
        if ($this->builder instanceof BaseBuilder) {
205✔
605
            // Make sure the requested table matches the builder
606
            if ((string) $table !== '' && $this->builder->getTable() !== $table) {
121✔
607
                return $this->db->table($table);
1✔
608
            }
609

610
            return $this->builder;
121✔
611
        }
612

613
        // We're going to force a primary key to exist
614
        // so we don't have overly convoluted code,
615
        // and future features are likely to require them.
616
        if ($this->primaryKey === '') {
205✔
617
            throw ModelException::forNoPrimaryKey(static::class);
1✔
618
        }
619

620
        $table = ((string) $table === '') ? $this->table : $table;
204✔
621

622
        // Ensure we have a good db connection
623
        if (! $this->db instanceof BaseConnection) {
204✔
UNCOV
624
            $this->db = Database::connect($this->DBGroup);
×
625
        }
626

627
        $builder = $this->db->table($table);
204✔
628

629
        // Only consider it "shared" if the table is correct
630
        if ($table === $this->table) {
204✔
631
            $this->builder = $builder;
204✔
632
        }
633

634
        return $builder;
204✔
635
    }
636

637
    /**
638
     * Captures the builder's set() method so that we can validate the
639
     * data here. This allows it to be used with any of the other
640
     * builder methods and still get validated data, like replace.
641
     *
642
     * @param object|row_array|string           $key    Field name, or an array of field/value pairs, or an object
643
     * @param bool|float|int|object|string|null $value  Field value, if $key is a single field
644
     * @param bool|null                         $escape Whether to escape values
645
     *
646
     * @return $this
647
     */
648
    public function set($key, $value = '', ?bool $escape = null)
649
    {
650
        if (is_object($key)) {
10✔
651
            $key = $key instanceof stdClass ? (array) $key : $this->objectToArray($key);
2✔
652
        }
653

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

656
        foreach (array_keys($data) as $k) {
10✔
657
            $this->tempData['escape'][$k] = $escape;
10✔
658
        }
659

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

662
        return $this;
10✔
663
    }
664

665
    protected function shouldUpdate($row): bool
666
    {
667
        if (parent::shouldUpdate($row) === false) {
27✔
668
            return false;
12✔
669
        }
670

671
        if ($this->useAutoIncrement === true) {
17✔
672
            return true;
14✔
673
        }
674

675
        // When useAutoIncrement feature is disabled, check
676
        // in the database if given record already exists
677
        return $this->where($this->primaryKey, $this->getIdValue($row))->countAllResults() === 1;
3✔
678
    }
679

680
    public function insert($row = null, bool $returnID = true)
681
    {
682
        if (isset($this->tempData['data'])) {
119✔
683
            if ($row === null) {
2✔
684
                $row = $this->tempData['data'];
1✔
685
            } else {
686
                $row = $this->transformDataToArray($row, 'insert');
1✔
687
                $row = array_merge($this->tempData['data'], $row);
1✔
688
            }
689
        }
690

691
        $this->escape   = $this->tempData['escape'] ?? [];
119✔
692
        $this->tempData = [];
119✔
693

694
        return parent::insert($row, $returnID);
119✔
695
    }
696

697
    protected function doProtectFieldsForInsert(array $row): array
698
    {
699
        if (! $this->protectFields) {
121✔
700
            return $row;
9✔
701
        }
702

703
        if ($this->allowedFields === []) {
112✔
704
            throw DataException::forInvalidAllowedFields(static::class);
1✔
705
        }
706

707
        foreach (array_keys($row) as $key) {
111✔
708
            // Do not remove the non-auto-incrementing primary key data.
709
            if ($this->useAutoIncrement === false && $key === $this->primaryKey) {
110✔
710
                continue;
25✔
711
            }
712

713
            if (! in_array($key, $this->allowedFields, true)) {
110✔
714
                unset($row[$key]);
25✔
715
            }
716
        }
717

718
        return $row;
111✔
719
    }
720

721
    public function update($id = null, $row = null): bool
722
    {
723
        if (isset($this->tempData['data'])) {
58✔
724
            if ($row === null) {
6✔
725
                $row = $this->tempData['data'];
5✔
726
            } else {
727
                $row = $this->transformDataToArray($row, 'update');
1✔
728
                $row = array_merge($this->tempData['data'], $row);
1✔
729
            }
730
        }
731

732
        $this->escape   = $this->tempData['escape'] ?? [];
58✔
733
        $this->tempData = [];
58✔
734

735
        return parent::update($id, $row);
58✔
736
    }
737

738
    protected function objectToRawArray($object, bool $onlyChanged = true, bool $recursive = false): array
739
    {
740
        return parent::objectToRawArray($object, $onlyChanged);
25✔
741
    }
742

743
    /**
744
     * Provides/instantiates the builder/db connection and model's table/primary key names and return type.
745
     *
746
     * @return array<int|string, mixed>|BaseBuilder|bool|float|int|object|string|null
747
     */
748
    public function __get(string $name)
749
    {
750
        if (parent::__isset($name)) {
50✔
751
            return parent::__get($name);
50✔
752
        }
753

754
        return $this->builder()->{$name} ?? null;
1✔
755
    }
756

757
    /**
758
     * Checks for the existence of properties across this model, builder, and db connection.
759
     */
760
    public function __isset(string $name): bool
761
    {
762
        if (parent::__isset($name)) {
47✔
763
            return true;
47✔
764
        }
765

766
        return isset($this->builder()->{$name});
1✔
767
    }
768

769
    /**
770
     * Provides direct access to method in the builder (if available)
771
     * and the database connection.
772
     *
773
     * @return $this|array<int|string, mixed>|BaseBuilder|bool|float|int|object|string|null
774
     */
775
    public function __call(string $name, array $params)
776
    {
777
        $builder = $this->builder();
48✔
778
        $result  = null;
48✔
779

780
        if (method_exists($this->db, $name)) {
48✔
781
            $result = $this->db->{$name}(...$params);
2✔
782
        } elseif (method_exists($builder, $name)) {
47✔
783
            $this->checkBuilderMethod($name);
46✔
784

785
            $result = $builder->{$name}(...$params);
44✔
786
        } else {
787
            throw new BadMethodCallException('Call to undefined method ' . static::class . '::' . $name);
1✔
788
        }
789

790
        if ($result instanceof BaseBuilder) {
45✔
791
            return $this;
44✔
792
        }
793

794
        return $result;
2✔
795
    }
796

797
    /**
798
     * Checks the Builder method name that should not be used in the Model.
799
     */
800
    private function checkBuilderMethod(string $name): void
801
    {
802
        if (in_array($name, $this->builderMethodsNotAvailable, true)) {
46✔
803
            throw ModelException::forMethodNotAvailable(static::class, $name . '()');
2✔
804
        }
805
    }
806
}
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