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

codeigniter4 / CodeIgniter4 / 7346990850

28 Dec 2023 10:57AM UTC coverage: 85.044% (-0.006%) from 85.05%
7346990850

push

github

kenjis
Merge remote-tracking branch 'origin/develop' into 4.5

 Conflicts:
	phpstan-baseline.php
	system/BaseModel.php
	system/CodeIgniter.php
	system/Model.php
	system/Test/FeatureTestCase.php
	system/Test/TestResponse.php

87 of 91 new or added lines in 20 files covered. (95.6%)

2 existing lines in 1 file now uncovered.

19368 of 22774 relevant lines covered (85.04%)

193.82 hits per line

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

95.08
/system/BaseModel.php
1
<?php
2

3
declare(strict_types=1);
4

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

14
namespace CodeIgniter;
15

16
use Closure;
17
use CodeIgniter\Database\BaseConnection;
18
use CodeIgniter\Database\BaseResult;
19
use CodeIgniter\Database\Exceptions\DatabaseException;
20
use CodeIgniter\Database\Exceptions\DataException;
21
use CodeIgniter\Database\Query;
22
use CodeIgniter\Exceptions\ModelException;
23
use CodeIgniter\I18n\Time;
24
use CodeIgniter\Pager\Pager;
25
use CodeIgniter\Validation\ValidationInterface;
26
use Config\Feature;
27
use Config\Services;
28
use InvalidArgumentException;
29
use ReflectionClass;
30
use ReflectionException;
31
use ReflectionProperty;
32
use stdClass;
33

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

68
    /**
69
     * Last insert ID
70
     *
71
     * @var int|string
72
     */
73
    protected $insertID = 0;
74

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

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

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

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

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

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

126
    /**
127
     * The column used for insert timestamps
128
     *
129
     * @var string
130
     */
131
    protected $createdField = 'created_at';
132

133
    /**
134
     * The column used for update timestamps
135
     *
136
     * @var string
137
     */
138
    protected $updatedField = 'updated_at';
139

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

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

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

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

171
    /**
172
     * Database Connection
173
     *
174
     * @var BaseConnection
175
     */
176
    protected $db;
177

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

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

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

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

211
    /**
212
     * Our validator instance.
213
     *
214
     * @var ValidationInterface|null
215
     */
216
    protected $validation;
217

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

231
    /**
232
     * Whether to trigger the defined callbacks
233
     *
234
     * @var bool
235
     */
236
    protected $allowCallbacks = true;
237

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

246
    /**
247
     * Callbacks for beforeInsert
248
     *
249
     * @var array
250
     */
251
    protected $beforeInsert = [];
252

253
    /**
254
     * Callbacks for afterInsert
255
     *
256
     * @var array
257
     */
258
    protected $afterInsert = [];
259

260
    /**
261
     * Callbacks for beforeUpdate
262
     *
263
     * @var array
264
     */
265
    protected $beforeUpdate = [];
266

267
    /**
268
     * Callbacks for afterUpdate
269
     *
270
     * @var array
271
     */
272
    protected $afterUpdate = [];
273

274
    /**
275
     * Callbacks for beforeInsertBatch
276
     *
277
     * @var array
278
     */
279
    protected $beforeInsertBatch = [];
280

281
    /**
282
     * Callbacks for afterInsertBatch
283
     *
284
     * @var array
285
     */
286
    protected $afterInsertBatch = [];
287

288
    /**
289
     * Callbacks for beforeUpdateBatch
290
     *
291
     * @var array
292
     */
293
    protected $beforeUpdateBatch = [];
294

295
    /**
296
     * Callbacks for afterUpdateBatch
297
     *
298
     * @var array
299
     */
300
    protected $afterUpdateBatch = [];
301

302
    /**
303
     * Callbacks for beforeFind
304
     *
305
     * @var array
306
     */
307
    protected $beforeFind = [];
308

309
    /**
310
     * Callbacks for afterFind
311
     *
312
     * @var array
313
     */
314
    protected $afterFind = [];
315

316
    /**
317
     * Callbacks for beforeDelete
318
     *
319
     * @var array
320
     */
321
    protected $beforeDelete = [];
322

323
    /**
324
     * Callbacks for afterDelete
325
     *
326
     * @var array
327
     */
328
    protected $afterDelete = [];
329

330
    /**
331
     * Whether to allow inserting empty data.
332
     */
333
    protected bool $allowEmptyInserts = false;
334

335
    public function __construct(?ValidationInterface $validation = null)
336
    {
337
        $this->tempReturnType     = $this->returnType;
275✔
338
        $this->tempUseSoftDeletes = $this->useSoftDeletes;
275✔
339
        $this->tempAllowCallbacks = $this->allowCallbacks;
275✔
340

341
        $this->validation = $validation;
275✔
342

343
        $this->initialize();
275✔
344
    }
345

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

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

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

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

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

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

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

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

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

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

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

469
    /**
470
     * Works with the find* methods to return only the rows that
471
     * have been deleted.
472
     * This method works only with dbCalls.
473
     *
474
     * @return void
475
     */
476
    abstract protected function doOnlyDeleted();
477

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

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

498
    /**
499
     * Public getter to return the id value using the idValue() method.
500
     * For example with SQL this will return $data->$this->primaryKey.
501
     *
502
     * @param array|object $row Row data
503
     * @phpstan-param row_array|object $row
504
     *
505
     * @return array|int|string|null
506
     */
507
    abstract public function getIdValue($row);
508

509
    /**
510
     * Override countAllResults to account for soft deleted accounts.
511
     * This method works only with dbCalls.
512
     *
513
     * @param bool $reset Reset
514
     * @param bool $test  Test
515
     *
516
     * @return int|string
517
     */
518
    abstract public function countAllResults(bool $reset = true, bool $test = false);
519

