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

codeigniter4 / CodeIgniter4 / 8678307574

14 Apr 2024 04:14AM UTC coverage: 84.44%. Remained the same
8678307574

push

github

web-flow
Merge pull request #8783 from codeigniter4/develop

4.5.1 Ready code

24 of 32 new or added lines in 12 files covered. (75.0%)

164 existing lines in 12 files now uncovered.

20318 of 24062 relevant lines covered (84.44%)

188.23 hits per line

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

97.92
/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 BadMethodCallException;
17
use Closure;
18
use CodeIgniter\Database\BaseBuilder;
19
use CodeIgniter\Database\BaseConnection;
20
use CodeIgniter\Database\BaseResult;
21
use CodeIgniter\Database\ConnectionInterface;
22
use CodeIgniter\Database\Exceptions\DatabaseException;
23
use CodeIgniter\Database\Exceptions\DataException;
24
use CodeIgniter\Database\Query;
25
use CodeIgniter\Entity\Entity;
26
use CodeIgniter\Exceptions\ModelException;
27
use CodeIgniter\Validation\ValidationInterface;
28
use Config\Database;
29
use Config\Feature;
30
use ReflectionException;
31
use stdClass;
32

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

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

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

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

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

131
    /**
132
     * Escape array that maps usage of escape
133
     * flag for every parameter.
134
     *
135
     * @var array
136
     */
137
    protected $escape = [];
138

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

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

157
        $this->db = $db;
306✔
158

159
        parent::__construct($validation);
306✔
160
    }
161

162
    /**
163
     * Specify the table associated with a model
164
     *
165
     * @param string $table Table
166
     *
167
     * @return $this
168
     */
169
    public function setTable(string $table)
170
    {
171
        $this->table = $table;
1✔
172

173
        return $this;
1✔
174
    }
175

176
    /**
177
     * Fetches the row of database from $this->table with a primary key
178
     * matching $id.
179
     * This method works only with dbCalls.
180
     *
181
     * @param bool                  $singleton Single or multiple results
182
     * @param array|int|string|null $id        One primary key or an array of primary keys
183
     *
184
     * @return         array|object|null                                                     The resulting row of data, or null.
185
     * @phpstan-return ($singleton is true ? row_array|null|object : list<row_array|object>)
186
     */
187
    protected function doFind(bool $singleton, $id = null)
188
    {
189
        $builder = $this->builder();
52✔
190

191
        $useCast = $this->useCasts();
51✔
192
        if ($useCast) {
51✔
193
            $returnType = $this->tempReturnType;
15✔
194
            $this->asArray();
15✔
195
        }
196

197
        if ($this->tempUseSoftDeletes) {
51✔
198
            $builder->where($this->table . '.' . $this->deletedField, null);
28✔
199
        }
200

201
        if (is_array($id)) {
51✔
202
            $row = $builder->whereIn($this->table . '.' . $this->primaryKey, $id)
2✔
203
                ->get()
2✔
204
                ->getResult($this->tempReturnType);
2✔
205
        } elseif ($singleton) {
49✔
206
            $row = $builder->where($this->table . '.' . $this->primaryKey, $id)
42✔
207
                ->get()
42✔
208
                ->getFirstRow($this->tempReturnType);
42✔
209
        } else {
210
            $row = $builder->get()->getResult($this->tempReturnType);
8✔
211
        }
212

213
        if ($useCast) {
51✔
214
            $row = $this->convertToReturnType($row, $returnType);
15✔
215

216
            $this->tempReturnType = $returnType;
15✔
217
        }
218

219
        return $row;
51✔
220
    }
221

222
    /**
223
     * Fetches the column of database from $this->table.
224
     * This method works only with dbCalls.
225
     *
226
     * @param string $columnName Column Name
227
     *
228
     * @return         array|null           The resulting row of data, or null if no data found.
229
     * @phpstan-return list<row_array>|null
230
     */
231
    protected function doFindColumn(string $columnName)
