• 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

95.72
/system/BaseModel.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 Closure;
15
use CodeIgniter\Database\BaseConnection;
16
use CodeIgniter\Database\BaseResult;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Database\Exceptions\DataException;
19
use CodeIgniter\Database\Query;
20
use CodeIgniter\Exceptions\ModelException;
21
use CodeIgniter\I18n\Time;
22
use CodeIgniter\Pager\Pager;
23
use CodeIgniter\Validation\ValidationInterface;
24
use Config\Services;
25
use InvalidArgumentException;
26
use ReflectionClass;
27
use ReflectionException;
28
use ReflectionProperty;
29
use stdClass;
30

31
/**
32
 * The BaseModel class provides a number of convenient features that
33
 * makes working with a databases less painful. Extending this class
34
 * provide means of implementing various database systems
35
 *
36
 * It will:
37
 *      - simplifies pagination
38
 *      - allow specifying the return type (array, object, etc) with each call
39
 *      - automatically set and update timestamps
40
 *      - handle soft deletes
41
 *      - ensure validation is run against objects when saving items
42
 *      - process various callbacks
43
 *      - allow intermingling calls to the db connection
44
 *
45
 * @phpstan-type row_array array<int|string, float|int|null|string>
46
 * @phpstan-type event_data_beforeinsert array{data: row_array}
47
 * @phpstan-type event_data_afterinsert array{id: int|string, data: row_array, result: bool}
48
 * @phpstan-type event_data_beforefind array{id?: int|string, method: string, singleton: bool, limit?: int, offset?: int}
49
 * @phpstan-type event_data_afterfind array{id: int|string|null|list<int|string>, data: row_array|list<row_array>|object|null, method: string, singleton: bool}
50
 * @phpstan-type event_data_beforeupdate array{id: null|list<int|string>, data: row_array}
51
 * @phpstan-type event_data_afterupdate array{id: null|list<int|string>, data: row_array|object, result: bool}
52
 * @phpstan-type event_data_beforedelete array{id: null|list<int|string>, purge: bool}
53
 * @phpstan-type event_data_afterdelete array{id: null|list<int|string>, data: null, purge: bool, result: bool}
54
 */
55
abstract class BaseModel
56
{
57
    /**
58
     * Pager instance.
59
     * Populated after calling $this->paginate()
60
     *
61
     * @var Pager
62
     */
63
    public $pager;
64

65
    /**
66
     * Last insert ID
67
     *
68
     * @var int|string
69
     */
70
    protected $insertID = 0;
71

72
    /**
73
     * The Database connection group that
74
     * should be instantiated.
75
     *
76
     * @var non-empty-string|null
77
     */
78
    protected $DBGroup;
79

80
    /**
81
     * The format that the results should be returned as.
82
     * Will be overridden if the as* methods are used.
83
     *
84
     * @var string
85
     */
86
    protected $returnType = 'array';
87

88
    /**
89
     * If this model should use "softDeletes" and
90
     * simply set a date when rows are deleted, or
91
     * do hard deletes.
92
     *
93
     * @var bool
94
     */
95
    protected $useSoftDeletes = false;
96

97
    /**
98
     * An array of field names that are allowed
99
     * to be set by the user in inserts/updates.
100
     *
101
     * @var array
102
     */
103
    protected $allowedFields = [];
104

105
    /**
106
     * If true, will set created_at, and updated_at
107
     * values during insert and update routines.
108
     *
109
     * @var bool
110
     */
111
    protected $useTimestamps = false;
112

113
    /**
114
     * The type of column that created_at and updated_at
115
     * are expected to.
116
     *
117
     * Allowed: 'datetime', 'date', 'int'
118
     *
119
     * @var string
120
     */
121
    protected $dateFormat = 'datetime';
122

123
    /**
124
     * The column used for insert timestamps
125
     *
126
     * @var string
127
     */
128
    protected $createdField = 'created_at';
129

130
    /**
131
     * The column used for update timestamps
132
     *
133
     * @var string
134
     */
135
    protected $updatedField = 'updated_at';
136

137
    /**
138
     * Used by withDeleted to override the
139
     * model's softDelete setting.
140
     *
141
     * @var bool
142
     */
143
    protected $tempUseSoftDeletes;
144

145
    /**
146
     * The column used to save soft delete state
147
     *
148
     * @var string
149
     */
150
    protected $deletedField = 'deleted_at';
151

152
    /**
153
     * Used by asArray and asObject to provide
154
     * temporary overrides of model default.
155
     *
156
     * @var string
157
     */
158
    protected $tempReturnType;
159

160
    /**
161
     * Whether we should limit fields in inserts
162
     * and updates to those available in $allowedFields or not.
163
     *
164
     * @var bool
165
     */
166
    protected $protectFields = true;
167

168
    /**
169
     * Database Connection
170
     *
171
     * @var BaseConnection
172
     */
173
    protected $db;
174

175
    /**
176
     * Rules used to validate data in insert, update, and save methods.
177
     * The array must match the format of data passed to the Validation
178
     * library.
179
     *
180
     * @var array|string
181
     */
182
    protected $validationRules = [];
183

184
    /**
185
     * Contains any custom error messages to be
186
     * used during data validation.
187
     *
188
     * @var array
189
     */
190
    protected $validationMessages = [];
191

192
    /**
193
     * Skip the model's validation. Used in conjunction with skipValidation()
194
     * to skip data validation for any future calls.
195
     *
196
     * @var bool
197
     */
198
    protected $skipValidation = false;
199

200
    /**
201
     * Whether rules should be removed that do not exist
202
     * in the passed data. Used in updates.
203
     *
204
     * @var bool
205
     */
206
    protected $cleanValidationRules = true;
207

208
    /**
209
     * Our validator instance.
210
     *
211
     * @var ValidationInterface
212
     */
213
    protected $validation;
214

215
    /*
216
     * Callbacks.
217
     *
218
     * Each array should contain the method names (within the model)
219
     * that should be called when those events are triggered.
220
     *
221
     * "Update" and "delete" methods are passed the same items that
222
     * are given to their respective method.
223
     *
224
     * "Find" methods receive the ID searched for (if present), and
225
     * 'afterFind' additionally receives the results that were found.
226
     */
227

228
    /**
229
     * Whether to trigger the defined callbacks
230
     *
231
     * @var bool
232
     */
233
    protected $allowCallbacks = true;
234

235
    /**
236
     * Used by allowCallbacks() to override the
237
     * model's allowCallbacks setting.
238
     *
239
     * @var bool
240
     */
241
    protected $tempAllowCallbacks;
242

243
    /**
244
     * Callbacks for beforeInsert
245
     *
246
     * @var array
247
     */
248
    protected $beforeInsert = [];
249

250
    /**
251
     * Callbacks for afterInsert
252
     *
253
     * @var array
254
     */
255
    protected $afterInsert = [];
256

257
    /**
258
     * Callbacks for beforeUpdate
259
     *
260
     * @var array
261
     */
262
    protected $beforeUpdate = [];
263

264
    /**
265
     * Callbacks for afterUpdate
266
     *
267
     * @var array
268
     */
269
    protected $afterUpdate = [];
270

271
    /**
272
     * Callbacks for beforeInsertBatch
273
     *
274
     * @var array
275
     */
276
    protected $beforeInsertBatch = [];
277

278
    /**
279
     * Callbacks for afterInsertBatch
280
     *
281
     * @var array
282
     */
283
    protected $afterInsertBatch = [];
284

285
    /**
286
     * Callbacks for beforeUpdateBatch
287
     *
288
     * @var array
289
     */
290
    protected $beforeUpdateBatch = [];
291

292
    /**
293
     * Callbacks for afterUpdateBatch
294
     *
295
     * @var array
296
     */
297
    protected $afterUpdateBatch = [];
298

299
    /**
300
     * Callbacks for beforeFind
301
     *
302
     * @var array
303
     */
304
    protected $beforeFind = [];
305

306
    /**
307
     * Callbacks for afterFind
308
     *
309
     * @var array
310
     */
311
    protected $afterFind = [];
312

313
    /**
314
     * Callbacks for beforeDelete
315
     *
316
     * @var array
317
     */
318
    protected $beforeDelete = [];
319

320
    /**
321
     * Callbacks for afterDelete
322
     *
323
     * @var array
324
     */
325
    protected $afterDelete = [];
326

327
    /**
328
     * Whether to allow inserting empty data.
329
     */
330
    protected bool $allowEmptyInserts = false;
331

332
    public function __construct(?ValidationInterface $validation = null)
333
    {
334
        $this->tempReturnType     = $this->returnType;
278✔
335
        $this->tempUseSoftDeletes = $this->useSoftDeletes;
278✔
336
        $this->tempAllowCallbacks = $this->allowCallbacks;
278✔
337

338
        /**
339
         * @var ValidationInterface|null $validation
340
         */
341
        $validation ??= Services::validation(null, false);
278✔
342
        $this->validation = $validation;
278✔
343

344
        $this->initialize();
278✔
345
    }
346

347
    /**
348
     * Initializes the instance with any additional steps.
349
     * Optionally implemented by child classes.
350
     *
351
     * @return void
352
     */
353
    protected function initialize()
354
    {
355
    }
277✔
356

357
    /**
358
     * Fetches the row of database.
359
     * This method works only with dbCalls.
360
     *
361
     * @param bool                  $singleton Single or multiple results
362
     * @param array|int|string|null $id        One primary key or an array of primary keys
363
     *
364
     * @return array|object|null The resulting row of data, or null.
365
     */
366
    abstract protected function doFind(bool $singleton, $id = null);
367

368
    /**
369
     * Fetches the column of database.
370
     * This method works only with dbCalls.
371
     *
372
     * @param string $columnName Column Name
373
     *
374
     * @return array|null The resulting row of data, or null if no data found.
375
     *
376
     * @throws DataException
377
     */
378
    abstract protected function doFindColumn(string $columnName);
379

380
    /**
381
     * Fetches all results, while optionally limiting them.
382
     * This method works only with dbCalls.
383
     *
384
     * @param int $limit  Limit
385
     * @param int $offset Offset
386
     *
387
     * @return array
388
     */
389
    abstract protected function doFindAll(int $limit = 0, int $offset = 0);
390

391
    /**
392
     * Returns the first row of the result set.
393
     * This method works only with dbCalls.
394
     *
395
     * @return array|object|null
396
     */
397
    abstract protected function doFirst();
398

399
    /**
400
     * Inserts data into the current database.
401
     * This method works only with dbCalls.
402
     *
403
     * @param array $row Row data
404
     * @phpstan-param row_array $row
405
     *
406
     * @return bool
407
     */
408
    abstract protected function doInsert(array $row);
409

410
    /**
411
     * Compiles batch insert and runs the queries, validating each row prior.
412
     * This method works only with dbCalls.
413
     *
414
     * @param array|null $set       An associative array of insert values
415
     * @param bool|null  $escape    Whether to escape values
416
     * @param int        $batchSize The size of the batch to run
417
     * @param bool       $testing   True means only number of records is returned, false will execute the query
418
     *
419
     * @return bool|int Number of rows inserted or FALSE on failure
420
     */
421
    abstract protected function doInsertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100, bool $testing = false);