520
    /**
521
     * Loops over records in batches, allowing you to operate on them.
522
     * This method works only with dbCalls.
523
     *
524
     * @param int     $size     Size
525
     * @param Closure $userFunc Callback Function
526
     *
527
     * @return void
528
     *
529
     * @throws DataException
530
     */
531
    abstract public function chunk(int $size, Closure $userFunc);
532

533
    /**
534
     * Fetches the row of database.
535
     *
536
     * @param array|int|string|null $id One primary key or an array of primary keys
537
     *
538
     * @return array|object|null The resulting row of data, or null.
539
     * @phpstan-return ($id is int|string ? row_array|object|null : list<row_array|object>)
540
     */
541
    public function find($id = null)
542
    {
543
        $singleton = is_numeric($id) || is_string($id);
37✔
544

545
        if ($this->tempAllowCallbacks) {
37✔
546
            // Call the before event and check for a return
547
            $eventData = $this->trigger('beforeFind', [
34✔
548
                'id'        => $id,
34✔
549
                'method'    => 'find',
34✔
550
                'singleton' => $singleton,
34✔
551
            ]);
34✔
552

553
            if (isset($eventData['returnData']) && $eventData['returnData'] === true) {
34✔
554
                return $eventData['data'];
3✔
555
            }
556
        }
557

558
        $eventData = [
35✔
559
            'id'        => $id,
35✔
560
            'data'      => $this->doFind($singleton, $id),
35✔
561
            'method'    => 'find',
35✔
562
            'singleton' => $singleton,
35✔
563
        ];
35✔
564

565
        if ($this->tempAllowCallbacks) {
34✔
566
            $eventData = $this->trigger('afterFind', $eventData);
31✔
567
        }
568

569
        $this->tempReturnType     = $this->returnType;
34✔
570
        $this->tempUseSoftDeletes = $this->useSoftDeletes;
34✔
571
        $this->tempAllowCallbacks = $this->allowCallbacks;
34✔
572

573
        return $eventData['data'];
34✔
574
    }
575

576
    /**
577
     * Fetches the column of database.
578
     *
579
     * @param string $columnName Column Name
580
     *
581
     * @return array|null The resulting row of data, or null if no data found.
582
     *
583
     * @throws DataException
584
     */
585
    public function findColumn(string $columnName)
586
    {
587
        if (strpos($columnName, ',') !== false) {
2✔
588
            throw DataException::forFindColumnHaveMultipleColumns();
1✔
589
        }
590

591
        $resultSet = $this->doFindColumn($columnName);
1✔
592

593
        return $resultSet ? array_column($resultSet, $columnName) : null;
1✔
594
    }
595

596
    /**
597
     * Fetches all results, while optionally limiting them.
598
     *
599
     * @param int $limit  Limit
600
     * @param int $offset Offset
601
     *
602
     * @return array
603
     */
604
    public function findAll(?int $limit = null, int $offset = 0)
605
    {
606
        if (config(Feature::class)->limitZeroAsAll) {
17✔
607
            $limit ??= 0;
17✔
608
        }
609

610
        if ($this->tempAllowCallbacks) {
17✔
611
            // Call the before event and check for a return
612
            $eventData = $this->trigger('beforeFind', [
17✔
613
                'method'    => 'findAll',
17✔
614
                'limit'     => $limit,
17✔
615
                'offset'    => $offset,
17✔
616
                'singleton' => false,
17✔
617
            ]);
17✔
618

619
            if (isset($eventData['returnData']) && $eventData['returnData'] === true) {
17✔
620
                return $eventData['data'];
1✔
621
            }
622
        }
623

624
        $eventData = [
17✔
625
            'data'      => $this->doFindAll($limit, $offset),
17✔
626
            'limit'     => $limit,
17✔
627
            'offset'    => $offset,
17✔
628
            'method'    => 'findAll',
17✔
629
            'singleton' => false,
17✔
630
        ];
17✔
631

632
        if ($this->tempAllowCallbacks) {
17✔
633
            $eventData = $this->trigger('afterFind', $eventData);
17✔
634
        }
635

636
        $this->tempReturnType     = $this->returnType;
17✔
637
        $this->tempUseSoftDeletes = $this->useSoftDeletes;
17✔
638
        $this->tempAllowCallbacks = $this->allowCallbacks;
17✔
639

640
        return $eventData['data'];
17✔
641
    }
642

643
    /**
644
     * Returns the first row of the result set.
645
     *
646
     * @return array|object|null
647
     */
648
    public function first()
649
    {
650
        if ($this->tempAllowCallbacks) {
19✔
651
            // Call the before event and check for a return
652
            $eventData = $this->trigger('beforeFind', [
19✔
653
                'method'    => 'first',
19✔
654
                'singleton' => true,
19✔
655
            ]);
19✔
656

657
            if (isset($eventData['returnData']) && $eventData['returnData'] === true) {
19✔
658
                return $eventData['data'];
1✔
659
            }
660
        }
661

662
        $eventData = [
19✔
663
            'data'      => $this->doFirst(),
19✔
664
            'method'    => 'first',
19✔
665
            'singleton' => true,
19✔
666
        ];
19✔
667

668
        if ($this->tempAllowCallbacks) {
19✔
669
            $eventData = $this->trigger('afterFind', $eventData);
19✔
670
        }
671

672
        $this->tempReturnType     = $this->returnType;
19✔
673
        $this->tempUseSoftDeletes = $this->useSoftDeletes;
19✔
674
        $this->tempAllowCallbacks = $this->allowCallbacks;
19✔
675

676
        return $eventData['data'];
19✔
677
    }
678

