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

codeigniter4 / CodeIgniter4 / 7293561159

21 Dec 2023 09:55PM UTC coverage: 85.237% (+0.004%) from 85.233%
7293561159

push

github

web-flow
Merge pull request #8355 from paulbalandan/replace

Add `replace` to composer.json

18597 of 21818 relevant lines covered (85.24%)

199.84 hits per line

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

97.12
/system/Model.php
1
<?php
2

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

12
namespace CodeIgniter;
13

14
use BadMethodCallException;
15
use Closure;
16
use CodeIgniter\Database\BaseBuilder;
17
use CodeIgniter\Database\BaseConnection;
18
use CodeIgniter\Database\BaseResult;
19
use CodeIgniter\Database\ConnectionInterface;
20
use CodeIgniter\Database\Exceptions\DatabaseException;
21
use CodeIgniter\Database\Exceptions\DataException;
22
use CodeIgniter\Database\Query;
23
use CodeIgniter\Entity\Entity;
24
use CodeIgniter\Exceptions\ModelException;
25
use CodeIgniter\I18n\Time;
26
use CodeIgniter\Validation\ValidationInterface;
27
use Config\Database;
28
use ReflectionClass;
29
use ReflectionException;
30
use ReflectionProperty;
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);
280✔
154

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

157
        parent::__construct($validation);
280✔
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 $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 = 0, int $offset = 0)
234
    {
235
        $builder = $this->builder();
17✔
236

237
        if ($this->tempUseSoftDeletes) {
17✔
238
            $builder->where($this->table . '.' . $this->deletedField, null);
5✔
239
        }
240

241
        return $builder->limit($limit, $offset)
17✔
242
            ->get()
17✔
243
            ->getResult($this->tempReturnType);
17✔
244
    }
245

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

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

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

270
        return $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType);
19✔
271
    }
272

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

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

293
        $builder = $this->builder();
55✔
294

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

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

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

324
            $result = $this->db->query($sql);
1✔
325
        } else {
326
            $result = $builder->insert();
54✔
327
        }
328

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

334
        return $result;
55✔
335
    }
336

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

360
        return $this->builder()->testMode($testing)->insertBatch($set, $escape, $batchSize);
10✔
361
    }
362

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

376
        $builder = $this->builder();
25✔
377

378
        if ($id) {
25✔
379
            $builder = $builder->whereIn($this->table . '.' . $this->primaryKey, $id);
21✔
380
        }
381

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

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

393
        return $builder->update();
24✔
394
    }
395

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

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

431
        if ($id) {
40✔
432
            $builder = $builder->whereIn($this->primaryKey, $id);
21✔
433
        }
434

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

442
            $builder->where($this->deletedField);
19✔
443

444
            $set[$this->deletedField] = $this->setDate();
19✔
445

446
            if ($this->useTimestamps && $this->updatedField !== '') {
18✔
447
                $set[$this->updatedField] = $this->setDate();
1✔
448
            }
449

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

453
        return $builder->delete();
12✔
454
    }
455

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

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

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

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

510
        if ((int) $error['code'] === 0) {
2✔
511
            return [];
2✔
512
        }
513

514
        return [get_class($this->db) => $error['message']];
×
515
    }
516

517
    /**
518
     * Returns the id value for the data array or object
519
     *
520
     * @param array|object $data Data
521
     *
522
     * @return array|int|string|null
523
     *
524
     * @deprecated Use getIdValue() instead. Will be removed in version 5.0.
525
     */
526
    protected function idValue($data)
527
    {
528
        return $this->getIdValue($data);
×
529
    }
530

531
    /**
532
     * Returns the id value for the data array or object
533
     *
534
     * @param array|object $row Row data
535
     * @phpstan-param row_array|object $row
536
     *
537
     * @return array|int|string|null
538
     */
539
    public function getIdValue($row)
