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

codeigniter4 / CodeIgniter4 / 7298596047

22 Dec 2023 10:01AM UTC coverage: 85.049% (+0.004%) from 85.045%
7298596047

push

github

web-flow
Merge pull request #8359 from kenjis/docs-FileLocatorInterface

[4.5] fix: merge mistake

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

147 existing lines in 11 files now uncovered.

19358 of 22761 relevant lines covered (85.05%)

193.82 hits per line

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

97.61
/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

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 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
126
     */
127
    protected $tempData = [];
128

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

137
    /**
138
     * Builder method names that should not be used in the Model.
139
     *
140
     * @var string[] method name
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);
275✔
154

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

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

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

171
        return $this;
1✔
172
    }
173

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

189
        if ($this->tempUseSoftDeletes) {
34✔
190
            $builder->where($this->table . '.' . $this->deletedField, null);
11✔
191
        }
192

193
        if (is_array($id)) {
34✔
194
            $row = $builder->whereIn($this->table . '.' . $this->primaryKey, $id)
2✔
195
                ->get()
2✔
196
                ->getResult($this->tempReturnType);
2✔
197
        } elseif ($singleton) {
32✔
198
            $row = $builder->where($this->table . '.' . $this->primaryKey, $id)
26✔
199
                ->get()
26✔
200
                ->getFirstRow($this->tempReturnType);
26✔
201
        } else {
202
            $row = $builder->get()->getResult($this->tempReturnType);
7✔
203
        }
204

205
        return $row;
34✔
206
    }
207

208
    /**
209
     * Fetches the column of database from $this->table.
210
     * This method works only with dbCalls.
211
     *
212
     * @param string $columnName Column Name
213
     *
214
     * @return array|null The resulting row of data, or null if no data found.
215
     * @phpstan-return list<row_array>|null
216
     */
217
    protected function doFindColumn(string $columnName)
218
    {
219
        return $this->select($columnName)->asArray()->find();
1✔
220
    }
221

222
    /**
223
     * Works with the current Query Builder instance to return
224
     * all results, while optionally limiting them.
225
     * This method works only with dbCalls.
226
     *
227
     * @param int|null $limit  Limit
228
     * @param int      $offset Offset
229
     *
230
     * @return array
231
     * @phpstan-return list<row_array|object>
232
     */
233
    protected function doFindAll(?int $limit = null, int $offset = 0)
234
    {
235
        if (config(Feature::class)->limitZeroAsAll) {
17✔
236
            $limit ??= 0;
17✔
237
        }
238

239
        $builder = $this->builder();
17✔
240

241
        if ($this->tempUseSoftDeletes) {
17✔
242
            $builder->where($this->table . '.' . $this->deletedField, null);
5✔
243
        }
244

245
        return $builder->limit($limit, $offset)
17✔
246
            ->get()
17✔
247
            ->getResult($this->tempReturnType);
17✔
248
    }
249

250
    /**
251
     * Returns the first row of the result set. Will take any previous
252
     * Query Builder calls into account when determining the result set.
253
     * This method works only with dbCalls.
254
     *
255
     * @return array|object|null
256
     * @phpstan-return row_array|object|null
257
     */
258
    protected function doFirst()
259
    {
260
        $builder = $this->builder();
19✔
261

262
        if ($this->tempUseSoftDeletes) {
19✔
263
            $builder->where($this->table . '.' . $this->deletedField, null);
14✔
264
        } elseif ($this->useSoftDeletes && empty($builder->QBGroupBy) && $this->primaryKey) {
13✔
265
            $builder->groupBy($this->table . '.' . $this->primaryKey);
6✔
266
        }
267

268
        // Some databases, like PostgreSQL, need order
269
        // information to consistently return correct results.
270
        if ($builder->QBGroupBy && empty($builder->QBOrderBy) && $this->primaryKey) {
19✔
271
            $builder->orderBy($this->table . '.' . $this->primaryKey, 'asc');
9✔
272
        }
273

274
        return $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType);
19✔
275
    }
276

277
    /**
278
     * Inserts data into the current table.
279
     * This method works only with dbCalls.
280
     *
281
     * @param array $row Row data
282
     * @phpstan-param row_array $row
283
     *
284
     * @return bool
285
     */