679
    /**
680
     * A convenience method that will attempt to determine whether the
681
     * data should be inserted or updated. Will work with either
682
     * an array or object. When using with custom class objects,
683
     * you must ensure that the class will provide access to the class
684
     * variables, even if through a magic method.
685
     *
686
     * @param array|object $row Row data
687
     * @phpstan-param row_array|object $row
688
     *
689
     * @throws ReflectionException
690
     */
691
    public function save($row): bool
692
    {
693
        if ((array) $row === []) {
22✔
694
            return true;
1✔
695
        }
696

697
        if ($this->shouldUpdate($row)) {
21✔
698
            $response = $this->update($this->getIdValue($row), $row);
12✔
699
        } else {
700
            $response = $this->insert($row, false);
12✔
701

702
            if ($response !== false) {
11✔
703
                $response = true;
10✔
704
            }
705
        }
706

707
        return $response;
20✔
708
    }
709

710
    /**
711
     * This method is called on save to determine if entry have to be updated.
712
     * If this method returns false insert operation will be executed
713
     *
714
     * @param array|object $row Row data
715
     * @phpstan-param row_array|object $row
716
     */
717
    protected function shouldUpdate($row): bool
718
    {
719
        $id = $this->getIdValue($row);
21✔
720

721
        return ! ($id === null || $id === []);
21✔
722
    }
723

724
    /**
725
     * Returns last insert ID or 0.
726
     *
727
     * @return int|string
728
     */
729
    public function getInsertID()
730
    {
731
        return is_numeric($this->insertID) ? (int) $this->insertID : $this->insertID;
11✔
732
    }
733

734
    /**
735
     * Inserts data into the database. If an object is provided,
736
     * it will attempt to convert it to an array.
737
     *
738
     * @param array|object|null $row Row data
739
     * @phpstan-param row_array|object|null $row
740
     * @param bool $returnID Whether insert ID should be returned or not.
741
     *
742
     * @return bool|int|string insert ID or true on success. false on failure.
743
     * @phpstan-return ($returnID is true ? int|string|false : bool)
744
     *
745
     * @throws ReflectionException
746
     */
747
    public function insert($row = null, bool $returnID = true)
748
    {
749
        $this->insertID = 0;
79✔
750

751
        // Set $cleanValidationRules to false temporary.
752
        $cleanValidationRules       = $this->cleanValidationRules;
79✔
753
        $this->cleanValidationRules = false;
79✔
754

755
        $row = $this->transformDataToArray($row, 'insert');
79✔
756

757
        // Validate data before saving.
758
        if (! $this->skipValidation && ! $this->validate($row)) {
77✔
759
            // Restore $cleanValidationRules
760
            $this->cleanValidationRules = $cleanValidationRules;
19✔
761

762
            return false;
19✔
763
        }
764

765
        // Restore $cleanValidationRules
766
        $this->cleanValidationRules = $cleanValidationRules;
60✔
767

768
        // Must be called first, so we don't
769
        // strip out created_at values.
770
        $row = $this->doProtectFieldsForInsert($row);
60✔
771

772
        // doProtectFields() can further remove elements from
773
        // $row, so we need to check for empty dataset again
774
        if (! $this->allowEmptyInserts && $row === []) {
59✔
775
            throw DataException::forEmptyDataset('insert');
2✔
776
        }
777

778
        // Set created_at and updated_at with same time
779
        $date = $this->setDate();
57✔
780
        $row  = $this->setCreatedField($row, $date);
57✔
781
        $row  = $this->setUpdatedField($row, $date);
57✔
782

783
        $eventData = ['data' => $row];
57✔
784

785
        if ($this->tempAllowCallbacks) {
57✔
786
            $eventData = $this->trigger('beforeInsert', $eventData);
57✔
787
        }
788

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

791
        $eventData = [
55✔
792
            'id'     => $this->insertID,
55✔
793
            'data'   => $eventData['data'],
55✔
794
            'result' => $result,
55✔
795
        ];
55✔
796

797
        if ($this->tempAllowCallbacks) {
55✔
798
            // Trigger afterInsert events with the inserted data and new ID
799
            $this->trigger('afterInsert', $eventData);
55✔
800
        }
801

802
        $this->tempAllowCallbacks = $this->allowCallbacks;
55✔
803

804
        // If insertion failed, get out of here
805
        if (! $result) {
55✔
806
            return $result;
2✔
807
        }
808

809
        // otherwise return the insertID, if requested.
810
        return $returnID ? $this->insertID : $result;
53✔
811
    }
812

813
    /**
814
     * Set datetime to created field.
815
     *
816
     * @phpstan-param row_array $row
817
     * @param int|string $date timestamp or datetime string
818
     */
819
    protected function setCreatedField(array $row, $date): array
820
    {
821
        if ($this->useTimestamps && $this->createdField !== '' && ! array_key_exists($this->createdField, $row)) {
67✔
822
            $row[$this->createdField] = $date;
12✔
823
        }
824

825
        return $row;
67✔
826
    }
827

828
    /**
829
     * Set datetime to updated field.
830
     *
831
     * @phpstan-param row_array $row
832
     * @param int|string $date timestamp or datetime string
833
     */
834
    protected function setUpdatedField(array $row, $date): array
835
    {
836
        if ($this->useTimestamps && $this->updatedField !== '' && ! array_key_exists($this->updatedField, $row)) {
81✔
837
            $row[$this->updatedField] = $date;
15✔
838
        }
839

840
        return $row;
81✔
841
    }
842

843
    /**
844
     * Compiles batch insert runs the queries, validating each row prior.
845
     *
846
     * @param list<array|object>|null $set an associative array of insert values
847
     * @phpstan-param list<row_array|object>|null $set
848
     * @param bool|null $escape    Whether to escape values
849
     * @param int       $batchSize The size of the batch to run
850
     * @param bool      $testing   True means only number of records is returned, false will execute the query
851
     *
852
     * @return bool|int Number of rows inserted or FALSE on failure
853
     *
854
     * @throws ReflectionException
855
     */