540
    {
541
        if (is_object($row) && isset($row->{$this->primaryKey})) {
21✔
542
            // Get the raw primary key value of the Entity.
543
            if ($row instanceof Entity) {
10✔
544
                $cast = $row->cast();
7✔
545

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

549
                $primaryKey = $row->{$this->primaryKey};
7✔
550

551
                // Restore Entity casting setting.
552
                $row->cast($cast);
7✔
553

554
                return $primaryKey;
7✔
555
            }
556

557
            return $row->{$this->primaryKey};
3✔
558
        }
559

560
        if (is_array($row) && ! empty($row[$this->primaryKey])) {
12✔
561
            return $row[$this->primaryKey];
2✔
562
        }
563

564
        return null;
10✔
565
    }
566

567
    /**
568
     * Loops over records in batches, allowing you to operate on them.
569
     * Works with $this->builder to get the Compiled select to
570
     * determine the rows to operate on.
571
     * This method works only with dbCalls.
572
     *
573
     * @return void
574
     *
575
     * @throws DataException
576
     */
577
    public function chunk(int $size, Closure $userFunc)
578
    {
579
        $total  = $this->builder()->countAllResults(false);
1✔
580
        $offset = 0;
1✔
581

582
        while ($offset <= $total) {
1✔
583
            $builder = clone $this->builder();
1✔
584
            $rows    = $builder->get($size, $offset);
1✔
585

586
            if (! $rows) {
1✔
587
                throw DataException::forEmptyDataset('chunk');
×
588
            }
589

590
            $rows = $rows->getResult($this->tempReturnType);
1✔
591

592
            $offset += $size;
1✔
593

594
            if (empty($rows)) {
1✔
595
                continue;
1✔
596
            }
597

598
            foreach ($rows as $row) {
1✔
599
                if ($userFunc($row) === false) {
1✔
600
                    return;
×
601
                }
602
            }
603
        }
604
    }
605

606
    /**
607
     * Override countAllResults to account for soft deleted accounts.
608
     *
609
     * @return int|string
610
     */
611
    public function countAllResults(bool $reset = true, bool $test = false)
612
    {
613
        if ($this->tempUseSoftDeletes) {
17✔
614
            $this->builder()->where($this->table . '.' . $this->deletedField, null);
6✔
615
        }
616

617
        // When $reset === false, the $tempUseSoftDeletes will be
618
        // dependent on $useSoftDeletes value because we don't
619
        // want to add the same "where" condition for the second time
620
        $this->tempUseSoftDeletes = $reset
17✔
621
            ? $this->useSoftDeletes
10✔
622
            : ($this->useSoftDeletes ? false : $this->useSoftDeletes);
12✔
623

624
        return $this->builder()->testMode($test)->countAllResults($reset);
17✔
625
    }
626

627
    /**
628
     * Provides a shared instance of the Query Builder.
629
     *
630
     * @param non-empty-string|null $table
631
     *
632
     * @return BaseBuilder
633
     *
634
     * @throws ModelException
635
     */
636
    public function builder(?string $table = null)
637
    {
638
        // Check for an existing Builder
639
        if ($this->builder instanceof BaseBuilder) {
164✔
640
            // Make sure the requested table matches the builder
641
            if ($table && $this->builder->getTable() !== $table) {
84✔
642
                return $this->db->table($table);
1✔
643
            }
644

645
            return $this->builder;
84✔
646
        }
647

648
        // We're going to force a primary key to exist
649
        // so we don't have overly convoluted code,
650
        // and future features are likely to require them.
651
        if (empty($this->primaryKey)) {
164✔
652
            throw ModelException::forNoPrimaryKey(static::class);
1✔
653
        }
654

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

657
        // Ensure we have a good db connection
658
        if (! $this->db instanceof BaseConnection) {
163✔
659
            $this->db = Database::connect($this->DBGroup);
×
660
        }
661

662
        $builder = $this->db->table($table);
163✔
663

664
        // Only consider it "shared" if the table is correct
665
        if ($table === $this->table) {
163✔
666
            $this->builder = $builder;
163✔
667
        }
668

669
        return $builder;
163✔
670
    }
671

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