232
    {
233
        $results = $this->select($columnName)->asArray()->find();
2✔
234

235
        if ($this->useCasts()) {
2✔
236
            foreach ($results as $i => $row) {
1✔
237
                $results[$i] = $this->converter->fromDataSource($row);
1✔
238
            }
239
        }
240

241
        return $results;
2✔
242
    }
243

244
    /**
245
     * Works with the current Query Builder instance to return
246
     * all results, while optionally limiting them.
247
     * This method works only with dbCalls.
248
     *
249
     * @param int|null $limit  Limit
250
     * @param int      $offset Offset
251
     *
252
     * @return         array
253
     * @phpstan-return list<row_array|object>
254
     */
255
    protected function doFindAll(?int $limit = null, int $offset = 0)
256
    {
257
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
21✔
258
        if ($limitZeroAsAll) {
21✔
259
            $limit ??= 0;
21✔
260
        }
261

262
        $builder = $this->builder();
21✔
263

264
        $useCast = $this->useCasts();
21✔
265
        if ($useCast) {
21✔
266
            $returnType = $this->tempReturnType;
4✔
267
            $this->asArray();
4✔
268
        }
269

270
        if ($this->tempUseSoftDeletes) {
21✔
271
            $builder->where($this->table . '.' . $this->deletedField, null);
9✔
272
        }
273

274
        $results = $builder->limit($limit, $offset)
21✔
275
            ->get()
21✔
276
            ->getResult($this->tempReturnType);
21✔
277

278
        if ($useCast) {
21✔
279
            foreach ($results as $i => $row) {
4✔
280
                $results[$i] = $this->convertToReturnType($row, $returnType);
4✔
281
            }
282

283
            $this->tempReturnType = $returnType;
4✔
284
        }
285

286
        return $results;
21✔
287
    }
288

289
    /**
290
     * Returns the first row of the result set. Will take any previous
291
     * Query Builder calls into account when determining the result set.
292
     * This method works only with dbCalls.
293
     *
294
     * @return         array|object|null
295
     * @phpstan-return row_array|object|null
296
     */
297
    protected function doFirst()
298
    {
299
        $builder = $this->builder();
23✔
300

301
        $useCast = $this->useCasts();
23✔
302
        if ($useCast) {
23✔
303
            $returnType = $this->tempReturnType;
4✔
304
            $this->asArray();
4✔
305
        }
306

307
        if ($this->tempUseSoftDeletes) {
23✔
308
            $builder->where($this->table . '.' . $this->deletedField, null);
18✔
309
        } elseif ($this->useSoftDeletes && ($builder->QBGroupBy === []) && $this->primaryKey) {
13✔
310
            $builder->groupBy($this->table . '.' . $this->primaryKey);
6✔
311
        }
312

313
        // Some databases, like PostgreSQL, need order
314
        // information to consistently return correct results.
315
        if ($builder->QBGroupBy && ($builder->QBOrderBy === []) && $this->primaryKey) {
23✔
316
            $builder->orderBy($this->table . '.' . $this->primaryKey, 'asc');
9✔
317
        }
318

319
        $row = $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType);
23✔
320

321
        if ($useCast) {
23✔
322
            $row = $this->convertToReturnType($row, $returnType);
4✔
323

324
            $this->tempReturnType = $returnType;
4✔
325
        }
326

327
        return $row;
23✔
328
    }
329

330
    /**
331
     * Inserts data into the current table.
332
     * This method works only with dbCalls.
333
     *
334
     * @param         array     $row Row data
335
     * @phpstan-param row_array $row
336
     *
337
     * @return bool
338
     */
339
    protected function doInsert(array $row)