856
    public function insertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100, bool $testing = false)
857
    {
858
        // Set $cleanValidationRules to false temporary.
859
        $cleanValidationRules       = $this->cleanValidationRules;
13✔
860
        $this->cleanValidationRules = false;
13✔
861

862
        if (is_array($set)) {
13✔
863
            foreach ($set as &$row) {
13✔
864
                // If $row is using a custom class with public or protected
865
                // properties representing the collection elements, we need to grab
866
                // them as an array.
867
                if (is_object($row) && ! $row instanceof stdClass) {
13✔
868
                    $row = $this->objectToArray($row, false, true);
1✔
869
                }
870

871
                // If it's still a stdClass, go ahead and convert to
872
                // an array so doProtectFields and other model methods
873
                // don't have to do special checks.
874
                if (is_object($row)) {
13✔
875
                    $row = (array) $row;
×
876
                }
877

878
                // Validate every row.
879
                if (! $this->skipValidation && ! $this->validate($row)) {
13✔
880
                    // Restore $cleanValidationRules
881
                    $this->cleanValidationRules = $cleanValidationRules;
3✔
882

883
                    return false;
3✔
884
                }
885

886
                // Must be called first so we don't
887
                // strip out created_at values.
888
                $row = $this->doProtectFieldsForInsert($row);
10✔
889

890
                // Set created_at and updated_at with same time
891
                $date = $this->setDate();
10✔
892
                $row  = $this->setCreatedField($row, $date);
10✔
893
                $row  = $this->setUpdatedField($row, $date);
10✔
894
            }
895
        }
896

897
        // Restore $cleanValidationRules
898
        $this->cleanValidationRules = $cleanValidationRules;
10✔
899

900
        $eventData = ['data' => $set];
10✔
901

902
        if ($this->tempAllowCallbacks) {
10✔
903
            $eventData = $this->trigger('beforeInsertBatch', $eventData);
10✔
904
        }
905

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

908
        $eventData = [
10✔
909
            'data'   => $eventData['data'],
10✔
910
            'result' => $result,
10✔
911
        ];
10✔
912

913
        if ($this->tempAllowCallbacks) {
10✔
914
            // Trigger afterInsert events with the inserted data and new ID
915
            $this->trigger('afterInsertBatch', $eventData);
10✔
916
        }
917

918
        $this->tempAllowCallbacks = $this->allowCallbacks;
10✔
919

920
        return $result;
10✔
921
    }
922

923
    /**
924
     * Updates a single record in the database. If an object is provided,
925
     * it will attempt to convert it into an array.
926
     *
927
     * @param array|int|string|null $id
928
     * @param array|object|null     $row Row data
929
     * @phpstan-param row_array|object|null $row
930
     *
931
     * @throws ReflectionException
932
     */
933
    public function update($id = null, $row = null): bool
934
    {
935
        if (is_bool($id)) {
34✔
936
            throw new InvalidArgumentException('update(): argument #1 ($id) should not be boolean.');
1✔
937
        }
938

939
        if (is_numeric($id) || is_string($id)) {
33✔
940
            $id = [$id];
28✔
941
        }
942

943
        $row = $this->transformDataToArray($row, 'update');
33✔
944

945
        // Validate data before saving.
946
        if (! $this->skipValidation && ! $this->validate($row)) {
31✔
947
            return false;
4✔
948
        }
949

950
        // Must be called first, so we don't
951
        // strip out updated_at values.
952
        $row = $this->doProtectFields($row);
27✔
953

954
        // doProtectFields() can further remove elements from
955
        // $row, so we need to check for empty dataset again
956
        if ($row === []) {
27✔
957
            throw DataException::forEmptyDataset('update');
2✔
958
        }
959

960
        $row = $this->setUpdatedField($row, $this->setDate());
25✔
961

962
        $eventData = [
25✔
963
            'id'   => $id,
25✔
964
            'data' => $row,
25✔
965
        ];
25✔
966

967
        if ($this->tempAllowCallbacks) {
25✔
968
            $eventData = $this->trigger('beforeUpdate', $eventData);
25✔
969
        }
970

971
        $eventData = [
25✔
972
            'id'     => $id,
25✔
973
            'data'   => $eventData['data'],
25✔
974
            'result' => $this->doUpdate($id, $eventData['data']),
25✔
975
        ];
25✔
976

977
        if ($this->tempAllowCallbacks) {
24✔
978
            $this->trigger('afterUpdate', $eventData);
24✔
979
        }
980

981
        $this->tempAllowCallbacks = $this->allowCallbacks;
24✔
982

983
        return $eventData['result'];
24✔
984
    }
985

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

1013
                // If it's still a stdClass, go ahead and convert to
1014
                // an array so doProtectFields and other model methods
1015
                // don't have to do special checks.
1016
                if (is_object($row)) {
5✔
1017
                    $row = (array) $row;
×
1018
                }
1019

1020
                // Validate data before saving.
1021
                if (! $this->skipValidation && ! $this->validate($row)) {
5✔
1022
                    return false;
1✔
1023
                }
1024

1025
                // Save updateIndex for later
1026
                $updateIndex = $row[$index] ?? null;
4✔
1027

1028
                if ($updateIndex === null) {
4✔
1029
                    throw new InvalidArgumentException(
1✔
1030
                        'The index ("' . $index . '") for updateBatch() is missing in the data: '
1✔
1031
                        . json_encode($row)
1✔
1032
                    );
1✔
1033
                }
1034

1035
                // Must be called first so we don't
1036
                // strip out updated_at values.
1037
                $row = $this->doProtectFields($row);
3✔
1038

1039
                // Restore updateIndex value in case it was wiped out
1040
                if ($updateIndex !== null) {
3✔
1041
                    $row[$index] = $updateIndex;
3✔
1042
                }