687
        foreach (array_keys($data) as $k) {
8✔
688
            $this->tempData['escape'][$k] = $escape;
8✔
689
        }
690

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

693
        return $this;
8✔
694
    }
695

696
    /**
697
     * This method is called on save to determine if entry have to be updated
698
     * If this method return false insert operation will be executed
699
     *
700
     * @param array|object $row Data
701
     */
702
    protected function shouldUpdate($row): bool
703
    {
704
        if (parent::shouldUpdate($row) === false) {
21✔
705
            return false;
10✔
706
        }
707

708
        if ($this->useAutoIncrement === true) {
12✔
709
            return true;
9✔
710
        }
711

712
        // When useAutoIncrement feature is disabled, check
713
        // in the database if given record already exists
714
        return $this->where($this->primaryKey, $this->getIdValue($row))->countAllResults() === 1;
3✔
715
    }
716

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

741
        $this->escape   = $this->tempData['escape'] ?? [];
79✔
742
        $this->tempData = [];
79✔
743

744
        return parent::insert($row, $returnID);
79✔
745
    }
746

747
    /**
748
     * Ensures that only the fields that are allowed to be inserted are in
749
     * the data array.
750
     *
751
     * @used-by insert() to protect against mass assignment vulnerabilities.
752
     * @used-by insertBatch() to protect against mass assignment vulnerabilities.
753
     *
754
     * @param array $row Row data
755
     * @phpstan-param row_array $row
756
     *
757
     * @throws DataException
758
     */
759
    protected function doProtectFieldsForInsert(array $row): array
760
    {
761
        if (! $this->protectFields) {
70✔
762
            return $row;
9✔
763
        }
764

765
        if ($this->allowedFields === []) {
61✔
766
            throw DataException::forInvalidAllowedFields(static::class);
1✔
767
        }
768

769
        foreach (array_keys($row) as $key) {
60✔
770
            // Do not remove the non-auto-incrementing primary key data.
771
            if ($this->useAutoIncrement === false && $key === $this->primaryKey) {
59✔
772
                continue;
5✔
773
            }
774

775
            if (! in_array($key, $this->allowedFields, true)) {
59✔
776
                unset($row[$key]);
22✔
777
            }
778
        }
779

780
        return $row;
60✔
781
    }
782

783
    /**
784
     * Updates a single record in the database. If an object is provided,
785
     * it will attempt to convert it into an array.
786
     *
787
     * @param array|int|string|null $id
788
     * @param array|object|null     $row
789
     * @phpstan-param row_array|object|null $row
790
     *
791
     * @throws ReflectionException
792
     */
793
    public function update($id = null, $row = null): bool
794
    {
795
        if (! empty($this->tempData['data'])) {
34✔
796
            if (empty($row)) {
4✔
797
                $row = $this->tempData['data'];
3✔
798
            } else {
799
                $row = $this->transformDataToArray($row, 'update');
1✔
800
                $row = array_merge($this->tempData['data'], $row);
1✔
801
            }
802
        }
803

804
        $this->escape   = $this->tempData['escape'] ?? [];
34✔
805
        $this->tempData = [];
34✔
806

807
        return parent::update($id, $row);
34✔
808
    }
809

810
    /**
811
     * Takes a class and returns an array of its public and protected
812
     * properties as an array with raw values.
813
     *
814
     * @param object $object    Object
815
     * @param bool   $recursive If true, inner entities will be cast as array as well
816
     *
817
     * @return array<string, mixed>
818
     *
819
     * @throws ReflectionException
820
     */
821
    protected function objectToRawArray($object, bool $onlyChanged = true, bool $recursive = false): array
822
    {
823
        return parent::objectToRawArray($object, $onlyChanged);
20✔
824
    }
825

826
    /**
827
     * Provides/instantiates the builder/db connection and model's table/primary key names and return type.
828
     *
829
     * @param string $name Name
830
     *
831
     * @return array|BaseBuilder|bool|float|int|object|string|null
832
     */
833
    public function __get(string $name)