286
    protected function doInsert(array $row)
287
    {
288
        $escape       = $this->escape;
56✔
289
        $this->escape = [];
56✔
290

291
        // Require non-empty primaryKey when
292
        // not using auto-increment feature
293
        if (! $this->useAutoIncrement && empty($row[$this->primaryKey])) {
56✔
294
            throw DataException::forEmptyPrimaryKey('insert');
1✔
295
        }
296

297
        $builder = $this->builder();
55✔
298

299
        // Must use the set() method to ensure to set the correct escape flag
300
        foreach ($row as $key => $val) {
55✔
301
            $builder->set($key, $val, $escape[$key] ?? null);
54✔
302
        }
303

304
        if ($this->allowEmptyInserts && $row === []) {
55✔
305
            $table = $this->db->protectIdentifiers($this->table, true, null, false);
1✔
306
            if ($this->db->getPlatform() === 'MySQLi') {
1✔
307
                $sql = 'INSERT INTO ' . $table . ' VALUES ()';
1✔
308
            } elseif ($this->db->getPlatform() === 'OCI8') {
1✔
309
                $allFields = $this->db->protectIdentifiers(
1✔
310
                    array_map(
1✔
311
                        static fn ($row) => $row->name,
1✔
312
                        $this->db->getFieldData($this->table)
1✔
313
                    ),
1✔
314
                    false,
1✔
315
                    true
1✔
316
                );
1✔
317

318
                $sql = sprintf(
1✔
319
                    'INSERT INTO %s (%s) VALUES (%s)',
1✔
320
                    $table,
1✔
321
                    implode(',', $allFields),
1✔
322
                    substr(str_repeat(',DEFAULT', count($allFields)), 1)
1✔
323
                );
1✔
324
            } else {
325
                $sql = 'INSERT INTO ' . $table . ' DEFAULT VALUES';
1✔
326
            }
327

328
            $result = $this->db->query($sql);
1✔
329
        } else {
330
            $result = $builder->insert();
54✔
331
        }
332

333
        // If insertion succeeded then save the insert ID
334
        if ($result) {
55✔
335
            $this->insertID = ! $this->useAutoIncrement ? $row[$this->primaryKey] : $this->db->insertID();
53✔
336
        }
337

338
        return $result;
55✔
339
    }
340

341
    /**
342
     * Compiles batch insert strings and runs the queries, validating each row prior.
343
     * This method works only with dbCalls.
344
     *
345
     * @param array|null $set       An associative array of insert values
346
     * @param bool|null  $escape    Whether to escape values
347
     * @param int        $batchSize The size of the batch to run
348
     * @param bool       $testing   True means only number of records is returned, false will execute the query
349
     *
350
     * @return bool|int Number of rows inserted or FALSE on failure
351
     */
352
    protected function doInsertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100, bool $testing = false)
353
    {
354
        if (is_array($set)) {
10✔
355
            foreach ($set as $row) {
10✔
356
                // Require non-empty primaryKey when
357
                // not using auto-increment feature
358
                if (! $this->useAutoIncrement && empty($row[$this->primaryKey])) {
10✔
359
                    throw DataException::forEmptyPrimaryKey('insertBatch');
×
360
                }
361
            }
362
        }
363

364
        return $this->builder()->testMode($testing)->insertBatch($set, $escape, $batchSize);
10✔
365
    }
366

367
    /**
368
     * Updates a single record in $this->table.
369
     * This method works only with dbCalls.
370
     *
371
     * @param array|int|string|null $id
372
     * @param array|null            $row Row data
373
     * @phpstan-param row_array|null $row
374
     */
375
    protected function doUpdate($id = null, $row = null): bool
376
    {
377
        $escape       = $this->escape;
25✔
378
        $this->escape = [];
25✔
379

380
        $builder = $this->builder();
25✔
381

382
        if ($id) {
25✔
383
            $builder = $builder->whereIn($this->table . '.' . $this->primaryKey, $id);
21✔
384
        }
385

386
        // Must use the set() method to ensure to set the correct escape flag
387
        foreach ($row as $key => $val) {
25✔
388
            $builder->set($key, $val, $escape[$key] ?? null);
25✔
389
        }
390

391
        if ($builder->getCompiledQBWhere() === []) {
25✔
392
            throw new DatabaseException(
1✔
393
                'Updates are not allowed unless they contain a "where" or "like" clause.'
1✔
394
            );
1✔
395
        }
396

397
        return $builder->update();
24✔
398
    }