422

423
    /**
424
     * Updates a single record in the database.
425
     * This method works only with dbCalls.
426
     *
427
     * @param array|int|string|null $id  ID
428
     * @param array|null            $row Row data
429
     * @phpstan-param row_array|null $row
430
     */
431
    abstract protected function doUpdate($id = null, $row = null): bool;
432

433
    /**
434
     * Compiles an update and runs the query.
435
     * This method works only with dbCalls.
436
     *
437
     * @param array|null  $set       An associative array of update values
438
     * @param string|null $index     The where key
439
     * @param int         $batchSize The size of the batch to run
440
     * @param bool        $returnSQL True means SQL is returned, false will execute the query
441
     *
442
     * @return false|int|string[] Number of rows affected or FALSE on failure, SQL array when testMode
443
     *
444
     * @throws DatabaseException
445
     */
446
    abstract protected function doUpdateBatch(?array $set = null, ?string $index = null, int $batchSize = 100, bool $returnSQL = false);
447

448
    /**
449
     * Deletes a single record from the database where $id matches.
450
     * This method works only with dbCalls.
451
     *
452
     * @param array|int|string|null $id    The rows primary key(s)
453
     * @param bool                  $purge Allows overriding the soft deletes setting.
454
     *
455
     * @return bool|string
456
     *
457
     * @throws DatabaseException
458
     */
459
    abstract protected function doDelete($id = null, bool $purge = false);
460

461
    /**
462
     * Permanently deletes all rows that have been marked as deleted.
463
     * through soft deletes (deleted = 1).
464
     * This method works only with dbCalls.
465
     *
466
     * @return bool|string Returns a string if in test mode.
467
     */
468
    abstract protected function doPurgeDeleted();
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
    abstract protected function doOnlyDeleted();
478

479
    /**
480
     * Compiles a replace and runs the query.
481
     * This method works only with dbCalls.
482
     *
483
     * @param array|null $row Row data
484
     * @phpstan-param row_array|null $row
485
     * @param bool $returnSQL Set to true to return Query String
486
     *
487
     * @return BaseResult|false|Query|string
488
     */
489
    abstract protected function doReplace(?array $row = null, bool $returnSQL = false);
490

491
    /**
492
     * Grabs the last error(s) that occurred from the Database connection.
493
     * This method works only with dbCalls.
494
     *
495
     * @return array|null
496
     */
497
    abstract protected function doErrors();
498

499
    /**
500
     * Returns the id value for the data array or object.
501
     *
502
     * @param array|object $data Data
503
     *
504
     * @return array|int|string|null
505
     *
506
     * @deprecated Add an override on getIdValue() instead. Will be removed in version 5.0.
507
     */
508
    abstract protected function idValue($data);
509

510
    /**
511
     * Public getter to return the id value using the idValue() method.
512
     * For example with SQL this will return $data->$this->primaryKey.
513
     *
514
     * @param array|object $row Row data
515
     * @phpstan-param row_array|object $row
516
     *
517
     * @return array|int|string|null
518
     *
519
     * @todo: Make abstract in version 5.0
520
     */
521
    public function getIdValue($row)
522
    {
523
        return $this->idValue($row);
×
524
    }
525

526
    /**
527
     * Override countAllResults to account for soft deleted accounts.
528
     * This method works only with dbCalls.
529
     *
530
     * @param bool $reset Reset
531
     * @param bool $test  Test
532
     *
533
     * @return int|string
534
     */
535
    abstract public function countAllResults(bool $reset = true, bool $test = false);
536

537
    /**
538
     * Loops over records in batches, allowing you to operate on them.
539
     * This method works only with dbCalls.
540
     *
541
     * @param int     $size     Size
542
     * @param Closure $userFunc Callback Function
543
     *
544
     * @return void
545
     *
546
     * @throws DataException
547
     */
548
    abstract public function chunk(int $size, Closure $userFunc);
549

550
    /**
551
     * Fetches the row of database.
552
     *
553
     * @param array|int|string|null $id One primary key or an array of primary keys
554
     *
555
     * @return array|object|null The resulting row of data, or null.
556
     * @phpstan-return ($id is int|string ? row_array|object|null : list<row_array|object>)
557
     */
558
    public function find($id = null)