340
    {
341
        $escape       = $this->escape;
82✔
342
        $this->escape = [];
82✔
343

344
        // Require non-empty primaryKey when
345
        // not using auto-increment feature
346
        if (! $this->useAutoIncrement && ! isset($row[$this->primaryKey])) {
82✔
347
            throw DataException::forEmptyPrimaryKey('insert');
1✔
348
        }
349

350
        $builder = $this->builder();
81✔
351

352
        // Must use the set() method to ensure to set the correct escape flag
353
        foreach ($row as $key => $val) {
81✔
354
            $builder->set($key, $val, $escape[$key] ?? null);
80✔
355
        }
356

357
        if ($this->allowEmptyInserts && $row === []) {
81✔
358
            $table = $this->db->protectIdentifiers($this->table, true, null, false);
1✔
359
            if ($this->db->getPlatform() === 'MySQLi') {
1✔
360
                $sql = 'INSERT INTO ' . $table . ' VALUES ()';
1✔
361
            } elseif ($this->db->getPlatform() === 'OCI8') {
1✔
362
                $allFields = $this->db->protectIdentifiers(
1✔
363
                    array_map(
1✔
364
                        static fn ($row) => $row->name,
1✔
365
                        $this->db->getFieldData($this->table)
1✔
366
                    ),
1✔
367
                    false,
1✔
368
                    true
1✔
369
                );
1✔
370

371
                $sql = sprintf(
1✔
372
                    'INSERT INTO %s (%s) VALUES (%s)',
1✔
373
                    $table,
1✔
374
                    implode(',', $allFields),
1✔
375
                    substr(str_repeat(',DEFAULT', count($allFields)), 1)
1✔
376
                );
1✔
377
            } else {
378
                $sql = 'INSERT INTO ' . $table . ' DEFAULT VALUES';
1✔
379
            }
380

381
            $result = $this->db->query($sql);
1✔
382
        } else {
383
            $result = $builder->insert();
80✔
384
        }
385

386
        // If insertion succeeded then save the insert ID
387
        if ($result) {
81✔
388
            $this->insertID = ! $this->useAutoIncrement ? $row[$this->primaryKey] : $this->db->insertID();
79✔
389
        }
390

391
        return $result;
81✔
392
    }
393

394
    /**
395
     * Compiles batch insert strings and runs the queries, validating each row prior.
396
     * This method works only with dbCalls.
397
     *
398
     * @param array|null $set       An associative array of insert values
399
     * @param bool|null  $escape    Whether to escape values
400
     * @param int        $batchSize The size of the batch to run
401
     * @param bool       $testing   True means only number of records is returned, false will execute the query
402
     *
403
     * @return bool|int Number of rows inserted or FALSE on failure
404
     */
405
    protected function doInsertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100, bool $testing = false)
406
    {
407
        if (is_array($set)) {
10✔
408
            foreach ($set as $row) {
10✔
409
                // Require non-empty primaryKey when
410
                // not using auto-increment feature
411
                if (! $this->useAutoIncrement && ! isset($row[$this->primaryKey])) {
10✔
UNCOV
412
                    throw DataException::forEmptyPrimaryKey('insertBatch');
×
413
                }
414
            }
415
        }
416

417
        return $this->builder()->testMode($testing)->insertBatch($set, $escape, $batchSize);
10✔
418
    }
419

420
    /**
421
     * Updates a single record in $this->table.
422
     * This method works only with dbCalls.
423
     *
424
     * @param         array|int|string|null $id
425
     * @param         array|null            $row Row data
426
     * @phpstan-param row_array|null        $row
427
     */
428
    protected function doUpdate($id = null, $row = null): bool
429
    {
430
        $escape       = $this->escape;
36✔
431
        $this->escape = [];
36✔
432

433
        $builder = $this->builder();
36✔
434

435
        if ($id) {
36✔
436
            $builder = $builder->whereIn($this->table . '.' . $this->primaryKey, $id);
30✔
437
        }
438

439
        // Must use the set() method to ensure to set the correct escape flag
440
        foreach ($row as $key => $val) {
36✔
441
            $builder->set($key, $val, $escape[$key] ?? null);
36✔
442
        }
443

444
        if ($builder->getCompiledQBWhere() === []) {
36✔
445
            throw new DatabaseException(
1✔
446
                'Updates are not allowed unless they contain a "where" or "like" clause.'
1✔
447
            );
1✔
448
        }
449

450
        return $builder->update();
35✔
451
    }
452