399

400
    /**
401
     * Compiles an update string and runs the query
402
     * This method works only with dbCalls.
403
     *
404
     * @param array|null  $set       An associative array of update values
405
     * @param string|null $index     The where key
406
     * @param int         $batchSize The size of the batch to run
407
     * @param bool        $returnSQL True means SQL is returned, false will execute the query
408
     *
409
     * @return false|int|string[] Number of rows affected or FALSE on failure, SQL array when testMode
410
     *
411
     * @throws DatabaseException
412
     */
413
    protected function doUpdateBatch(?array $set = null, ?string $index = null, int $batchSize = 100, bool $returnSQL = false)
414
    {
415
        return $this->builder()->testMode($returnSQL)->updateBatch($set, $index, $batchSize);
3✔
416
    }
417

418
    /**
419
     * Deletes a single record from $this->table where $id matches
420
     * the table's primaryKey
421
     * This method works only with dbCalls.
422
     *
423
     * @param array|int|string|null $id    The rows primary key(s)
424
     * @param bool                  $purge Allows overriding the soft deletes setting.
425
     *
426
     * @return bool|string SQL string when testMode
427
     *
428
     * @throws DatabaseException
429
     */
430
    protected function doDelete($id = null, bool $purge = false)
431
    {
432
        $set     = [];
40✔
433
        $builder = $this->builder();
40✔
434

435
        if ($id) {
40✔
436
            $builder = $builder->whereIn($this->primaryKey, $id);
21✔
437
        }
438

439
        if ($this->useSoftDeletes && ! $purge) {
40✔
440
            if (empty($builder->getCompiledQBWhere())) {
28✔
441
                throw new DatabaseException(
9✔
442
                    'Deletes are not allowed unless they contain a "where" or "like" clause.'
9✔
443
                );
9✔
444
            }
445

446
            $builder->where($this->deletedField);
19✔
447

448
            $set[$this->deletedField] = $this->setDate();
19✔
449

450
            if ($this->useTimestamps && $this->updatedField !== '') {
18✔
451
                $set[$this->updatedField] = $this->setDate();
1✔
452
            }
453

454
            return $builder->update($set);
18✔
455
        }
456

457
        return $builder->delete();
12✔
458
    }
459

460
    /**
461
     * Permanently deletes all rows that have been marked as deleted
462
     * through soft deletes (deleted = 1)
463
     * This method works only with dbCalls.
464
     *
465
     * @return bool|string Returns a SQL string if in test mode.
466
     */
467
    protected function doPurgeDeleted()
468
    {
469
        return $this->builder()
1✔
470
            ->where($this->table . '.' . $this->deletedField . ' IS NOT NULL')
1✔
471
            ->delete();
1✔
472
    }
473

474
    /**
475
     * Works with the find* methods to return only the rows that
476
     * have been deleted.
477
     * This method works only with dbCalls.
478
     *
479
     * @return void
480
     */
481
    protected function doOnlyDeleted()
482
    {
483
        $this->builder()->where($this->table . '.' . $this->deletedField . ' IS NOT NULL');
1✔
484
    }
485

486
    /**
487
     * Compiles a replace into string and runs the query
488
     * This method works only with dbCalls.
489
     *
490
     * @param array|null $row Data
491
     * @phpstan-param row_array|null $row
492
     * @param bool $returnSQL Set to true to return Query String
493
     *
494
     * @return BaseResult|false|Query|string
495
     */
496
    protected function doReplace(?array $row = null, bool $returnSQL = false)
497
    {
498
        return $this->builder()->testMode($returnSQL)->replace($row);
2✔
499
    }
500

501
    /**
502
     * Grabs the last error(s) that occurred from the Database connection.
503
     * The return array should be in the following format:
504
     *  ['source' => 'message']
505
     * This method works only with dbCalls.
506
     *
507
     * @return array<string, string>
508
     */