559
    {
560
        $singleton = is_numeric($id) || is_string($id);
37✔
561

562
        if ($this->tempAllowCallbacks) {
37✔
563
            // Call the before event and check for a return
564
            $eventData = $this->trigger('beforeFind', [
34✔
565
                'id'        => $id,
34✔
566
                'method'    => 'find',
34✔
567
                'singleton' => $singleton,
34✔
568
            ]);
34✔
569

570
            if (! empty($eventData['returnData'])) {
34✔
571
                return $eventData['data'];
3✔
572
            }
573
        }
574

575
        $eventData = [
35✔
576
            'id'        => $id,
35✔
577
            'data'      => $this->doFind($singleton, $id),
35✔
578
            'method'    => 'find',
35✔
579
            'singleton' => $singleton,
35✔
580
        ];
35✔
581

582
        if ($this->tempAllowCallbacks) {
34✔
583
            $eventData = $this->trigger('afterFind', $eventData);
31✔
584
        }
585

586
        $this->tempReturnType     = $this->returnType;
34✔
587
        $this->tempUseSoftDeletes = $this->useSoftDeletes;
34✔
588
        $this->tempAllowCallbacks = $this->allowCallbacks;
34✔
589

590
        return $eventData['data'];
34✔
591
    }
592

593
    /**
594
     * Fetches the column of database.
595
     *
596
     * @param string $columnName Column Name
597
     *
598
     * @return array|null The resulting row of data, or null if no data found.
599
     *
600
     * @throws DataException
601
     */
602
    public function findColumn(string $columnName)
603
    {
604
        if (strpos($columnName, ',') !== false) {
2✔
605
            throw DataException::forFindColumnHaveMultipleColumns();
1✔
606
        }
607

608
        $resultSet = $this->doFindColumn($columnName);
1✔
609

610
        return $resultSet ? array_column($resultSet, $columnName) : null;
1✔
611
    }
612

613
    /**
614
     * Fetches all results, while optionally limiting them.
615
     *
616
     * @param int $limit  Limit
617
     * @param int $offset Offset
618
     *
619
     * @return array
620
     */
621
    public function findAll(int $limit = 0, int $offset = 0)
622
    {
623
        if ($this->tempAllowCallbacks) {
17✔
624
            // Call the before event and check for a return
625
            $eventData = $this->trigger('beforeFind', [
17✔
626
                'method'    => 'findAll',
17✔
627
                'limit'     => $limit,
17✔
628
                'offset'    => $offset,
17✔
629
                'singleton' => false,
17✔
630
            ]);
17✔
631

632
            if (! empty($eventData['returnData'])) {
17✔
633
                return $eventData['data'];
1✔
634
            }
635
        }
636

637
        $eventData = [
17✔
638
            'data'      => $this->doFindAll($limit, $offset),
17✔
639
            'limit'     => $limit,
17✔
640
            'offset'    => $offset,
17✔
641
            'method'    => 'findAll',
17✔
642
            'singleton' => false,
17✔
643
        ];
17✔
644

645
        if ($this->tempAllowCallbacks) {
17✔
646
            $eventData = $this->trigger('afterFind', $eventData);
17✔
647
        }
648

649
        $this->tempReturnType     = $this->returnType;
17✔
650
        $this->tempUseSoftDeletes = $this->useSoftDeletes;
17✔
651
        $this->tempAllowCallbacks = $this->allowCallbacks;
17✔
652

653
        return $eventData['data'];
17✔
654
    }
655

656
    /**
657
     * Returns the first row of the result set.
658
     *
659
     * @return array|object|null
660
     */
661
    public function first()
662
    {
663
        if ($this->tempAllowCallbacks) {
19✔
664
            // Call the before event and check for a return
665
            $eventData = $this->trigger('beforeFind', [
19✔
666
                'method'    => 'first',
19✔
667
                'singleton' => true,
19✔
668
            ]);
19✔
669

670
            if (! empty($eventData['returnData'])) {
19✔
671
                return $eventData['data'];
1✔
672
            }
673
        }
674

675
        $eventData = [
19✔
676
            'data'      => $this->doFirst(),
19✔
677
            'method'    => 'first',
19✔
678
            'singleton' => true,
19✔
679
        ];
19✔
680

681
        if ($this->tempAllowCallbacks) {
19✔
682
            $eventData = $this->trigger('afterFind', $eventData);
19✔
683
        }
684

685
        $this->tempReturnType     = $this->returnType;
19✔
686
        $this->tempUseSoftDeletes = $this->useSoftDeletes;
19✔
687
        $this->tempAllowCallbacks = $this->allowCallbacks;
19✔
688

689
        return $eventData['data'];
19✔
690
    }
691

692
    /**
693
     * A convenience method that will attempt to determine whether the
694
     * data should be inserted or updated. Will work with either
695
     * an array or object. When using with custom class objects,
696
     * you must ensure that the class will provide access to the class
697
     * variables, even if through a magic method.
698
     *
699
     * @param array|object $row Row data
700
     * @phpstan-param row_array|object $row
701
     *
702
     * @throws ReflectionException
703
     */
704
    public function save($row): bool
705
    {
706
        if (empty($row)) {
22✔
707
            return true;
1✔
708
        }
709

710
        if ($this->shouldUpdate($row)) {
21✔
711
            $response = $this->update($this->getIdValue($row), $row);
12✔
712
        } else {
713
            $response = $this->insert($row, false);
12✔
714

715
            if ($response !== false) {
11✔
716
                $response = true;
10✔
717
            }
718
        }
719

720
        return $response;
20✔
721
    }
722

723
    /**
724
     * This method is called on save to determine if entry have to be updated.
725
     * If this method returns false insert operation will be executed
726
     *
727
     * @param array|object $row Row data
728
     * @phpstan-param row_array|object $row
729
     */
730
    protected function shouldUpdate($row): bool
731
    {
732
        return ! empty($this->getIdValue($row));
21✔
733
    }
734

735
    /**
736
     * Returns last insert ID or 0.
737
     *
738
     * @return int|string
739
     */
740
    public function getInsertID()
741
    {
742
        return is_numeric($this->insertID) ? (int) $this->insertID : $this->insertID;
11✔
743
    }
744

745
    /**
746
     * Inserts data into the database. If an object is provided,
747
     * it will attempt to convert it to an array.
748
     *
749
     * @param array|object|null $row Row data
750
     * @phpstan-param row_array|object|null $row
751
     * @param bool $returnID Whether insert ID should be returned or not.
752
     *
753
     * @return bool|int|string insert ID or true on success. false on failure.
754
     * @phpstan-return ($returnID is true ? int|string|false : bool)
755
     *
756
     * @throws ReflectionException
757
     */
758
    public function insert($row = null, bool $returnID = true)