453
    /**
454
     * Compiles an update string and runs the query
455
     * This method works only with dbCalls.
456
     *
457
     * @param array|null  $set       An associative array of update values
458
     * @param string|null $index     The where key
459
     * @param int         $batchSize The size of the batch to run
460
     * @param bool        $returnSQL True means SQL is returned, false will execute the query
461
     *
462
     * @return false|int|list<string> Number of rows affected or FALSE on failure, SQL array when testMode
463
     *
464
     * @throws DatabaseException
465
     */
466
    protected function doUpdateBatch(?array $set = null, ?string $index = null, int $batchSize = 100, bool $returnSQL = false)
467
    {
468
        return $this->builder()->testMode($returnSQL)->updateBatch($set, $index, $batchSize);
3✔
469
    }
470

471
    /**
472
     * Deletes a single record from $this->table where $id matches
473
     * the table's primaryKey
474
     * This method works only with dbCalls.
475
     *
476
     * @param array|int|string|null $id    The rows primary key(s)
477
     * @param bool                  $purge Allows overriding the soft deletes setting.
478
     *
479
     * @return bool|string SQL string when testMode
480
     *
481
     * @throws DatabaseException
482
     */
483
    protected function doDelete($id = null, bool $purge = false)
484
    {
485
        $set     = [];
40✔
486
        $builder = $this->builder();
40✔
487

488
        if ($id) {
40✔
489
            $builder = $builder->whereIn($this->primaryKey, $id);
21✔
490
        }
491

492
        if ($this->useSoftDeletes && ! $purge) {
40✔
493
            if ($builder->getCompiledQBWhere() === []) {
28✔
494
                throw new DatabaseException(
9✔
495
                    'Deletes are not allowed unless they contain a "where" or "like" clause.'
9✔
496
                );
9✔
497
            }
498

499
            $builder->where($this->deletedField);
19✔
500

501
            $set[$this->deletedField] = $this->setDate();
19✔
502

503
            if ($this->useTimestamps && $this->updatedField !== '') {
18✔
504
                $set[$this->updatedField] = $this->setDate();
1✔
505
            }
506

507
            return $builder->update($set);
18✔
508
        }
509

510
        return $builder->delete();
12✔
511
    }
512

513
    /**
514
     * Permanently deletes all rows that have been marked as deleted
515
     * through soft deletes (deleted = 1)
516
     * This method works only with dbCalls.
517
     *
518
     * @return bool|string Returns a SQL string if in test mode.
519
     */
520
    protected function doPurgeDeleted()
521
    {
522
        return $this->builder()
1✔
523
            ->where($this->table . '.' . $this->deletedField . ' IS NOT NULL')
1✔
524
            ->delete();
1✔
525
    }
526

527
    /**
528
     * Works with the find* methods to return only the rows that
529
     * have been deleted.
530
     * This method works only with dbCalls.
531
     *
532
     * @return void
533
     */
534
    protected function doOnlyDeleted()
535
    {
536
        $this->builder()->where($this->table . '.' . $this->deletedField . ' IS NOT NULL');
1✔
537
    }
538

539
    /**
540
     * Compiles a replace into string and runs the query
541
     * This method works only with dbCalls.
542
     *
543
     * @param         array|null     $row       Data
544
     * @phpstan-param row_array|null $row
545
     * @param         bool           $returnSQL Set to true to return Query String
546
     *
547
     * @return BaseResult|false|Query|string
548
     */
549
    protected function doReplace(?array $row = null, bool $returnSQL = false)
550
    {
551
        return $this->builder()->testMode($returnSQL)->replace($row);
2✔
552
    }
553

554
    /**
555
     * Grabs the last error(s) that occurred from the Database connection.
556
     * The return array should be in the following format:
557
     *  ['source' => 'message']
558
     * This method works only with dbCalls.
559
     *
560
     * @return array<string, string>
561
     */
562
    protected function doErrors()
563
    {
564
        // $error is always ['code' => string|int, 'message' => string]
565
        $error = $this->db->error();
2✔
566

567
        if ((int) $error['code'] === 0) {
2✔
568
            return [];
2✔
569
        }
570

UNCOV
571
        return [$this->db::class => $error['message']];
×
572
    }