509
    protected function doErrors()
510
    {
511
        // $error is always ['code' => string|int, 'message' => string]
512
        $error = $this->db->error();
2✔
513

514
        if ((int) $error['code'] === 0) {
2✔
515
            return [];
2✔
516
        }
517

518
        return [get_class($this->db) => $error['message']];
×
519
    }
520

521
    /**
522
     * Returns the id value for the data array or object
523
     *
524
     * @param array|object $row Row data
525
     * @phpstan-param row_array|object $row
526
     *
527
     * @return array|int|string|null
528
     */
529
    public function getIdValue($row)
530
    {
531
        if (is_object($row) && isset($row->{$this->primaryKey})) {
21✔
532
            // Get the raw primary key value of the Entity.
533
            if ($row instanceof Entity) {
10✔
534
                $cast = $row->cast();
7✔
535

536
                // Disable Entity casting, because raw primary key value is needed for database.
537
                $row->cast(false);
7✔
538

539
                $primaryKey = $row->{$this->primaryKey};
7✔
540

541
                // Restore Entity casting setting.
542
                $row->cast($cast);
7✔
543

544
                return $primaryKey;
7✔
545
            }
546

547
            return $row->{$this->primaryKey};
3✔
548
        }
549

550
        if (is_array($row) && ! empty($row[$this->primaryKey])) {
12✔
551
            return $row[$this->primaryKey];
2✔
552
        }
553

554
        return null;
10✔
555
    }
556

557
    /**
558
     * Loops over records in batches, allowing you to operate on them.
559
     * Works with $this->builder to get the Compiled select to
560
     * determine the rows to operate on.
561
     * This method works only with dbCalls.
562
     *
563
     * @return void
564
     *
565
     * @throws DataException
566
     */
567
    public function chunk(int $size, Closure $userFunc)
568
    {
569
        $total  = $this->builder()->countAllResults(false);
1✔
570
        $offset = 0;
1✔
571

572
        while ($offset <= $total) {
1✔
573
            $builder = clone $this->builder();
1✔
574
            $rows    = $builder->get($size, $offset);
1✔
575

576
            if (! $rows) {
1✔
577
                throw DataException::forEmptyDataset('chunk');
×
578
            }
579

580
            $rows = $rows->getResult($this->tempReturnType);
1✔
581

582
            $offset += $size;
1✔
583

584
            if (empty($rows)) {
1✔
585
                continue;
1✔
586
            }
587

588
            foreach ($rows as $row) {
1✔
589
                if ($userFunc($row) === false) {
1✔
590
                    return;
×
591
                }
592
            }
593
        }
594
    }
595

596
    /**
597
     * Override countAllResults to account for soft deleted accounts.
598
     *
599
     * @return int|string
600
     */
601
    public function countAllResults(bool $reset = true, bool $test = false)
602
    {
603
        if ($this->tempUseSoftDeletes) {
17✔
604
            $this->builder()->where($this->table . '.' . $this->deletedField, null);
6✔
605
        }
606

607
        // When $reset === false, the $tempUseSoftDeletes will be
608
        // dependent on $useSoftDeletes value because we don't
609
        // want to add the same "where" condition for the second time
610
        $this->tempUseSoftDeletes = $reset
17✔
611
            ? $this->useSoftDeletes
10✔
612
            : ($this->useSoftDeletes ? false : $this->useSoftDeletes);
12✔
613

614
        return $this->builder()->testMode($test)->countAllResults($reset);
17✔
615
    }
616

617
    /**
618
     * Provides a shared instance of the Query Builder.
619
     *
620
     * @param non-empty-string|null $table
621
     *
622
     * @return BaseBuilder
623
     *
624
     * @throws ModelException
625
     */
626
    public function builder(?string $table = null)