759
    {
760
        $this->insertID = 0;
79✔
761

762
        // Set $cleanValidationRules to false temporary.
763
        $cleanValidationRules       = $this->cleanValidationRules;
79✔
764
        $this->cleanValidationRules = false;
79✔
765

766
        $row = $this->transformDataToArray($row, 'insert');
79✔
767

768
        // Validate data before saving.
769
        if (! $this->skipValidation && ! $this->validate($row)) {
77✔
770
            // Restore $cleanValidationRules
771
            $this->cleanValidationRules = $cleanValidationRules;
19✔
772

773
            return false;
19✔
774
        }
775

776
        // Restore $cleanValidationRules
777
        $this->cleanValidationRules = $cleanValidationRules;
60✔
778

779
        // Must be called first, so we don't
780
        // strip out created_at values.
781
        $row = $this->doProtectFieldsForInsert($row);
60✔
782

783
        // doProtectFields() can further remove elements from
784
        // $row, so we need to check for empty dataset again
785
        if (! $this->allowEmptyInserts && empty($row)) {
59✔
786
            throw DataException::forEmptyDataset('insert');
2✔
787
        }
788

789
        // Set created_at and updated_at with same time
790
        $date = $this->setDate();
57✔
791
        $row  = $this->setCreatedField($row, $date);
57✔
792
        $row  = $this->setUpdatedField($row, $date);
57✔
793

794
        $eventData = ['data' => $row];
57✔
795

796
        if ($this->tempAllowCallbacks) {
57✔
797
            $eventData = $this->trigger('beforeInsert', $eventData);
57✔
798
        }
799

800
        $result = $this->doInsert($eventData['data']);
56✔
801

802
        $eventData = [
55✔
803
            'id'     => $this->insertID,
55✔
804
            'data'   => $eventData['data'],
55✔
805
            'result' => $result,
55✔
806
        ];
55✔
807

808
        if ($this->tempAllowCallbacks) {
55✔
809
            // Trigger afterInsert events with the inserted data and new ID
810
            $this->trigger('afterInsert', $eventData);
55✔
811
        }
812

813
        $this->tempAllowCallbacks = $this->allowCallbacks;
55✔
814

815
        // If insertion failed, get out of here
816
        if (! $result) {
55✔
817
            return $result;
2✔
818
        }
819

820
        // otherwise return the insertID, if requested.
821
        return $returnID ? $this->insertID : $result;
53✔
822
    }
823

824
    /**
825
     * Set datetime to created field.
826
     *
827
     * @phpstan-param row_array $row
828
     * @param int|string $date timestamp or datetime string
829
     */
830
    protected function setCreatedField(array $row, $date): array
831
    {
832
        if ($this->useTimestamps && $this->createdField !== '' && ! array_key_exists($this->createdField, $row)) {
67✔
833
            $row[$this->createdField] = $date;
12✔
834
        }
835

836
        return $row;
67✔
837
    }
838

839
    /**
840
     * Set datetime to updated field.
841
     *
842
     * @phpstan-param row_array $row
843
     * @param int|string $date timestamp or datetime string
844
     */
845
    protected function setUpdatedField(array $row, $date): array
846
    {
847
        if ($this->useTimestamps && $this->updatedField !== '' && ! array_key_exists($this->updatedField, $row)) {
81✔
848
            $row[$this->updatedField] = $date;
15✔
849
        }
850

851
        return $row;
81✔
852
    }
853

854
    /**
855
     * Compiles batch insert runs the queries, validating each row prior.
856
     *
857
     * @param list<array|object>|null $set an associative array of insert values
858
     * @phpstan-param list<row_array|object>|null $set
859
     * @param bool|null $escape    Whether to escape values
860
     * @param int       $batchSize The size of the batch to run
861
     * @param bool      $testing   True means only number of records is returned, false will execute the query
862
     *
863
     * @return bool|int Number of rows inserted or FALSE on failure
864
     *
865
     * @throws ReflectionException
866
     */
867
    public function insertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100, bool $testing = false)
868
    {
869
        // Set $cleanValidationRules to false temporary.
870
        $cleanValidationRules       = $this->cleanValidationRules;
13✔
871
        $this->cleanValidationRules = false;
13✔
872

873
        if (is_array($set)) {
13✔
874
            foreach ($set as &$row) {
13✔
875
                // If $row is using a custom class with public or protected
876
                // properties representing the collection elements, we need to grab
877
                // them as an array.
878
                if (is_object($row) && ! $row instanceof stdClass) {
13✔
879
                    $row = $this->objectToArray($row, false, true);
1✔
880
                }
881

882
                // If it's still a stdClass, go ahead and convert to
883
                // an array so doProtectFields and other model methods
884
                // don't have to do special checks.
885
                if (is_object($row)) {
13✔
886
                    $row = (array) $row;
×
887
                }
888

889
                // Validate every row.
890
                if (! $this->skipValidation && ! $this->validate($row)) {
13✔
891
                    // Restore $cleanValidationRules
892
                    $this->cleanValidationRules = $cleanValidationRules;
3✔
893

894
                    return false;
3✔
895
                }
896

897
                // Must be called first so we don't
898
                // strip out created_at values.
899
                $row = $this->doProtectFieldsForInsert($row);
10✔
900

901
                // Set created_at and updated_at with same time
902
                $date = $this->setDate();
10✔
903
                $row  = $this->setCreatedField($row, $date);
10✔
904
                $row  = $this->setUpdatedField($row, $date);
10✔
905
            }
906
        }
907

908
        // Restore $cleanValidationRules
909
        $this->cleanValidationRules = $cleanValidationRules;
10✔
910

911
        $eventData = ['data' => $set];
10✔
912

913
        if ($this->tempAllowCallbacks) {
10✔
914
            $eventData = $this->trigger('beforeInsertBatch', $eventData);
10✔
915
        }
916

917
        $result = $this->doInsertBatch($eventData['data'], $escape, $batchSize, $testing);
10✔
918

919
        $eventData = [
10✔
920
            'data'   => $eventData['data'],
10✔
921
            'result' => $result,
10✔
922
        ];
10✔
923

924
        if ($this->tempAllowCallbacks) {
10✔
925
            // Trigger afterInsert events with the inserted data and new ID
926
            $this->trigger('afterInsertBatch', $eventData);
10✔
927
        }
928

929
        $this->tempAllowCallbacks = $this->allowCallbacks;
10✔
930

931
        return $result;
10✔
932
    }
933

934
    /**
935
     * Updates a single record in the database. If an object is provided,
936
     * it will attempt to convert it into an array.
937
     *
938
     * @param array|int|string|null $id
939
     * @param array|object|null     $row Row data
940
     * @phpstan-param row_array|object|null $row
941
     *
942
     * @throws ReflectionException
943
     */
944
    public function update($id = null, $row = null): bool
945
    {
946
        if (is_bool($id)) {
34✔
947
            throw new InvalidArgumentException('update(): argument #1 ($id) should not be boolean.');
1✔
948
        }
949

950
        if (is_numeric($id) || is_string($id)) {
33✔
951
            $id = [$id];
28✔
952
        }
953

954
        $row = $this->transformDataToArray($row, 'update');
33✔
955

956
        // Validate data before saving.
957
        if (! $this->skipValidation && ! $this->validate($row)) {
31✔
958
            return false;
4✔
959
        }
960

961
        // Must be called first, so we don't
962
        // strip out updated_at values.
963
        $row = $this->doProtectFields($row);
27✔
964

965
        // doProtectFields() can further remove elements from
966
        // $row, so we need to check for empty dataset again
967
        if (empty($row)) {
27✔
968
            throw DataException::forEmptyDataset('update');
2✔
969
        }
970

971
        $row = $this->setUpdatedField($row, $this->setDate());
25✔
972

973
        $eventData = [
25✔
974
            'id'   => $id,
25✔
975
            'data' => $row,
25✔
976
        ];
25✔
977

978
        if ($this->tempAllowCallbacks) {
25✔
979
            $eventData = $this->trigger('beforeUpdate', $eventData);
25✔
980
        }
981

982
        $eventData = [
25✔
983
            'id'     => $id,
25✔
984
            'data'   => $eventData['data'],
25✔
985
            'result' => $this->doUpdate($id, $eventData['data']),
25✔
986
        ];
25✔
987

988
        if ($this->tempAllowCallbacks) {
24✔
989
            $this->trigger('afterUpdate', $eventData);
24✔
990
        }
991

992
        $this->tempAllowCallbacks = $this->allowCallbacks;
24✔
993

994
        return $eventData['result'];
24✔
995
    }
996