1043

1044
                $row = $this->setUpdatedField($row, $this->setDate());
3✔
1045
            }
1046
        }
1047

1048
        $eventData = ['data' => $set];
3✔
1049

1050
        if ($this->tempAllowCallbacks) {
3✔
1051
            $eventData = $this->trigger('beforeUpdateBatch', $eventData);
3✔
1052
        }
1053

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

1056
        $eventData = [
3✔
1057
            'data'   => $eventData['data'],
3✔
1058
            'result' => $result,
3✔
1059
        ];
3✔
1060

1061
        if ($this->tempAllowCallbacks) {
3✔
1062
            // Trigger afterInsert events with the inserted data and new ID
1063
            $this->trigger('afterUpdateBatch', $eventData);
3✔
1064
        }
1065

1066
        $this->tempAllowCallbacks = $this->allowCallbacks;
3✔
1067

1068
        return $result;
3✔
1069
    }
1070

1071
    /**
1072
     * Deletes a single record from the database where $id matches.
1073
     *
1074
     * @param array|int|string|null $id    The rows primary key(s)
1075
     * @param bool                  $purge Allows overriding the soft deletes setting.
1076
     *
1077
     * @return BaseResult|bool
1078
     *
1079
     * @throws DatabaseException
1080
     */
1081
    public function delete($id = null, bool $purge = false)
1082
    {
1083
        if (is_bool($id)) {
40✔
1084
            throw new InvalidArgumentException('delete(): argument #1 ($id) should not be boolean.');
×
1085
        }
1086

1087
        if ($id && (is_numeric($id) || is_string($id))) {
40✔
1088
            $id = [$id];
20✔
1089
        }
1090

1091
        $eventData = [
40✔
1092
            'id'    => $id,
40✔
1093
            'purge' => $purge,
40✔
1094
        ];
40✔
1095

1096
        if ($this->tempAllowCallbacks) {
40✔
1097
            $this->trigger('beforeDelete', $eventData);
39✔
1098
        }
1099

1100
        $eventData = [
40✔
1101
            'id'     => $id,
40✔
1102
            'data'   => null,
40✔
1103
            'purge'  => $purge,
40✔
1104
            'result' => $this->doDelete($id, $purge),
40✔
1105
        ];
40✔
1106

1107
        if ($this->tempAllowCallbacks) {
27✔
1108
            $this->trigger('afterDelete', $eventData);
26✔
1109
        }
1110

1111
        $this->tempAllowCallbacks = $this->allowCallbacks;
27✔
1112

1113
        return $eventData['result'];
27✔
1114
    }
1115

1116
    /**
1117
     * Permanently deletes all rows that have been marked as deleted
1118
     * through soft deletes (deleted = 1).
1119
     *
1120
     * @return bool|string Returns a string if in test mode.
1121
     */
1122
    public function purgeDeleted()
1123
    {
1124
        if (! $this->useSoftDeletes) {
2✔
1125
            return true;
1✔
1126
        }
1127

1128
        return $this->doPurgeDeleted();
1✔
1129
    }
1130

1131
    /**
1132
     * Sets $useSoftDeletes value so that we can temporarily override
1133
     * the soft deletes settings. Can be used for all find* methods.
1134
     *
1135
     * @param bool $val Value
1136
     *
1137
     * @return $this
1138
     */
1139
    public function withDeleted(bool $val = true)
1140
    {
1141
        $this->tempUseSoftDeletes = ! $val;
23✔
1142

1143
        return $this;
23✔
1144
    }
1145

1146
    /**
1147
     * Works with the find* methods to return only the rows that
1148
     * have been deleted.
1149
     *
1150
     * @return $this
1151
     */
1152
    public function onlyDeleted()
1153
    {
1154
        $this->tempUseSoftDeletes = false;
1✔
1155
        $this->doOnlyDeleted();
1✔
1156

1157
        return $this;
1✔
1158
    }
1159

1160
    /**
1161
     * Compiles a replace and runs the query.
1162
     *
1163
     * @param array|null $row Row data
1164
     * @phpstan-param row_array|null $row
1165
     * @param bool $returnSQL Set to true to return Query String
1166
     *
1167
     * @return BaseResult|false|Query|string
1168
     */
1169
    public function replace(?array $row = null, bool $returnSQL = false)
1170
    {
1171
        // Validate data before saving.
1172
        if (($row !== null) && ! $this->skipValidation && ! $this->validate($row)) {
3✔
1173
            return false;
1✔
1174
        }
1175

1176
        $row = $this->setUpdatedField((array) $row, $this->setDate());
2✔
1177

1178
        return $this->doReplace($row, $returnSQL);
2✔
1179
    }
1180

1181
    /**
1182
     * Grabs the last error(s) that occurred. If data was validated,
1183
     * it will first check for errors there, otherwise will try to
1184
     * grab the last error from the Database connection.
1185
     *
1186
     * The return array should be in the following format:
1187
     *  ['source' => 'message']
1188
     *
1189
     * @param bool $forceDB Always grab the db error, not validation
1190
     *
1191
     * @return array<string,string>
1192
     */
1193
    public function errors(bool $forceDB = false)
1194
    {
1195
        if ($this->validation === null) {
24✔
1196
            return $this->doErrors();
×
1197
        }
1198

1199
        // Do we have validation errors?
1200
        if (! $forceDB && ! $this->skipValidation && ($errors = $this->validation->getErrors())) {
24✔
1201
            return $errors;
22✔
1202
        }
1203

1204
        return $this->doErrors();
2✔
1205
    }
1206

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

1224
        if ($segment !== 0) {
8✔
1225
            $pager->setSegment($segment, $group);
×
1226
        }
1227

1228
        $page = $page >= 1 ? $page : $pager->getCurrentPage($group);