627
    {
628
        // Check for an existing Builder
629
        if ($this->builder instanceof BaseBuilder) {
164✔
630
            // Make sure the requested table matches the builder
631
            if ($table && $this->builder->getTable() !== $table) {
84✔
632
                return $this->db->table($table);
1✔
633
            }
634

635
            return $this->builder;
84✔
636
        }
637

638
        // We're going to force a primary key to exist
639
        // so we don't have overly convoluted code,
640
        // and future features are likely to require them.
641
        if (empty($this->primaryKey)) {
164✔
642
            throw ModelException::forNoPrimaryKey(static::class);
1✔
643
        }
644

645
        $table = ($table === null || $table === '') ? $this->table : $table;
163✔
646

647
        // Ensure we have a good db connection
648
        if (! $this->db instanceof BaseConnection) {
163✔
UNCOV
649
            $this->db = Database::connect($this->DBGroup);
×
650
        }
651

652
        $builder = $this->db->table($table);
163✔
653

654
        // Only consider it "shared" if the table is correct
655
        if ($table === $this->table) {
163✔
656
            $this->builder = $builder;
163✔
657
        }
658

659
        return $builder;
163✔
660
    }
661

662
    /**
663
     * Captures the builder's set() method so that we can validate the
664
     * data here. This allows it to be used with any of the other
665
     * builder methods and still get validated data, like replace.
666
     *
667
     * @param array|object|string               $key    Field name, or an array of field/value pairs
668
     * @param bool|float|int|object|string|null $value  Field value, if $key is a single field
669
     * @param bool|null                         $escape Whether to escape values
670
     *
671
     * @return $this
672
     */
673
    public function set($key, $value = '', ?bool $escape = null)
674
    {
675
        $data = is_array($key) ? $key : [$key => $value];
8✔
676

677
        foreach (array_keys($data) as $k) {
8✔
678
            $this->tempData['escape'][$k] = $escape;
8✔
679
        }
680

681
        $this->tempData['data'] = array_merge($this->tempData['data'] ?? [], $data);
8✔
682

683
        return $this;
8✔
684
    }
685

686
    /**
687
     * This method is called on save to determine if entry have to be updated
688
     * If this method return false insert operation will be executed
689
     *
690
     * @param array|object $row Data
691
     */
692
    protected function shouldUpdate($row): bool
693
    {
694
        if (parent::shouldUpdate($row) === false) {
21✔
695
            return false;
10✔
696
        }
697

698
        if ($this->useAutoIncrement === true) {
12✔
699
            return true;
9✔
700
        }
701

702
        // When useAutoIncrement feature is disabled, check
703
        // in the database if given record already exists
704
        return $this->where($this->primaryKey, $this->getIdValue($row))->countAllResults() === 1;
3✔
705
    }
706

707
    /**
708
     * Inserts data into the database. If an object is provided,
709
     * it will attempt to convert it to an array.
710
     *
711
     * @param array|object|null $row
712
     * @phpstan-param row_array|object|null $row
713
     * @param bool $returnID Whether insert ID should be returned or not.
714
     *
715
     * @return bool|int|string
716
     * @phpstan-return ($returnID is true ? int|string|false : bool)
717
     *
718
     * @throws ReflectionException
719
     */
720
    public function insert($row = null, bool $returnID = true)
721
    {
722
        if (! empty($this->tempData['data'])) {
79✔
723
            if (empty($row)) {
2✔
724
                $row = $this->tempData['data'];
1✔
725
            } else {
726
                $row = $this->transformDataToArray($row, 'insert');
1✔
727
                $row = array_merge($this->tempData['data'], $row);
1✔
728
            }
729
        }
730

731
        $this->escape   = $this->tempData['escape'] ?? [];
79✔
732
        $this->tempData = [];
79✔
733

734
        return parent::insert($row, $returnID);
79✔
735
    }
736

737
    /**
738
     * Ensures that only the fields that are allowed to be inserted are in
739
     * the data array.
740
     *
741
     * @used-by insert() to protect against mass assignment vulnerabilities.
742
     * @used-by insertBatch() to protect against mass assignment vulnerabilities.
743
     *
744
     * @param array $row Row data
745
     * @phpstan-param row_array $row
746
     *
747
     * @throws DataException
748
     */
749
    protected function doProtectFieldsForInsert(array $row): array