997
    /**
998
     * Compiles an update and runs the query.
999
     *
1000
     * @param list<array|object>|null $set an associative array of insert values
1001
     * @phpstan-param list<row_array|object>|null $set
1002
     * @param string|null $index     The where key
1003
     * @param int         $batchSize The size of the batch to run
1004
     * @param bool        $returnSQL True means SQL is returned, false will execute the query
1005
     *
1006
     * @return false|int|string[] Number of rows affected or FALSE on failure, SQL array when testMode
1007
     *
1008
     * @throws DatabaseException
1009
     * @throws ReflectionException
1010
     */
1011
    public function updateBatch(?array $set = null, ?string $index = null, int $batchSize = 100, bool $returnSQL = false)
1012
    {
1013
        if (is_array($set)) {
5✔
1014
            foreach ($set as &$row) {
5✔
1015
                // If $row is using a custom class with public or protected
1016
                // properties representing the collection elements, we need to grab
1017
                // them as an array.
1018
                if (is_object($row) && ! $row instanceof stdClass) {
5✔
1019
                    // For updates the index field is needed even if it is not changed.
1020
                    // So set $onlyChanged to false.
1021
                    $row = $this->objectToArray($row, false, true);
1✔
1022
                }
1023

1024
                // If it's still a stdClass, go ahead and convert to
1025
                // an array so doProtectFields and other model methods
1026
                // don't have to do special checks.
1027
                if (is_object($row)) {
5✔
1028
                    $row = (array) $row;
×
1029
                }
1030

1031
                // Validate data before saving.
1032
                if (! $this->skipValidation && ! $this->validate($row)) {
5✔
1033
                    return false;
1✔
1034
                }
1035

1036
                // Save updateIndex for later
1037
                $updateIndex = $row[$index] ?? null;
4✔
1038

1039
                if ($updateIndex === null) {
4✔
1040
                    throw new InvalidArgumentException(
1✔
1041
                        'The index ("' . $index . '") for updateBatch() is missing in the data: '
1✔
1042
                        . json_encode($row)
1✔
1043
                    );
1✔
1044
                }
1045

1046
                // Must be called first so we don't
1047
                // strip out updated_at values.
1048
                $row = $this->doProtectFields($row);
3✔
1049

1050
                // Restore updateIndex value in case it was wiped out
1051
                if ($updateIndex !== null) {
3✔
1052
                    $row[$index] = $updateIndex;
3✔
1053
                }
1054

1055
                $row = $this->setUpdatedField($row, $this->setDate());
3✔
1056
            }
1057
        }
1058

1059
        $eventData = ['data' => $set];
3✔
1060

1061
        if ($this->tempAllowCallbacks) {
3✔
1062
            $eventData = $this->trigger('beforeUpdateBatch', $eventData);
3✔
1063
        }
1064

1065
        $result = $this->doUpdateBatch($eventData['data'], $index, $batchSize, $returnSQL);
3✔
1066

1067
        $eventData = [
3✔
1068
            'data'   => $eventData['data'],
3✔
1069
            'result' => $result,
3✔
1070
        ];
3✔
1071

1072
        if ($this->tempAllowCallbacks) {
3✔
1073
            // Trigger afterInsert events with the inserted data and new ID
1074
            $this->trigger('afterUpdateBatch', $eventData);
3✔
1075
        }
1076

1077
        $this->tempAllowCallbacks = $this->allowCallbacks;
3✔
1078

1079
        return $result;
3✔
1080
    }
1081

1082
    /**
1083
     * Deletes a single record from the database where $id matches.
1084
     *
1085
     * @param array|int|string|null $id    The rows primary key(s)
1086
     * @param bool                  $purge Allows overriding the soft deletes setting.
1087
     *
1088
     * @return BaseResult|bool
1089
     *
1090
     * @throws DatabaseException
1091
     */
1092
    public function delete($id = null, bool $purge = false)
1093
    {
1094
        if (is_bool($id)) {
40✔
1095
            throw new InvalidArgumentException('delete(): argument #1 ($id) should not be boolean.');
×
1096
        }
1097

1098
        if ($id && (is_numeric($id) || is_string($id))) {
40✔
1099
            $id = [$id];
20✔
1100
        }
1101

1102
        $eventData = [
40✔
1103
            'id'    => $id,
40✔
1104
            'purge' => $purge,
40✔
1105
        ];
40✔
1106

1107
        if ($this->tempAllowCallbacks) {
40✔
1108
            $this->trigger('beforeDelete', $eventData);
39✔
1109
        }
1110

1111
        $eventData = [
40✔
1112
            'id'     => $id,
40✔
1113
            'data'   => null,
40✔
1114
            'purge'  => $purge,
40✔
1115
            'result' => $this->doDelete($id, $purge),
40✔
1116
        ];
40✔
1117

1118
        if ($this->tempAllowCallbacks) {
27✔
1119
            $this->trigger('afterDelete', $eventData);
26✔
1120
        }
1121

1122
        $this->tempAllowCallbacks = $this->allowCallbacks;
27✔
1123

1124
        return $eventData['result'];
27✔
1125
    }
1126

1127
    /**
1128
     * Permanently deletes all rows that have been marked as deleted
1129
     * through soft deletes (deleted = 1).
1130
     *
1131
     * @return bool|string Returns a string if in test mode.
1132
     */
1133
    public function purgeDeleted()
1134
    {
1135
        if (! $this->useSoftDeletes) {
2✔
1136
            return true;
1✔
1137
        }
1138

1139
        return $this->doPurgeDeleted();
1✔
1140
    }
1141

1142
    /**
1143
     * Sets $useSoftDeletes value so that we can temporarily override
1144
     * the soft deletes settings. Can be used for all find* methods.
1145
     *
1146
     * @param bool $val Value
1147
     *
1148
     * @return $this
1149
     */
1150
    public function withDeleted(bool $val = true)
1151
    {
1152
        $this->tempUseSoftDeletes = ! $val;
23✔
1153

1154
        return $this;
23✔
1155
    }
1156

1157
    /**
1158
     * Works with the find* methods to return only the rows that
1159
     * have been deleted.
1160
     *
1161
     * @return $this
1162
     */
1163
    public function onlyDeleted()
1164
    {
1165
        $this->tempUseSoftDeletes = false;
1✔
1166
        $this->doOnlyDeleted();
1✔
1167

1168
        return $this;
1✔
1169
    }
1170

1171
    /**
1172
     * Compiles a replace and runs the query.
1173
     *
1174
     * @param array|null $row Row data
1175
     * @phpstan-param row_array|null $row
1176
     * @param bool $returnSQL Set to true to return Query String
1177
     *
1178
     * @return BaseResult|false|Query|string
1179
     */
1180
    public function replace(?array $row = null, bool $returnSQL = false)
1181
    {
1182
        // Validate data before saving.
1183
        if (($row !== null) && ! $this->skipValidation && ! $this->validate($row)) {
3✔
1184
            return false;
1✔
1185
        }
1186

1187
        $row = $this->setUpdatedField((array) $row, $this->setDate());
2✔
1188

1189
        return $this->doReplace($row, $returnSQL);
2✔
1190
    }
1191

1192
    /**
1193
     * Grabs the last error(s) that occurred. If data was validated,
1194
     * it will first check for errors there, otherwise will try to
1195
     * grab the last error from the Database connection.
1196
     *
1197
     * The return array should be in the following format:
1198
     *  ['source' => 'message']
1199
     *
1200
     * @param bool $forceDB Always grab the db error, not validation
1201
     *
1202
     * @return array<string,string>
1203
     */
1204
    public function errors(bool $forceDB = false)
1205
    {
1206
        // Do we have validation errors?
1207
        if (! $forceDB && ! $this->skipValidation && ($errors = $this->validation->getErrors())) {
24✔
1208
            return $errors;
22✔
1209
        }
1210

1211
        return $this->doErrors();
2✔
1212
    }
1213