8✔
1229
        // Store it in the Pager library, so it can be paginated in the views.
1230
        $this->pager = $pager->store($group, $page, $perPage, $this->countAllResults(false), $segment);
8✔
1231
        $perPage     = $this->pager->getPerPage($group);
8✔
1232
        $offset      = ($pager->getCurrentPage($group) - 1) * $perPage;
8✔
1233

1234
        return $this->findAll($perPage, $offset);
8✔
1235
    }
1236

1237
    /**
1238
     * It could be used when you have to change default or override current allowed fields.
1239
     *
1240
     * @param array $allowedFields Array with names of fields
1241
     *
1242
     * @return $this
1243
     */
1244
    public function setAllowedFields(array $allowedFields)
1245
    {
1246
        $this->allowedFields = $allowedFields;
10✔
1247

1248
        return $this;
10✔
1249
    }
1250

1251
    /**
1252
     * Sets whether or not we should whitelist data set during
1253
     * updates or inserts against $this->availableFields.
1254
     *
1255
     * @param bool $protect Value
1256
     *
1257
     * @return $this
1258
     */
1259
    public function protect(bool $protect = true)
1260
    {
1261
        $this->protectFields = $protect;
12✔
1262

1263
        return $this;
12✔
1264
    }
1265

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

1284
        if ($this->allowedFields === []) {
28✔
1285
            throw DataException::forInvalidAllowedFields(static::class);
×
1286
        }
1287

1288
        foreach (array_keys($row) as $key) {
28✔
1289
            if (! in_array($key, $this->allowedFields, true)) {
28✔
1290
                unset($row[$key]);
12✔
1291
            }
1292
        }
1293

1294
        return $row;
28✔
1295
    }
1296

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

1314
    /**
1315
     * Sets the date or current date if null value is passed.
1316
     *
1317
     * @param int|null $userData An optional PHP timestamp to be converted.
1318
     *
1319
     * @return int|string
1320
     *
1321
     * @throws ModelException
1322
     */
1323
    protected function setDate(?int $userData = null)
1324
    {
1325
        $currentDate = $userData ?? Time::now()->getTimestamp();
100✔
1326

1327
        return $this->intToDate($currentDate);
100✔
1328
    }
1329

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

1353
            case 'datetime':
65✔
1354
                return date('Y-m-d H:i:s', $value);
63✔
1355

1356
            case 'date':
2✔
1357
                return date('Y-m-d', $value);
1✔
1358

1359
            default:
1360
                throw ModelException::forNoDateFormat(static::class);
1✔
1361
        }
1362
    }
1363

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

1382
            case 'date':
2✔
1383
                return $value->format('Y-m-d');
1✔
1384

1385
            case 'int':
1✔
1386
                return $value->getTimestamp();
1✔
1387

1388
            default:
1389
                return (string) $value;
×
1390
        }
1391
    }
1392

1393
    /**
1394
     * Set the value of the skipValidation flag.
1395
     *
1396
     * @param bool $skip Value
1397
     *
1398
     * @return $this
1399
     */
1400
    public function skipValidation(bool $skip = true)
1401
    {
1402
        $this->skipValidation = $skip;
2✔
1403

1404
        return $this;
2✔
1405
    }
1406

1407
    /**
1408
     * Allows to set (and reset) validation messages.
1409
     * It could be used when you have to change default or override current validate messages.
1410
     *
1411
     * @param array $validationMessages Value
1412
     *
1413
     * @return $this
1414
     */
1415
    public function setValidationMessages(array $validationMessages)
1416
    {
1417
        $this->validationMessages = $validationMessages;
×
1418

1419
        return $this;
×
1420
    }
1421

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

1435
        return $this;
2✔
1436
    }
1437

1438
    /**
1439
     * Allows to set (and reset) validation rules.
1440
     * It could be used when you have to change default or override current validate rules.
1441
     *
1442
     * @param array $validationRules Value
1443
     *
1444
     * @return $this
1445
     */
1446
    public function setValidationRules(array $validationRules)
1447
    {
1448
        $this->validationRules = $validationRules;
2✔
1449

1450
        return $this;
2✔
1451
    }
1452

1453
    /**
1454
     * Allows to set field wise validation rules.
1455
     * It could be used when you have to change default or override current validate rules.
1456
     *
1457
     * @param string       $field      Field Name
1458
     * @param array|string $fieldRules Validation rules
1459
     *
1460
     * @return $this
1461
     */
1462
    public function setValidationRule(string $field, $fieldRules)
1463
    {
1464
        $rules = $this->validationRules;
2✔
1465

1466
        // ValidationRules can be either a string, which is the group name,
1467
        // or an array of rules.
1468
        if (is_string($rules)) {
2✔
1469
            $this->ensureValidation();
1✔
1470

1471
            [$rules, $customErrors] = $this->validation->loadRuleGroup($rules);
1✔
1472

1473
            $this->validationRules = $rules;
1✔
1474
            $this->validationMessages += $customErrors;
1✔
1475
        }
1476

1477
        $this->validationRules[$field] = $fieldRules;
2✔
1478

1479
        return $this;
2✔
1480
    }
1481

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

1494
        return $this;
2✔
1495
    }
1496

1497
    /**
1498
     * Validate the row data against the validation rules (or the validation group)
1499
     * specified in the class property, $validationRules.
1500
     *
1501
     * @param array|object $row Row data
1502
     * @phpstan-param row_array|object $row
1503
     */
1504
    public function validate($row): bool
