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

codeigniter4 / CodeIgniter4 / 7345642681

28 Dec 2023 07:58AM UTC coverage: 85.244% (+0.007%) from 85.237%
7345642681

push

github

web-flow
Merge pull request #8345 from kenjis/replace-empty

refactor: replace empty() Part 1

90 of 93 new or added lines in 19 files covered. (96.77%)

1 existing line in 1 file now uncovered.

18607 of 21828 relevant lines covered (85.24%)

199.79 hits per line

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

95.47
/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;
280✔
335
        $this->tempUseSoftDeletes = $this->useSoftDeletes;
280✔
336
        $this->tempAllowCallbacks = $this->allowCallbacks;
280✔
337

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

344
        $this->initialize();
280✔
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
    }
279✔
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 (isset($eventData['returnData']) && $eventData['returnData'] === true) {
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 (isset($eventData['returnData']) && $eventData['returnData'] === true) {
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 (isset($eventData['returnData']) && $eventData['returnData'] === true) {
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 ((array) $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
        $id = $this->getIdValue($row);
21✔
733

734
        return ! ($id === null || $id === []);
21✔
735
    }
736

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

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

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

768
        $row = $this->transformDataToArray($row, 'insert');
79✔
769

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

775
            return false;
19✔
776
        }
777

778
        // Restore $cleanValidationRules
779
        $this->cleanValidationRules = $cleanValidationRules;
60✔
780

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

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

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

796
        $eventData = ['data' => $row];
57✔
797

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

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

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

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

815
        $this->tempAllowCallbacks = $this->allowCallbacks;
55✔
816

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

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

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

838
        return $row;
67✔
839
    }
840

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

853
        return $row;
81✔
854
    }
855

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

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

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

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

896
                    return false;
3✔
897
                }
898

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

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

910
        // Restore $cleanValidationRules
911
        $this->cleanValidationRules = $cleanValidationRules;
10✔
912

913
        $eventData = ['data' => $set];
10✔
914

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

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

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

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

931
        $this->tempAllowCallbacks = $this->allowCallbacks;
10✔
932

933
        return $result;
10✔
934
    }
935

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

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

956
        $row = $this->transformDataToArray($row, 'update');
33✔
957

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

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

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

973
        $row = $this->setUpdatedField($row, $this->setDate());
25✔
974

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

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

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

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

994
        $this->tempAllowCallbacks = $this->allowCallbacks;
24✔
995

996
        return $eventData['result'];
24✔
997
    }
998

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

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

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

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

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

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

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

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

1061
        $eventData = ['data' => $set];
3✔
1062

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

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

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

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

1079
        $this->tempAllowCallbacks = $this->allowCallbacks;
3✔
1080

1081
        return $result;
3✔
1082
    }
1083

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

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

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

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

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

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

1124
        $this->tempAllowCallbacks = $this->allowCallbacks;
27✔
1125

1126
        return $eventData['result'];
27✔
1127
    }
1128

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

1141
        return $this->doPurgeDeleted();
1✔
1142
    }
1143

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

1156
        return $this;
23✔
1157
    }
1158

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

1170
        return $this;
1✔
1171
    }
1172

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

1189
        $row = $this->setUpdatedField((array) $row, $this->setDate());
2✔
1190

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

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

1213
        return $this->doErrors();
2✔
1214
    }
1215

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

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

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

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

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

1257
        return $this;
10✔
1258
    }
1259

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

1272
        return $this;
12✔
1273
    }
1274

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

1293
        if ($this->allowedFields === []) {
28✔
1294
            throw DataException::forInvalidAllowedFields(static::class);
×
1295
        }
1296

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

1303
        return $row;
28✔
1304
    }
1305

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

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

1336
        return $this->intToDate($currentDate);
100✔
1337
    }
1338

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

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

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

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

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

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

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

1397
            default:
1398
                return (string) $value;
×
1399
        }
1400
    }
1401

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

1413
        return $this;
2✔
1414
    }
1415

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

1428
        return $this;
×
1429
    }
1430

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

1444
        return $this;
2✔
1445
    }
1446

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

1459
        return $this;
2✔
1460
    }
1461

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

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

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

1484
        $this->validationRules[$field] = $fieldRules;
2✔
1485

1486
        return $this;
2✔
1487
    }
1488

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

1501
        return $this;
2✔
1502
    }
1503

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

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

1520
        if ($this->skipValidation || $rules === [] || $row === []) {
118✔
1521
            return true;
72✔
1522
        }
1523

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

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

1532
        $this->validation->reset()->setRules($rules, $this->validationMessages);
44✔
1533

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

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

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

1552
            $this->validationMessages += $customErrors;
13✔
1553
        }
1554

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

1561
        return $rules;
120✔
1562
    }
1563

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

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

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

1594
        return $rules;
19✔
1595
    }
1596

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

1609
        return $this;
3✔
1610
    }
1611

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

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

1646
            $eventData = $this->{$callback}($eventData);
14✔
1647
        }
1648

1649
        return $eventData;
14✔
1650
    }
1651

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

1661
        return $this;
3✔
1662
    }
1663

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

1678
        return $this;
1✔
1679
    }
1680

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

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

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

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

1721
            return $value;
20✔
1722
        }, $properties);
20✔
1723
    }
1724

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

1746
            $properties = [];
2✔
1747

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

1757
        return $properties;
20✔
1758
    }
1759

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

1777
        if (! $this->allowEmptyInserts && ($row === null || (array) $row === [])) {
94✔
1778
            throw DataException::forEmptyDataset($type);
6✔
1779
        }
1780

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

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

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

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

1804
        return $row;
91✔
1805
    }
1806

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

1820
        return $this->db->{$name} ?? null;
1✔
1821
    }
1822

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

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

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

1851
        return null;
×
1852
    }
1853

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

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

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

1894
                        $row = strtr($row, $replacements);
1895
                    }
1896

1897
                    continue;
1898
                }
1899

1900
                $rule = strtr($rule, $replacements);
1901
            }
1902
        }
1903

1904
        return $rules;
1905
    }
1906

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

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