750
    {
751
        if (! $this->protectFields) {
70✔
752
            return $row;
9✔
753
        }
754

755
        if ($this->allowedFields === []) {
61✔
756
            throw DataException::forInvalidAllowedFields(static::class);
1✔
757
        }
758

759
        foreach (array_keys($row) as $key) {
60✔
760
            // Do not remove the non-auto-incrementing primary key data.
761
            if ($this->useAutoIncrement === false && $key === $this->primaryKey) {
59✔
762
                continue;
5✔
763
            }
764

765
            if (! in_array($key, $this->allowedFields, true)) {
59✔
766
                unset($row[$key]);
22✔
767
            }
768
        }
769

770
        return $row;
60✔
771
    }
772

773
    /**
774
     * Updates a single record in the database. If an object is provided,
775
     * it will attempt to convert it into an array.
776
     *
777
     * @param array|int|string|null $id
778
     * @param array|object|null     $row
779
     * @phpstan-param row_array|object|null $row
780
     *
781
     * @throws ReflectionException
782
     */
783
    public function update($id = null, $row = null): bool
784
    {
785
        if (! empty($this->tempData['data'])) {
34✔
786
            if (empty($row)) {
4✔
787
                $row = $this->tempData['data'];
3✔
788
            } else {
789
                $row = $this->transformDataToArray($row, 'update');
1✔
790
                $row = array_merge($this->tempData['data'], $row);
1✔
791
            }
792
        }
793

794
        $this->escape   = $this->tempData['escape'] ?? [];
34✔
795
        $this->tempData = [];
34✔
796

797
        return parent::update($id, $row);
34✔
798
    }
799

800
    /**
801
     * Takes a class and returns an array of its public and protected
802
     * properties as an array with raw values.
803
     *
804
     * @param object $object    Object
805
     * @param bool   $recursive If true, inner entities will be cast as array as well
806
     *
807
     * @return array<string, mixed> Array with raw values.
808
     *
809
     * @throws ReflectionException
810
     */
811
    protected function objectToRawArray($object, bool $onlyChanged = true, bool $recursive = false): array
812
    {
813
        return parent::objectToRawArray($object, $onlyChanged);
20✔
814
    }
815

816
    /**
817
     * Provides/instantiates the builder/db connection and model's table/primary key names and return type.
818
     *
819
     * @param string $name Name
820
     *
821
     * @return array|BaseBuilder|bool|float|int|object|string|null
822
     */
823
    public function __get(string $name)
824
    {
825
        if (parent::__isset($name)) {
45✔
826
            return parent::__get($name);
45✔
827
        }
828

829
        return $this->builder()->{$name} ?? null;
1✔
830
    }
831

832
    /**
833
     * Checks for the existence of properties across this model, builder, and db connection.
834
     *
835
     * @param string $name Name
836
     */
837
    public function __isset(string $name): bool
838
    {
839
        if (parent::__isset($name)) {
44✔
840
            return true;
44✔
841
        }
842

843
        return isset($this->builder()->{$name});
1✔
844
    }
845

846
    /**
847
     * Provides direct access to method in the builder (if available)
848
     * and the database connection.
849
     *
850
     * @return $this|array|BaseBuilder|bool|float|int|object|string|null
851
     */
852
    public function __call(string $name, array $params)
853
    {
854
        $builder = $this->builder();
43✔
855
        $result  = null;
43✔
856

857
        if (method_exists($this->db, $name)) {
43✔
858
            $result = $this->db->{$name}(...$params);
2✔
859
        } elseif (method_exists($builder, $name)) {
42✔
860
            $this->checkBuilderMethod($name);
41✔
861

862
            $result = $builder->{$name}(...$params);
39✔
863
        } else {
864
            throw new BadMethodCallException('Call to undefined method ' . static::class . '::' . $name);
1✔
865
        }
866

867
        if ($result instanceof BaseBuilder) {
40✔
868
            return $this;
38✔
869
        }
870

871
        return $result;
3✔
872
    }
873

874
    /**
875
     * Checks the Builder method name that should not be used in the Model.
876
     */
877
    private function checkBuilderMethod(string $name): void
878
    {
879
        if (in_array($name, $this->builderMethodsNotAvailable, true)) {
41✔
880
            throw ModelException::forMethodNotAvailable(static::class, $name . '()');
2✔
881
        }
882
    }
883
}
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

© 2025 Coveralls, Inc