1214
    /**
1215
     * Works with Pager to get the size and offset parameters.
1216
     * Expects a GET variable (?page=2) that specifies the page of results
1217
     * to display.
1218
     *
1219
     * @param int|null $perPage Items per page
1220
     * @param string   $group   Will be used by the pagination library to identify a unique pagination set.
1221
     * @param int|null $page    Optional page number (useful when the page number is provided in different way)
1222
     * @param int      $segment Optional URI segment number (if page number is provided by URI segment)
1223
     *
1224
     * @return array|null
1225
     */
1226
    public function paginate(?int $perPage = null, string $group = 'default', ?int $page = null, int $segment = 0)
1227
    {
1228
        // Since multiple models may use the Pager, the Pager must be shared.
1229
        $pager = Services::pager();
8✔
1230

1231
        if ($segment !== 0) {
8✔
1232
            $pager->setSegment($segment, $group);
×
1233
        }
1234

1235
        $page = $page >= 1 ? $page : $pager->getCurrentPage($group);
8✔
1236
        // Store it in the Pager library, so it can be paginated in the views.
1237
        $this->pager = $pager->store($group, $page, $perPage, $this->countAllResults(false), $segment);
8✔
1238
        $perPage     = $this->pager->getPerPage($group);
8✔
1239
        $offset      = ($pager->getCurrentPage($group) - 1) * $perPage;
8✔
1240

1241
        return $this->findAll($perPage, $offset);
8✔
1242
    }
1243

1244
    /**
1245
     * It could be used when you have to change default or override current allowed fields.
1246
     *
1247
     * @param array $allowedFields Array with names of fields
1248
     *
1249
     * @return $this
1250
     */
1251
    public function setAllowedFields(array $allowedFields)
1252
    {
1253
        $this->allowedFields = $allowedFields;
10✔
1254

1255
        return $this;
10✔
1256
    }
1257

1258
    /**
1259
     * Sets whether or not we should whitelist data set during
1260
     * updates or inserts against $this->availableFields.
1261
     *
1262
     * @param bool $protect Value
1263
     *
1264
     * @return $this
1265
     */
1266
    public function protect(bool $protect = true)
1267
    {
1268
        $this->protectFields = $protect;
12✔
1269

1270
        return $this;
12✔
1271
    }
1272

1273
    /**
1274
     * Ensures that only the fields that are allowed to be updated are
1275
     * in the data array.
1276
     *
1277
     * @used-by update() to protect against mass assignment vulnerabilities.
1278
     * @used-by updateBatch() to protect against mass assignment vulnerabilities.
1279
     *
1280
     * @param array $row Row data
1281
     * @phpstan-param row_array $row
1282
     *
1283
     * @throws DataException
1284
     */
1285
    protected function doProtectFields(array $row): array
1286
    {
1287
        if (! $this->protectFields) {
30✔
1288
            return $row;
2✔
1289
        }
1290

1291
        if (empty($this->allowedFields)) {
28✔
1292
            throw DataException::forInvalidAllowedFields(static::class);
×
1293
        }
1294

1295
        foreach (array_keys($row) as $key) {
28✔
1296
            if (! in_array($key, $this->allowedFields, true)) {
28✔
1297
                unset($row[$key]);
12✔
1298
            }
1299
        }
1300

1301
        return $row;
28✔
1302
    }
1303

1304
    /**
1305
     * Ensures that only the fields that are allowed to be inserted are in
1306
     * the data array.
1307
     *
1308
     * @used-by insert() to protect against mass assignment vulnerabilities.
1309
     * @used-by insertBatch() to protect against mass assignment vulnerabilities.
1310
     *
1311
     * @param array $row Row data
1312
     * @phpstan-param row_array $row
1313
     *
1314
     * @throws DataException
1315
     */
1316
    protected function doProtectFieldsForInsert(array $row): array
1317
    {
1318
        return $this->doProtectFields($row);
×
1319
    }
1320

1321
    /**
1322
     * Sets the date or current date if null value is passed.
1323
     *
1324
     * @param int|null $userData An optional PHP timestamp to be converted.
1325
     *
1326
     * @return int|string
1327
     *
1328
     * @throws ModelException
1329
     */
1330
    protected function setDate(?int $userData = null)
1331
    {
1332
        $currentDate = $userData ?? Time::now()->getTimestamp();
100✔
1333

1334
        return $this->intToDate($currentDate);
100✔
1335
    }
1336

1337
    /**
1338
     * A utility function to allow child models to use the type of
1339
     * date/time format that they prefer. This is primarily used for
1340
     * setting created_at, updated_at and deleted_at values, but can be
1341
     * used by inheriting classes.
1342
     *
1343
     * The available time formats are:
1344
     *  - 'int'      - Stores the date as an integer timestamp
1345
     *  - 'datetime' - Stores the data in the SQL datetime format
1346
     *  - 'date'     - Stores the date (only) in the SQL date format.
1347
     *
1348
     * @param int $value value
1349
     *
1350
     * @return int|string
1351
     *
1352
     * @throws ModelException
1353
     */
1354
    protected function intToDate(int $value)
1355
    {
1356
        switch ($this->dateFormat) {
100✔
1357
            case 'int':
100✔
1358
                return $value;
35✔
1359

1360
            case 'datetime':
65✔
1361
                return date('Y-m-d H:i:s', $value);
63✔
1362

1363
            case 'date':
2✔
1364
                return date('Y-m-d', $value);
1✔
1365

1366
            default:
1367
                throw ModelException::forNoDateFormat(static::class);
1✔
1368
        }
1369
    }
1370

1371
    /**
1372
     * Converts Time value to string using $this->dateFormat.
1373
     *
1374
     * The available time formats are:
1375
     *  - 'int'      - Stores the date as an integer timestamp
1376
     *  - 'datetime' - Stores the data in the SQL datetime format
1377
     *  - 'date'     - Stores the date (only) in the SQL date format.
1378
     *
1379
     * @param Time $value value
1380
     *
1381
     * @return int|string
1382
     */
1383
    protected function timeToDate(Time $value)
1384
    {
1385
        switch ($this->dateFormat) {
5✔
1386
            case 'datetime':
5✔
1387
                return $value->format('Y-m-d H:i:s');
3✔
1388

1389
            case 'date':
2✔
1390
                return $value->format('Y-m-d');
1✔
1391

1392
            case 'int':
1✔
1393
                return $value->getTimestamp();
1✔
1394

1395
            default:
1396
                return (string) $value;
×
1397
        }
1398
    }
1399

1400
    /**
1401
     * Set the value of the skipValidation flag.
1402
     *
1403
     * @param bool $skip Value
1404
     *
1405
     * @return $this
1406
     */
1407
    public function skipValidation(bool $skip = true)
1408
    {
1409
        $this->skipValidation = $skip;
2✔
1410

1411
        return $this;
2✔
1412
    }
1413

1414
    /**
1415
     * Allows to set (and reset) validation messages.
1416
     * It could be used when you have to change default or override current validate messages.
1417
     *
1418
     * @param array $validationMessages Value
1419
     *
1420
     * @return $this
1421
     */
1422
    public function setValidationMessages(array $validationMessages)
1423
    {
1424
        $this->validationMessages = $validationMessages;
×
1425

1426
        return $this;
×
1427
    }
1428

1429
    /**
1430
     * Allows to set field wise validation message.
1431
     * It could be used when you have to change default or override current validate messages.
1432
     *
1433
     * @param string $field         Field Name
1434
     * @param array  $fieldMessages Validation messages
1435
     *
1436
     * @return $this
1437
     */
1438
    public function setValidationMessage(string $field, array $fieldMessages)
1439
    {
1440
        $this->validationMessages[$field] = $fieldMessages;
2✔
1441

1442
        return $this;
2✔
1443
    }
1444