573

574
    /**
575
     * Returns the id value for the data array or object
576
     *
577
     * @param         array|object     $row Row data
578
     * @phpstan-param row_array|object $row
579
     *
580
     * @return array|int|string|null
581
     */
582
    public function getIdValue($row)
583
    {
584
        if (is_object($row) && isset($row->{$this->primaryKey})) {
26✔
585
            // Get the raw primary key value of the Entity.
586
            if ($row instanceof Entity) {
13✔
587
                $cast = $row->cast();
8✔
588

589
                // Disable Entity casting, because raw primary key value is needed for database.
590
                $row->cast(false);
8✔
591

592
                $primaryKey = $row->{$this->primaryKey};
8✔
593

594
                // Restore Entity casting setting.
595
                $row->cast($cast);
8✔
596

597
                return $primaryKey;
8✔
598
            }
599

600
            return $row->{$this->primaryKey};
5✔
601
        }
602

603
        if (is_array($row) && isset($row[$this->primaryKey])) {
14✔
604
            return $row[$this->primaryKey];
4✔
605
        }
606

607
        return null;
10✔
608
    }
609

610
    /**
611
     * Loops over records in batches, allowing you to operate on them.
612
     * Works with $this->builder to get the Compiled select to
613
     * determine the rows to operate on.
614
     * This method works only with dbCalls.
615
     *
616
     * @return void
617
     *
618
     * @throws DataException
619
     */
620
    public function chunk(int $size, Closure $userFunc)
621
    {
622
        $total  = $this->builder()->countAllResults(false);
1✔
623
        $offset = 0;
1✔
624

625
        while ($offset <= $total) {
1✔
626
            $builder = clone $this->builder();
1✔
627
            $rows    = $builder->get($size, $offset);
1✔
628

629
            if (! $rows) {
1✔
UNCOV
630
                throw DataException::forEmptyDataset('chunk');
×
631
            }
632

633
            $rows = $rows->getResult($this->tempReturnType);
1✔
634

635
            $offset += $size;
1✔
636

637
            if ($rows === []) {
1✔
638
                continue;
1✔
639
            }
640

641
            foreach ($rows as $row) {
1✔
642
                if ($userFunc($row) === false) {
1✔
UNCOV
643
                    return;
×
644
                }
645
            }
646
        }
647
    }
648

649
    /**
650
     * Override countAllResults to account for soft deleted accounts.
651
     *
652
     * @return int|string
653
     */
654
    public function countAllResults(bool $reset = true, bool $test = false)
655
    {
656
        if ($this->tempUseSoftDeletes) {
17✔
657
            $this->builder()->where($this->table . '.' . $this->deletedField, null);
6✔
658
        }
659

660
        // When $reset === false, the $tempUseSoftDeletes will be
661
        // dependent on $useSoftDeletes value because we don't
662
        // want to add the same "where" condition for the second time
663
        $this->tempUseSoftDeletes = $reset
17✔
664
            ? $this->useSoftDeletes
10✔
665
            : ($this->useSoftDeletes ? false : $this->useSoftDeletes);
12✔
666

667
        return $this->builder()->testMode($test)->countAllResults($reset);
17✔
668
    }
669

670
    /**
671
     * Provides a shared instance of the Query Builder.
672
     *
673
     * @param non-empty-string|null $table
674
     *
675
     * @return BaseBuilder
676
     *
677
     * @throws ModelException
678
     */
679
    public function builder(?string $table = null)