1505
    {
1506
        if ($this->skipValidation) {
118✔
UNCOV
1507
            return true;
×
1508
        }
1509

1510
        $rules = $this->getValidationRules();
118✔
1511

1512
        if ($rules === []) {
118✔
1513
            return true;
72✔
1514
        }
1515

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

1521
        if ($row === []) {
46✔
NEW
1522
            return true;
×
1523
        }
1524

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

1527
        // If no data existed that needs validation
1528
        // our job is done here.
1529
        if ($rules === []) {
46✔
1530
            return true;
2✔
1531
        }
1532

1533
        $this->ensureValidation();
44✔
1534

1535
        $this->validation->reset()->setRules($rules, $this->validationMessages);
44✔
1536

1537
        return $this->validation->run($row, null, $this->DBGroup);
44✔
1538
    }
1539

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

1550
        // ValidationRules can be either a string, which is the group name,
1551
        // or an array of rules.
1552
        if (is_string($rules)) {
120✔
1553
            $this->ensureValidation();
13✔
1554

1555
            [$rules, $customErrors] = $this->validation->loadRuleGroup($rules);
13✔
1556

1557
            $this->validationMessages += $customErrors;
13✔
1558
        }
1559

1560
        if (isset($options['except'])) {
120✔
1561
            $rules = array_diff_key($rules, array_flip($options['except']));
×
1562
        } elseif (isset($options['only'])) {
120✔
1563
            $rules = array_intersect_key($rules, array_flip($options['only']));
×
1564
        }
1565

1566
        return $rules;
120✔
1567
    }
1568

1569
    protected function ensureValidation(): void
1570
    {
1571
        if ($this->validation === null) {
45✔
1572
            $this->validation = Services::validation(null, false);
29✔
1573
        }
1574
    }
1575

1576
    /**
1577
     * Returns the model's validation messages, so they
1578
     * can be used elsewhere, if needed.
1579
     */
1580
    public function getValidationMessages(): array
1581
    {
1582
        return $this->validationMessages;
2✔
1583
    }
1584

1585
    /**
1586
     * Removes any rules that apply to fields that have not been set
1587
     * currently so that rules don't block updating when only updating
1588
     * a partial row.
1589
     *
1590
     * @param array $rules Array containing field name and rule
1591
     * @param array $row   Row data (@TODO Remove null in param type)
1592
     * @phpstan-param row_array $row
1593
     */
1594
    protected function cleanValidationRules(array $rules, ?array $row = null): array
1595
    {
1596
        if ($row === null || $row === []) {
21✔
1597
            return [];
2✔
1598
        }
1599

1600
        foreach (array_keys($rules) as $field) {
19✔
1601
            if (! array_key_exists($field, $row)) {
19✔
1602
                unset($rules[$field]);
8✔
1603
            }
1604
        }
1605

1606
        return $rules;
19✔
1607
    }
1608

1609
    /**
1610
     * Sets $tempAllowCallbacks value so that we can temporarily override
1611
     * the setting. Resets after the next method that uses triggers.
1612
     *
1613
     * @param bool $val value
1614
     *
1615
     * @return $this
1616
     */
1617
    public function allowCallbacks(bool $val = true)
1618
    {
1619
        $this->tempAllowCallbacks = $val;
3✔
1620

1621
        return $this;
3✔
1622
    }
1623

1624
    /**
1625
     * A simple event trigger for Model Events that allows additional
1626
     * data manipulation within the model. Specifically intended for
1627
     * usage by child models this can be used to format data,
1628
     * save/load related classes, etc.
1629
     *
1630
     * It is the responsibility of the callback methods to return
1631
     * the data itself.
1632
     *
1633
     * Each $eventData array MUST have a 'data' key with the relevant
1634
     * data for callback methods (like an array of key/value pairs to insert
1635
     * or update, an array of results, etc.)
1636
     *
1637
     * If callbacks are not allowed then returns $eventData immediately.
1638
     *
1639
     * @param string $event     Event
1640
     * @param array  $eventData Event Data
1641
     *
1642
     * @return array
1643
     *
1644
     * @throws DataException
1645
     */
1646
    protected function trigger(string $event, array $eventData)
1647
    {
1648
        // Ensure it's a valid event
1649
        if (! isset($this->{$event}) || $this->{$event} === []) {
157✔
1650
            return $eventData;
142✔
1651
        }
1652

1653
        foreach ($this->{$event} as $callback) {
15✔
1654
            if (! method_exists($this, $callback)) {
15✔
1655
                throw DataException::forInvalidMethodTriggered($callback);
1✔
1656
            }
1657

1658
            $eventData = $this->{$callback}($eventData);
14✔
1659
        }
1660

1661
        return $eventData;
14✔
1662
    }
1663

1664
    /**
1665
     * Sets the return type of the results to be as an associative array.
1666
     *
1667
     * @return $this
1668
     */
1669
    public function asArray()
1670
    {
1671
        $this->tempReturnType = 'array';
3✔
1672

1673
        return $this;
3✔
1674
    }
1675

1676
    /**
1677
     * Sets the return type to be of the specified type of object.
1678
     * Defaults to a simple object, but can be any class that has
1679
     * class vars with the same name as the collection columns,
1680
     * or at least allows them to be created.
1681
     *
1682
     * @param string $class Class Name
1683
     *
1684
     * @return $this
1685
     */
1686
    public function asObject(string $class = 'object')
1687
    {
1688
        $this->tempReturnType = $class;
1✔
1689

1690
        return $this;
1✔
1691
    }
1692

1693
    /**
1694
     * Takes a class and returns an array of its public and protected
1695
     * properties as an array suitable for use in creates and updates.
1696
     * This method uses objectToRawArray() internally and does conversion
1697
     * to string on all Time instances
1698
     *
1699
     * @param object $object      Object
1700
     * @param bool   $onlyChanged Only Changed Property
1701
     * @param bool   $recursive   If true, inner entities will be cast as array as well
1702
     *
1703
     * @return array<string, mixed>
1704
     *
1705
     * @throws ReflectionException
1706
     */
1707
    protected function objectToArray($object, bool $onlyChanged = true, bool $recursive = false): array