1445
    /**
1446
     * Allows to set (and reset) validation rules.
1447
     * It could be used when you have to change default or override current validate rules.
1448
     *
1449
     * @param array $validationRules Value
1450
     *
1451
     * @return $this
1452
     */
1453
    public function setValidationRules(array $validationRules)
1454
    {
1455
        $this->validationRules = $validationRules;
2✔
1456

1457
        return $this;
2✔
1458
    }
1459

1460
    /**
1461
     * Allows to set field wise validation rules.
1462
     * It could be used when you have to change default or override current validate rules.
1463
     *
1464
     * @param string       $field      Field Name
1465
     * @param array|string $fieldRules Validation rules
1466
     *
1467
     * @return $this
1468
     */
1469
    public function setValidationRule(string $field, $fieldRules)
1470
    {
1471
        $rules = $this->validationRules;
2✔
1472

1473
        // ValidationRules can be either a string, which is the group name,
1474
        // or an array of rules.
1475
        if (is_string($rules)) {
2✔
1476
            [$rules, $customErrors] = $this->validation->loadRuleGroup($rules);
1✔
1477

1478
            $this->validationRules = $rules;
1✔
1479
            $this->validationMessages += $customErrors;
1✔
1480
        }
1481

1482
        $this->validationRules[$field] = $fieldRules;
2✔
1483

1484
        return $this;
2✔
1485
    }
1486

1487
    /**
1488
     * Should validation rules be removed before saving?
1489
     * Most handy when doing updates.
1490
     *
1491
     * @param bool $choice Value
1492
     *
1493
     * @return $this
1494
     */
1495
    public function cleanRules(bool $choice = false)
1496
    {
1497
        $this->cleanValidationRules = $choice;
2✔
1498

1499
        return $this;
2✔
1500
    }
1501

1502
    /**
1503
     * Validate the row data against the validation rules (or the validation group)
1504
     * specified in the class property, $validationRules.
1505
     *
1506
     * @param array|object $row Row data
1507
     * @phpstan-param row_array|object $row
1508
     */
1509
    public function validate($row): bool
1510
    {
1511
        $rules = $this->getValidationRules();
118✔
1512

1513
        if ($this->skipValidation || empty($rules) || empty($row)) {
118✔
1514
            return true;
72✔
1515
        }
1516

1517
        // Validation requires array, so cast away.
1518
        if (is_object($row)) {
46✔
1519
            $row = (array) $row;
2✔
1520
        }
1521

1522
        $rules = $this->cleanValidationRules ? $this->cleanValidationRules($rules, $row) : $rules;
46✔
1523

1524
        // If no data existed that needs validation
1525
        // our job is done here.
1526
        if (empty($rules)) {
46✔
1527
            return true;
2✔
1528
        }
1529

1530
        $this->validation->reset()->setRules($rules, $this->validationMessages);
44✔
1531

1532
        return $this->validation->run($row, null, $this->DBGroup);
44✔
1533
    }
1534

1535
    /**
1536
     * Returns the model's defined validation rules so that they
1537
     * can be used elsewhere, if needed.
1538
     *
1539
     * @param array $options Options
1540
     */
1541
    public function getValidationRules(array $options = []): array
1542
    {
1543
        $rules = $this->validationRules;
120✔
1544

1545
        // ValidationRules can be either a string, which is the group name,
1546
        // or an array of rules.
1547
        if (is_string($rules)) {
120✔
1548
            [$rules, $customErrors] = $this->validation->loadRuleGroup($rules);
13✔
1549

1550
            $this->validationMessages += $customErrors;
13✔
1551
        }
1552

1553
        if (isset($options['except'])) {
120✔
1554
            $rules = array_diff_key($rules, array_flip($options['except']));
×
1555
        } elseif (isset($options['only'])) {
120✔
1556
            $rules = array_intersect_key($rules, array_flip($options['only']));
×
1557
        }
1558

1559
        return $rules;
120✔
1560
    }
1561

1562
    /**
1563
     * Returns the model's validation messages, so they
1564
     * can be used elsewhere, if needed.
1565
     */
1566
    public function getValidationMessages(): array
1567
    {
1568
        return $this->validationMessages;
2✔
1569
    }
1570

1571
    /**
1572
     * Removes any rules that apply to fields that have not been set
1573
     * currently so that rules don't block updating when only updating
1574
     * a partial row.
1575
     *
1576
     * @param array $rules Array containing field name and rule
1577
     * @param array $row   Row data (@TODO Remove null in param type)
1578
     * @phpstan-param row_array $row
1579
     */
1580
    protected function cleanValidationRules(array $rules, ?array $row = null): array
1581
    {
1582
        if (empty($row)) {
21✔
1583
            return [];
2✔
1584
        }
1585

1586
        foreach (array_keys($rules) as $field) {
19✔
1587
            if (! array_key_exists($field, $row)) {
19✔
1588
                unset($rules[$field]);
8✔
1589
            }
1590
        }
1591

1592
        return $rules;
19✔
1593
    }
1594

1595
    /**
1596
     * Sets $tempAllowCallbacks value so that we can temporarily override
1597
     * the setting. Resets after the next method that uses triggers.
1598
     *
1599
     * @param bool $val value
1600
     *
1601
     * @return $this
1602
     */
1603
    public function allowCallbacks(bool $val = true)
1604
    {
1605
        $this->tempAllowCallbacks = $val;
3✔
1606

1607
        return $this;
3✔
1608
    }
1609

1610
    /**
1611
     * A simple event trigger for Model Events that allows additional
1612
     * data manipulation within the model. Specifically intended for
1613
     * usage by child models this can be used to format data,
1614
     * save/load related classes, etc.
1615
     *
1616
     * It is the responsibility of the callback methods to return
1617
     * the data itself.
1618
     *
1619
     * Each $eventData array MUST have a 'data' key with the relevant
1620
     * data for callback methods (like an array of key/value pairs to insert
1621
     * or update, an array of results, etc.)
1622
     *
1623
     * If callbacks are not allowed then returns $eventData immediately.
1624
     *
1625
     * @param string $event     Event
1626
     * @param array  $eventData Event Data
1627
     *
1628
     * @return array
1629
     *
1630
     * @throws DataException
1631
     */
1632
    protected function trigger(string $event, array $eventData)
1633
    {
1634
        // Ensure it's a valid event
1635
        if (! isset($this->{$event}) || empty($this->{$event})) {
157✔
1636
            return $eventData;
142✔
1637
        }
1638

1639
        foreach ($this->{$event} as $callback) {
15✔
1640
            if (! method_exists($this, $callback)) {
15✔
1641
                throw DataException::forInvalidMethodTriggered($callback);
1✔
1642
            }
1643

1644
            $eventData = $this->{$callback}($eventData);
14✔
1645
        }
1646

1647
        return $eventData;
14✔
1648
    }
1649

1650
    /**
1651
     * Sets the return type of the results to be as an associative array.
1652
     *
1653
     * @return $this
1654
     */
1655
    public function asArray()
1656
    {
1657
        $this->tempReturnType = 'array';
3✔
1658

1659
        return $this;
3✔
1660
    }
1661

1662
    /**
1663
     * Sets the return type to be of the specified type of object.
1664
     * Defaults to a simple object, but can be any class that has
1665
     * class vars with the same name as the collection columns,
1666
     * or at least allows them to be created.
1667
     *
1668
     * @param string $class Class Name
1669
     *
1670
     * @return $this
1671
     */
1672
    public function asObject(string $class = 'object')
1673
    {
1674
        $this->tempReturnType = $class;
1✔
1675

1676
        return $this;
1✔
1677
    }
1678