680
    {
681
        // Check for an existing Builder
682
        if ($this->builder instanceof BaseBuilder) {
191✔
683
            // Make sure the requested table matches the builder
684
            if ($table && $this->builder->getTable() !== $table) {
110✔
685
                return $this->db->table($table);
1✔
686
            }
687

688
            return $this->builder;
110✔
689
        }
690

691
        // We're going to force a primary key to exist
692
        // so we don't have overly convoluted code,
693
        // and future features are likely to require them.
694
        if ($this->primaryKey === '') {
191✔
695
            throw ModelException::forNoPrimaryKey(static::class);
1✔
696
        }
697

698
        $table = ($table === null || $table === '') ? $this->table : $table;
190✔
699

700
        // Ensure we have a good db connection
701
        if (! $this->db instanceof BaseConnection) {
190✔
UNCOV
702
            $this->db = Database::connect($this->DBGroup);
×
703
        }
704

705
        $builder = $this->db->table($table);
190✔
706

707
        // Only consider it "shared" if the table is correct
708
        if ($table === $this->table) {
190✔
709
            $this->builder = $builder;
190✔
710
        }
711

712
        return $builder;
190✔
713
    }
714

715
    /**
716
     * Captures the builder's set() method so that we can validate the
717
     * data here. This allows it to be used with any of the other
718
     * builder methods and still get validated data, like replace.
719
     *
720
     * @param array|object|string               $key    Field name, or an array of field/value pairs, or an object
721
     * @param bool|float|int|object|string|null $value  Field value, if $key is a single field
722
     * @param bool|null                         $escape Whether to escape values
723
     *
724
     * @return $this
725
     */
726
    public function set($key, $value = '', ?bool $escape = null)
727
    {
728
        if (is_object($key)) {
10✔
729
            $key = $key instanceof stdClass ? (array) $key : $this->objectToArray($key);
2✔
730
        }
731

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

734
        foreach (array_keys($data) as $k) {
10✔
735
            $this->tempData['escape'][$k] = $escape;
10✔
736
        }
737

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

740
        return $this;
10✔
741
    }
742

743
    /**
744
     * This method is called on save to determine if entry have to be updated
745
     * If this method return false insert operation will be executed
746
     *
747
     * @param array|object $row Data
748
     */
749
    protected function shouldUpdate($row): bool
750
    {
751
        if (parent::shouldUpdate($row) === false) {
26✔
752
            return false;
11✔
753
        }
754

755
        if ($this->useAutoIncrement === true) {
16✔
756
            return true;
13✔
757
        }
758

759
        // When useAutoIncrement feature is disabled, check
760
        // in the database if given record already exists
761
        return $this->where($this->primaryKey, $this->getIdValue($row))->countAllResults() === 1;
3✔
762
    }
763

764
    /**
765
     * Inserts data into the database. If an object is provided,
766
     * it will attempt to convert it to an array.
767
     *
768
     * @param         array|object|null     $row
769
     * @phpstan-param row_array|object|null $row
770
     * @param         bool                  $returnID Whether insert ID should be returned or not.
771
     *
772
     * @return         bool|int|string
773
     * @phpstan-return ($returnID is true ? int|string|false : bool)
774
     *
775
     * @throws ReflectionException
776
     */
777
    public function insert($row = null, bool $returnID = true)
778
    {
779
        if (isset($this->tempData['data'])) {
105✔
780
            if ($row === null) {
2✔
781
                $row = $this->tempData['data'];
1✔
782
            } else {
783
                $row = $this->transformDataToArray($row, 'insert');
1✔
784
                $row = array_merge($this->tempData['data'], $row);
1✔
785
            }
786
        }
787

788
        $this->escape   = $this->tempData['escape'] ?? [];
105✔
789
        $this->tempData = [];
105✔
790

791
        return parent::insert($row, $returnID);
105✔
792
    }
793

794
    /**
795
     * Ensures that only the fields that are allowed to be inserted are in
796
     * the data array.
797
     *
798
     * @used-by insert() to protect against mass assignment vulnerabilities.
799
     * @used-by insertBatch() to protect against mass assignment vulnerabilities.
800
     *
801
     * @param         array     $row Row data
802
     * @phpstan-param row_array $row
803
     *
804
     * @throws DataException
805
     */
806
    protected function doProtectFieldsForInsert(array $row): array