1708
    {
1709
        $properties = $this->objectToRawArray($object, $onlyChanged, $recursive);
20✔
1710

1711
        // Convert any Time instances to appropriate $dateFormat
1712
        return $this->timeToString($properties);
20✔
1713
    }
1714

1715
    /**
1716
     * Convert any Time instances to appropriate $dateFormat.
1717
     *
1718
     * @param array<string, mixed> $properties
1719
     *
1720
     * @return array<string, mixed>
1721
     */
1722
    protected function timeToString(array $properties): array
1723
    {
1724
        if ($properties === []) {
20✔
1725
            return [];
×
1726
        }
1727

1728
        return array_map(function ($value) {
20✔
1729
            if ($value instanceof Time) {
20✔
1730
                return $this->timeToDate($value);
5✔
1731
            }
1732

1733
            return $value;
20✔
1734
        }, $properties);
20✔
1735
    }
1736

1737
    /**
1738
     * Takes a class and returns an array of its public and protected
1739
     * properties as an array with raw values.
1740
     *
1741
     * @param object $object      Object
1742
     * @param bool   $onlyChanged Only Changed Property
1743
     * @param bool   $recursive   If true, inner entities will be casted as array as well
1744
     *
1745
     * @return array<string, mixed> Array with raw values.
1746
     *
1747
     * @throws ReflectionException
1748
     */
1749
    protected function objectToRawArray($object, bool $onlyChanged = true, bool $recursive = false): array
1750
    {
1751
        // Entity::toRawArray() returns array.
1752
        if (method_exists($object, 'toRawArray')) {
20✔
1753
            $properties = $object->toRawArray($onlyChanged, $recursive);
18✔
1754
        } else {
1755
            $mirror = new ReflectionClass($object);
2✔
1756
            $props  = $mirror->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED);
2✔
1757

1758
            $properties = [];
2✔
1759

1760
            // Loop over each property,
1761
            // saving the name/value in a new array we can return.
1762
            foreach ($props as $prop) {
2✔
1763
                // Must make protected values accessible.
1764
                $prop->setAccessible(true);
2✔
1765
                $properties[$prop->getName()] = $prop->getValue($object);
2✔
1766
            }
1767
        }
1768

1769
        return $properties;
20✔
1770
    }
1771

1772
    /**
1773
     * Transform data to array.
1774
     *
1775
     * @param array|object|null $row Row data
1776
     * @phpstan-param row_array|object|null $row
1777
     * @param string $type Type of data (insert|update)
1778
     *
1779
     * @throws DataException
1780
     * @throws InvalidArgumentException
1781
     * @throws ReflectionException
1782
     */
1783
    protected function transformDataToArray($row, string $type): array
1784
    {
1785
        if (! in_array($type, ['insert', 'update'], true)) {
95✔
1786
            throw new InvalidArgumentException(sprintf('Invalid type "%s" used upon transforming data to array.', $type));
1✔
1787
        }
1788

1789
        if (! $this->allowEmptyInserts && ($row === null || (array) $row === [])) {
94✔
1790
            throw DataException::forEmptyDataset($type);
6✔
1791
        }
1792

1793
        // If $row is using a custom class with public or protected
1794
        // properties representing the collection elements, we need to grab
1795
        // them as an array.
1796
        if (is_object($row) && ! $row instanceof stdClass) {
91✔
1797
            // If it validates with entire rules, all fields are needed.
1798
            $onlyChanged = ($this->skipValidation === false && $this->cleanValidationRules === false)
18✔
1799
                ? false : ($type === 'update');
18✔
1800

1801
            $row = $this->objectToArray($row, $onlyChanged, true);
18✔
1802
        }
1803

1804
        // If it's still a stdClass, go ahead and convert to
1805
        // an array so doProtectFields and other model methods
1806
        // don't have to do special checks.
1807
        if (is_object($row)) {
91✔
1808
            $row = (array) $row;
11✔
1809
        }
1810

1811
        // If it's still empty here, means $row is no change or is empty object
1812
        if (! $this->allowEmptyInserts && ($row === null || $row === [])) {
91✔
UNCOV
1813
            throw DataException::forEmptyDataset($type);
×
1814
        }
1815

1816
        return $row;
91✔
1817
    }
1818

1819
    /**
1820
     * Provides the db connection and model's properties.
1821
     *
1822
     * @param string $name Name
1823
     *
1824
     * @return array|bool|float|int|object|string|null
1825
     */
1826
    public function __get(string $name)
1827
    {
1828
        if (property_exists($this, $name)) {
45✔
1829
            return $this->{$name};
45✔
1830
        }
1831

1832
        return $this->db->{$name} ?? null;
1✔
1833
    }
1834

1835
    /**
1836
     * Checks for the existence of properties across this model, and db connection.
1837
     *
1838
     * @param string $name Name
1839
     */
1840
    public function __isset(string $name): bool
1841
    {
1842
        if (property_exists($this, $name)) {
45✔
1843
            return true;
45✔
1844
        }
1845

1846
        return isset($this->db->{$name});
1✔
1847
    }
1848

1849
    /**
1850
     * Provides direct access to method in the database connection.
1851
     *
1852
     * @param string $name   Name
1853
     * @param array  $params Params
1854
     *
1855
     * @return $this|null
1856
     */
1857
    public function __call(string $name, array $params)
1858
    {
1859
        if (method_exists($this->db, $name)) {
×
1860
            return $this->db->{$name}(...$params);
×
1861
        }
1862

1863
        return null;
×
1864
    }
1865

1866
    /**
1867
     * Sets $allowEmptyInserts.
1868
     */
1869
    public function allowEmptyInserts(bool $value = true): self
1870
    {
1871
        $this->allowEmptyInserts = $value;
1✔
1872

1873
        return $this;
1✔
1874
    }
1875
}
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