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

codeigniter4 / CodeIgniter4 / 7127058338

07 Dec 2023 10:41AM UTC coverage: 85.22%. Remained the same
7127058338

Pull #8304

github

web-flow
Merge f0f2bc41a into 75d1b3b77
Pull Request #8304: docs: fix `@return` in BaseBuilder.php

18584 of 21807 relevant lines covered (85.22%)

199.66 hits per line

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

96.63
/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);
278✔
154

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

157
        parent::__construct($validation);
278✔
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 && empty($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
     * @return BaseBuilder
631
     *
632
     * @throws ModelException
633
     */
634
    public function builder(?string $table = null)
635
    {
636
        // Check for an existing Builder
637
        if ($this->builder instanceof BaseBuilder) {
162✔
638
            // Make sure the requested table matches the builder
639
            if ($table && $this->builder->getTable() !== $table) {
84✔
640
                return $this->db->table($table);
1✔
641
            }
642

643
            return $this->builder;
84✔
644
        }
645

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

653
        $table = empty($table) ? $this->table : $table;
161✔
654

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

660
        $builder = $this->db->table($table);
161✔
661

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

667
        return $builder;
161✔
668
    }
669

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

685
        foreach (array_keys($data) as $k) {
6✔
686
            $this->tempData['escape'][$k] = $escape;
6✔
687
        }
688

689
        $this->tempData['data'] = array_merge($this->tempData['data'] ?? [], $data);
6✔
690

691
        return $this;
6✔
692
    }
693

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

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

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

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

739
        $this->escape   = $this->tempData['escape'] ?? [];
79✔
740
        $this->tempData = [];
79✔
741

742
        return parent::insert($row, $returnID);
79✔
743
    }
744

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

763
        if (empty($this->allowedFields)) {
61✔
764
            throw DataException::forInvalidAllowedFields(static::class);
1✔
765
        }
766

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

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

778
        return $row;
60✔
779
    }
780

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

802
        $this->escape   = $this->tempData['escape'] ?? [];
34✔
803
        $this->tempData = [];
34✔
804

805
        return parent::update($id, $row);
34✔
806
    }
807

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

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

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

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

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

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

865
        if (method_exists($this->db, $name)) {
41✔
866
            $result = $this->db->{$name}(...$params);
2✔
867
        } elseif (method_exists($builder, $name)) {
40✔
868
            $this->checkBuilderMethod($name);
39✔
869

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

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

879
        return $result;
3✔
880
    }
881

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

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

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

918
            $properties = [];
919

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

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

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

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

946
                        default:
947
                            $converted = (string) $value;
948
                    }
949

950
                    $properties[$key] = $converted;
951
                }
952
            }
953
        }
954

955
        return $properties;
956
    }
957
}
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