807
    {
808
        if (! $this->protectFields) {
96✔
809
            return $row;
9✔
810
        }
811

812
        if ($this->allowedFields === []) {
87✔
813
            throw DataException::forInvalidAllowedFields(static::class);
1✔
814
        }
815

816
        foreach (array_keys($row) as $key) {
86✔
817
            // Do not remove the non-auto-incrementing primary key data.
818
            if ($this->useAutoIncrement === false && $key === $this->primaryKey) {
85✔
819
                continue;
5✔
820
            }
821

822
            if (! in_array($key, $this->allowedFields, true)) {
85✔
823
                unset($row[$key]);
24✔
824
            }
825
        }
826

827
        return $row;
86✔
828
    }
829

830
    /**
831
     * Updates a single record in the database. If an object is provided,
832
     * it will attempt to convert it into an array.
833
     *
834
     * @param         array|int|string|null $id
835
     * @param         array|object|null     $row
836
     * @phpstan-param row_array|object|null $row
837
     *
838
     * @throws ReflectionException
839
     */
840
    public function update($id = null, $row = null): bool
841
    {
842
        if (isset($this->tempData['data'])) {
45✔
843
            if ($row === null) {
6✔
844
                $row = $this->tempData['data'];
5✔
845
            } else {
846
                $row = $this->transformDataToArray($row, 'update');
1✔
847
                $row = array_merge($this->tempData['data'], $row);
1✔
848
            }
849
        }
850

851
        $this->escape   = $this->tempData['escape'] ?? [];
45✔
852
        $this->tempData = [];
45✔
853

854
        return parent::update($id, $row);
45✔
855
    }
856

857
    /**
858
     * Takes a class and returns an array of its public and protected
859
     * properties as an array with raw values.
860
     *
861
     * @param object $object    Object
862
     * @param bool   $recursive If true, inner entities will be cast as array as well
863
     *
864
     * @return array<string, mixed> Array with raw values.
865
     *
866
     * @throws ReflectionException
867
     */
868
    protected function objectToRawArray($object, bool $onlyChanged = true, bool $recursive = false): array
869
    {
870
        return parent::objectToRawArray($object, $onlyChanged);
22✔
871
    }
872

873
    /**
874
     * Provides/instantiates the builder/db connection and model's table/primary key names and return type.
875
     *
876
     * @param string $name Name
877
     *
878
     * @return array|BaseBuilder|bool|float|int|object|string|null
879
     */
880
    public function __get(string $name)
881
    {
882
        if (parent::__isset($name)) {
48✔
883
            return parent::__get($name);
48✔
884
        }
885

886
        return $this->builder()->{$name} ?? null;
1✔
887
    }
888

889
    /**
890
     * Checks for the existence of properties across this model, builder, and db connection.
891
     *
892
     * @param string $name Name
893
     */
894
    public function __isset(string $name): bool
895
    {
896
        if (parent::__isset($name)) {
47✔
897
            return true;
47✔
898
        }
899

900
        return isset($this->builder()->{$name});
1✔
901
    }
902

903
    /**
904
     * Provides direct access to method in the builder (if available)
905
     * and the database connection.
906
     *
907
     * @return $this|array|BaseBuilder|bool|float|int|object|string|null
908
     */
909
    public function __call(string $name, array $params)
910
    {
911
        $builder = $this->builder();
46✔
912
        $result  = null;
46✔
913

914
        if (method_exists($this->db, $name)) {
46✔
915
            $result = $this->db->{$name}(...$params);
2✔
916
        } elseif (method_exists($builder, $name)) {
45✔
917
            $this->checkBuilderMethod($name);
44✔
918

919
            $result = $builder->{$name}(...$params);
42✔
920
        } else {
921
            throw new BadMethodCallException('Call to undefined method ' . static::class . '::' . $name);
1✔
922
        }
923

924
        if ($result instanceof BaseBuilder) {
43✔
925
            return $this;
41✔
926
        }
927

928
        return $result;
3✔
929
    }
930

931
    /**
932
     * Checks the Builder method name that should not be used in the Model.
933
     */
934
    private function checkBuilderMethod(string $name): void
935
    {
936
        if (in_array($name, $this->builderMethodsNotAvailable, true)) {
44✔
937
            throw ModelException::forMethodNotAvailable(static::class, $name . '()');
2✔
938
        }
939
    }
940
}
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