834
    {
835
        if (parent::__isset($name)) {
45✔
836
            return parent::__get($name);
45✔
837
        }
838

839
        return $this->builder()->{$name} ?? null;
1✔
840
    }
841

842
    /**
843
     * Checks for the existence of properties across this model, builder, and db connection.
844
     *
845
     * @param string $name Name
846
     */
847
    public function __isset(string $name): bool
848
    {
849
        if (parent::__isset($name)) {
44✔
850
            return true;
44✔
851
        }
852

853
        return isset($this->builder()->{$name});
1✔
854
    }
855

856
    /**
857
     * Provides direct access to method in the builder (if available)
858
     * and the database connection.
859
     *
860
     * @return $this|array|BaseBuilder|bool|float|int|object|string|null
861
     */
862
    public function __call(string $name, array $params)
863
    {
864
        $builder = $this->builder();
43✔
865
        $result  = null;
43✔
866

867
        if (method_exists($this->db, $name)) {
43✔
868
            $result = $this->db->{$name}(...$params);
2✔
869
        } elseif (method_exists($builder, $name)) {
42✔
870
            $this->checkBuilderMethod($name);
41✔
871

872
            $result = $builder->{$name}(...$params);
39✔
873
        } else {
874
            throw new BadMethodCallException('Call to undefined method ' . static::class . '::' . $name);
1✔
875
        }
876

877
        if ($result instanceof BaseBuilder) {
40✔
878
            return $this;
38✔
879
        }
880

881
        return $result;
3✔
882
    }
883

884
    /**
885
     * Checks the Builder method name that should not be used in the Model.
886
     */
887
    private function checkBuilderMethod(string $name): void
888
    {
889
        if (in_array($name, $this->builderMethodsNotAvailable, true)) {
41✔
890
            throw ModelException::forMethodNotAvailable(static::class, $name . '()');
2✔
891
        }
892
    }
893

894
    /**
895
     * Takes a class an returns an array of it's public and protected
896
     * properties as an array suitable for use in creates and updates.
897
     *
898
     * @param object|string $data
899
     * @param string|null   $primaryKey
900
     *
901
     * @throws ReflectionException
902
     *
903
     * @codeCoverageIgnore
904
     *
905
     * @deprecated 4.1.0
906
     */
907
    public static function classToArray($data, $primaryKey = null, string $dateFormat = 'datetime', bool $onlyChanged = true): array
908
    {
909
        if (method_exists($data, 'toRawArray')) {
910
            $properties = $data->toRawArray($onlyChanged);
911

912
            // Always grab the primary key otherwise updates will fail.
913
            if (! empty($properties) && ! empty($primaryKey) && ! in_array($primaryKey, $properties, true) && ! empty($data->{$primaryKey})) {
914
                $properties[$primaryKey] = $data->{$primaryKey};
915
            }
916
        } else {
917
            $mirror = new ReflectionClass($data);
918
            $props  = $mirror->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED);
919

920
            $properties = [];
921

922
            // Loop over each property,
923
            // saving the name/value in a new array we can return.
924
            foreach ($props as $prop) {
925
                // Must make protected values accessible.
926
                $prop->setAccessible(true);
927
                $properties[$prop->getName()] = $prop->getValue($data);
928
            }
929
        }
930

931
        // Convert any Time instances to appropriate $dateFormat
932
        if ($properties) {
933
            foreach ($properties as $key => $value) {
934
                if ($value instanceof Time) {
935
                    switch ($dateFormat) {
936
                        case 'datetime':
937
                            $converted = $value->format('Y-m-d H:i:s');
938
                            break;
939

940
                        case 'date':
941
                            $converted = $value->format('Y-m-d');
942
                            break;
943

944
                        case 'int':
945
                            $converted = $value->getTimestamp();
946
                            break;
947

948
                        default:
949
                            $converted = (string) $value;
950
                    }
951

952
                    $properties[$key] = $converted;
953
                }
954
            }
955
        }
956

957
        return $properties;
958
    }
959
}
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