1679
    /**
1680
     * Takes a class and returns an array of its public and protected
1681
     * properties as an array suitable for use in creates and updates.
1682
     * This method uses objectToRawArray() internally and does conversion
1683
     * to string on all Time instances
1684
     *
1685
     * @param object $object      Object
1686
     * @param bool   $onlyChanged Only Changed Property
1687
     * @param bool   $recursive   If true, inner entities will be cast as array as well
1688
     *
1689
     * @return array<string, mixed>
1690
     *
1691
     * @throws ReflectionException
1692
     */
1693
    protected function objectToArray($object, bool $onlyChanged = true, bool $recursive = false): array
1694
    {
1695
        $properties = $this->objectToRawArray($object, $onlyChanged, $recursive);
20✔
1696

1697
        // Convert any Time instances to appropriate $dateFormat
1698
        return $this->timeToString($properties);
20✔
1699
    }
1700

1701
    /**
1702
     * Convert any Time instances to appropriate $dateFormat.
1703
     *
1704
     * @param array<string, mixed> $properties
1705
     *
1706
     * @return array<string, mixed>
1707
     */
1708
    protected function timeToString(array $properties): array
1709
    {
1710
        if ($properties === []) {
20✔
1711
            return [];
×
1712
        }
1713

1714
        return array_map(function ($value) {
20✔
1715
            if ($value instanceof Time) {
20✔
1716
                return $this->timeToDate($value);
5✔
1717
            }
1718

1719
            return $value;
20✔
1720
        }, $properties);
20✔
1721
    }
1722

1723
    /**
1724
     * Takes a class and returns an array of its public and protected
1725
     * properties as an array with raw values.
1726
     *
1727
     * @param object $object      Object
1728
     * @param bool   $onlyChanged Only Changed Property
1729
     * @param bool   $recursive   If true, inner entities will be casted as array as well
1730
     *
1731
     * @return array<string, mixed>
1732
     *
1733
     * @throws ReflectionException
1734
     */
1735
    protected function objectToRawArray($object, bool $onlyChanged = true, bool $recursive = false): array
1736
    {
1737
        // Entity::toRawArray() returns array.
1738
        if (method_exists($object, 'toRawArray')) {
20✔
1739
            $properties = $object->toRawArray($onlyChanged, $recursive);
18✔
1740
        } else {
1741
            $mirror = new ReflectionClass($object);
2✔
1742
            $props  = $mirror->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED);
2✔
1743

1744
            $properties = [];
2✔
1745

1746
            // Loop over each property,
1747
            // saving the name/value in a new array we can return.
1748
            foreach ($props as $prop) {
2✔
1749
                // Must make protected values accessible.
1750
                $prop->setAccessible(true);
2✔
1751
                $properties[$prop->getName()] = $prop->getValue($object);
2✔
1752
            }
1753
        }
1754

1755
        return $properties;
20✔
1756
    }
1757

1758
    /**
1759
     * Transform data to array.
1760
     *
1761
     * @param array|object|null $row Row data
1762
     * @phpstan-param row_array|object|null $row
1763
     * @param string $type Type of data (insert|update)
1764
     *
1765
     * @throws DataException
1766
     * @throws InvalidArgumentException
1767
     * @throws ReflectionException
1768
     */
1769
    protected function transformDataToArray($row, string $type): array
1770
    {
1771
        if (! in_array($type, ['insert', 'update'], true)) {
95✔
1772
            throw new InvalidArgumentException(sprintf('Invalid type "%s" used upon transforming data to array.', $type));
1✔
1773
        }
1774

1775
        if (! $this->allowEmptyInserts && empty($row)) {
94✔
1776
            throw DataException::forEmptyDataset($type);
4✔
1777
        }
1778

1779
        // If $row is using a custom class with public or protected
1780
        // properties representing the collection elements, we need to grab
1781
        // them as an array.
1782
        if (is_object($row) && ! $row instanceof stdClass) {
92✔
1783
            // If it validates with entire rules, all fields are needed.
1784
            $onlyChanged = ($this->skipValidation === false && $this->cleanValidationRules === false)
18✔
1785
                ? false : ($type === 'update');
18✔
1786

1787
            $row = $this->objectToArray($row, $onlyChanged, true);
18✔
1788
        }
1789

1790
        // If it's still a stdClass, go ahead and convert to
1791
        // an array so doProtectFields and other model methods
1792
        // don't have to do special checks.
1793
        if (is_object($row)) {
92✔
1794
            $row = (array) $row;
12✔
1795
        }
1796

1797
        // If it's still empty here, means $row is no change or is empty object
1798
        if (! $this->allowEmptyInserts && empty($row)) {
92✔
1799
            throw DataException::forEmptyDataset($type);
2✔
1800
        }
1801

1802
        return $row;
91✔
1803
    }
1804

1805
    /**
1806
     * Provides the db connection and model's properties.
1807
     *
1808
     * @param string $name Name
1809
     *
1810
     * @return array|bool|float|int|object|string|null
1811
     */
1812
    public function __get(string $name)
1813
    {
1814
        if (property_exists($this, $name)) {
45✔
1815
            return $this->{$name};
45✔
1816
        }
1817

1818
        return $this->db->{$name} ?? null;
1✔
1819
    }
1820

1821
    /**
1822
     * Checks for the existence of properties across this model, and db connection.
1823
     *
1824
     * @param string $name Name
1825
     */
1826
    public function __isset(string $name): bool
1827
    {
1828
        if (property_exists($this, $name)) {
45✔
1829
            return true;
45✔
1830
        }
1831

1832
        return isset($this->db->{$name});
1✔
1833
    }
1834

1835
    /**
1836
     * Provides direct access to method in the database connection.
1837
     *
1838
     * @param string $name   Name
1839
     * @param array  $params Params
1840
     *
1841
     * @return $this|null
1842
     */
1843
    public function __call(string $name, array $params)
1844
    {
1845
        if (method_exists($this->db, $name)) {
×
1846
            return $this->db->{$name}(...$params);
×
1847
        }
1848

1849
        return null;
×
1850
    }
1851

1852
    /**
1853
     * Replace any placeholders within the rules with the values that
1854
     * match the 'key' of any properties being set. For example, if
1855
     * we had the following $data array:
1856
     *
1857
     * [ 'id' => 13 ]
1858
     *
1859
     * and the following rule:
1860
     *
1861
     *  'required|is_unique[users,email,id,{id}]'
1862
     *
1863
     * The value of {id} would be replaced with the actual id in the form data:
1864
     *
1865
     *  'required|is_unique[users,email,id,13]'
1866
     *
1867
     * @param array $rules Validation rules
1868
     * @param array $data  Data
1869
     *
1870
     * @codeCoverageIgnore
1871
     *
1872
     * @deprecated use fillPlaceholders($rules, $data) from Validation instead
1873
     */
1874
    protected function fillPlaceholders(array $rules, array $data): array
1875
    {
1876
        $replacements = [];
1877

1878
        foreach ($data as $key => $value) {
1879
            $replacements['{' . $key . '}'] = $value;
1880
        }
1881

1882
        if (! empty($replacements)) {
1883
            foreach ($rules as &$rule) {
1884
                if (is_array($rule)) {
1885
                    foreach ($rule as &$row) {
1886
                        // Should only be an `errors` array
1887
                        // which doesn't take placeholders.
1888
                        if (is_array($row)) {
1889
                            continue;
1890
                        }
1891

1892
                        $row = strtr($row, $replacements);
1893
                    }
1894

1895
                    continue;
1896
                }
1897

1898
                $rule = strtr($rule, $replacements);
1899
            }
1900
        }
1901

1902
        return $rules;
1903
    }
1904

1905
    /**
1906
     * Sets $allowEmptyInserts.
1907
     */
1908
    public function allowEmptyInserts(bool $value = true): self
1909
    {
1910
        $this->allowEmptyInserts = $value;
1✔
1911

1912
        return $this;
1✔
1913
    }
1914
}
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