ci4

Форк
0
/
Model.php 
951 строка · 30.2 Кб
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 BadMethodCallException;
17
use Closure;
18
use CodeIgniter\Database\BaseBuilder;
19
use CodeIgniter\Database\BaseConnection;
20
use CodeIgniter\Database\BaseResult;
21
use CodeIgniter\Database\ConnectionInterface;
22
use CodeIgniter\Database\Exceptions\DatabaseException;
23
use CodeIgniter\Database\Exceptions\DataException;
24
use CodeIgniter\Database\Query;
25
use CodeIgniter\Entity\Entity;
26
use CodeIgniter\Exceptions\ModelException;
27
use CodeIgniter\Validation\ValidationInterface;
28
use Config\Database;
29
use Config\Feature;
30
use ReflectionException;
31
use stdClass;
32

33
/**
34
 * The Model class extends BaseModel and provides additional
35
 * convenient features that makes working with a SQL database
36
 * table less painful.
37
 *
38
 * It will:
39
 *      - automatically connect to database
40
 *      - allow intermingling calls to the builder
41
 *      - removes the need to use Result object directly in most cases
42
 *
43
 * @property-read BaseConnection $db
44
 *
45
 * @method $this groupBy($by, ?bool $escape = null)
46
 * @method $this groupEnd()
47
 * @method $this groupStart()
48
 * @method $this having($key, $value = null, ?bool $escape = null)
49
 * @method $this havingGroupEnd()
50
 * @method $this havingGroupStart()
51
 * @method $this havingIn(?string $key = null, $values = null, ?bool $escape = null)
52
 * @method $this havingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
53
 * @method $this havingNotIn(?string $key = null, $values = null, ?bool $escape = null)
54
 * @method $this join(string $table, string $cond, string $type = '', ?bool $escape = null)
55
 * @method $this like($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
56
 * @method $this limit(?int $value = null, ?int $offset = 0)
57
 * @method $this notGroupStart()
58
 * @method $this notHavingGroupStart()
59
 * @method $this notHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
60
 * @method $this notLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
61
 * @method $this offset(int $offset)
62
 * @method $this orderBy(string $orderBy, string $direction = '', ?bool $escape = null)
63
 * @method $this orGroupStart()
64
 * @method $this orHaving($key, $value = null, ?bool $escape = null)
65
 * @method $this orHavingGroupStart()
66
 * @method $this orHavingIn(?string $key = null, $values = null, ?bool $escape = null)
67
 * @method $this orHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
68
 * @method $this orHavingNotIn(?string $key = null, $values = null, ?bool $escape = null)
69
 * @method $this orLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
70
 * @method $this orNotGroupStart()
71
 * @method $this orNotHavingGroupStart()
72
 * @method $this orNotHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
73
 * @method $this orNotLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
74
 * @method $this orWhere($key, $value = null, ?bool $escape = null)
75
 * @method $this orWhereIn(?string $key = null, $values = null, ?bool $escape = null)
76
 * @method $this orWhereNotIn(?string $key = null, $values = null, ?bool $escape = null)
77
 * @method $this select($select = '*', ?bool $escape = null)
78
 * @method $this selectAvg(string $select = '', string $alias = '')
79
 * @method $this selectCount(string $select = '', string $alias = '')
80
 * @method $this selectMax(string $select = '', string $alias = '')
81
 * @method $this selectMin(string $select = '', string $alias = '')
82
 * @method $this selectSum(string $select = '', string $alias = '')
83
 * @method $this when($condition, callable $callback, ?callable $defaultCallback = null)
84
 * @method $this whenNot($condition, callable $callback, ?callable $defaultCallback = null)
85
 * @method $this where($key, $value = null, ?bool $escape = null)
86
 * @method $this whereIn(?string $key = null, $values = null, ?bool $escape = null)
87
 * @method $this whereNotIn(?string $key = null, $values = null, ?bool $escape = null)
88
 *
89
 * @phpstan-import-type row_array from BaseModel
90
 */
91
class Model extends BaseModel
92
{
93
    /**
94
     * Name of database table
95
     *
96
     * @var string
97
     */
98
    protected $table;
99

100
    /**
101
     * The table's primary key.
102
     *
103
     * @var string
104
     */
105
    protected $primaryKey = 'id';
106

107
    /**
108
     * Whether primary key uses auto increment.
109
     *
110
     * @var bool
111
     */
112
    protected $useAutoIncrement = true;
113

114
    /**
115
     * Query Builder object
116
     *
117
     * @var BaseBuilder|null
118
     */
119
    protected $builder;
120

121
    /**
122
     * Holds information passed in via 'set'
123
     * so that we can capture it (not the builder)
124
     * and ensure it gets validated first.
125
     *
126
     * @var         array{escape: array, data: array}|array{}
127
     * @phpstan-var array{escape: array<int|string, bool|null>, data: row_array}|array{}
128
     */
129
    protected $tempData = [];
130

131
    /**
132
     * Escape array that maps usage of escape
133
     * flag for every parameter.
134
     *
135
     * @var array
136
     */
137
    protected $escape = [];
138

139
    /**
140
     * Builder method names that should not be used in the Model.
141
     *
142
     * @var list<string> method name
143
     */
144
    private array $builderMethodsNotAvailable = [
145
        'getCompiledInsert',
146
        'getCompiledSelect',
147
        'getCompiledUpdate',
148
    ];
149

150
    public function __construct(?ConnectionInterface $db = null, ?ValidationInterface $validation = null)
151
    {
152
        /**
153
         * @var BaseConnection|null $db
154
         */
155
        $db ??= Database::connect($this->DBGroup);
156

157
        $this->db = $db;
158

159
        parent::__construct($validation);
160
    }
161

162
    /**
163
     * Specify the table associated with a model
164
     *
165
     * @param string $table Table
166
     *
167
     * @return $this
168
     */
169
    public function setTable(string $table)
170
    {
171
        $this->table = $table;
172

173
        return $this;
174
    }
175

176
    /**
177
     * Fetches the row(s) of database from $this->table with a primary key
178
     * matching $id.
179
     * This method works only with dbCalls.
180
     *
181
     * @param bool                  $singleton Single or multiple results
182
     * @param array|int|string|null $id        One primary key or an array of primary keys
183
     *
184
     * @return         array|object|null                                                     The resulting row of data, or null.
185
     * @phpstan-return ($singleton is true ? row_array|null|object : list<row_array|object>)
186
     */
187
    protected function doFind(bool $singleton, $id = null)
188
    {
189
        $builder = $this->builder();
190

191
        $useCast = $this->useCasts();
192
        if ($useCast) {
193
            $returnType = $this->tempReturnType;
194
            $this->asArray();
195
        }
196

197
        if ($this->tempUseSoftDeletes) {
198
            $builder->where($this->table . '.' . $this->deletedField, null);
199
        }
200

201
        $row  = null;
202
        $rows = [];
203

204
        if (is_array($id)) {
205
            $rows = $builder->whereIn($this->table . '.' . $this->primaryKey, $id)
206
                ->get()
207
                ->getResult($this->tempReturnType);
208
        } elseif ($singleton) {
209
            $row = $builder->where($this->table . '.' . $this->primaryKey, $id)
210
                ->get()
211
                ->getFirstRow($this->tempReturnType);
212
        } else {
213
            $rows = $builder->get()->getResult($this->tempReturnType);
214
        }
215

216
        if ($useCast) {
217
            $this->tempReturnType = $returnType;
218

219
            if ($singleton) {
220
                if ($row === null) {
221
                    return null;
222
                }
223

224
                return $this->convertToReturnType($row, $returnType);
225
            }
226

227
            foreach ($rows as $i => $row) {
228
                $rows[$i] = $this->convertToReturnType($row, $returnType);
229
            }
230

231
            return $rows;
232
        }
233

234
        if ($singleton) {
235
            return $row;
236
        }
237

238
        return $rows;
239
    }
240

241
    /**
242
     * Fetches the column of database from $this->table.
243
     * This method works only with dbCalls.
244
     *
245
     * @param string $columnName Column Name
246
     *
247
     * @return         array|null           The resulting row of data, or null if no data found.
248
     * @phpstan-return list<row_array>|null
249
     */
250
    protected function doFindColumn(string $columnName)
251
    {
252
        return $this->select($columnName)->asArray()->find();
253
    }
254

255
    /**
256
     * Works with the current Query Builder instance to return
257
     * all results, while optionally limiting them.
258
     * This method works only with dbCalls.
259
     *
260
     * @param int|null $limit  Limit
261
     * @param int      $offset Offset
262
     *
263
     * @return         array
264
     * @phpstan-return list<row_array|object>
265
     */
266
    protected function doFindAll(?int $limit = null, int $offset = 0)
267
    {
268
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
269
        if ($limitZeroAsAll) {
270
            $limit ??= 0;
271
        }
272

273
        $builder = $this->builder();
274

275
        $useCast = $this->useCasts();
276
        if ($useCast) {
277
            $returnType = $this->tempReturnType;
278
            $this->asArray();
279
        }
280

281
        if ($this->tempUseSoftDeletes) {
282
            $builder->where($this->table . '.' . $this->deletedField, null);
283
        }
284

285
        $results = $builder->limit($limit, $offset)
286
            ->get()
287
            ->getResult($this->tempReturnType);
288

289
        if ($useCast) {
290
            foreach ($results as $i => $row) {
291
                $results[$i] = $this->convertToReturnType($row, $returnType);
292
            }
293

294
            $this->tempReturnType = $returnType;
295
        }
296

297
        return $results;
298
    }
299

300
    /**
301
     * Returns the first row of the result set. Will take any previous
302
     * Query Builder calls into account when determining the result set.
303
     * This method works only with dbCalls.
304
     *
305
     * @return         array|object|null
306
     * @phpstan-return row_array|object|null
307
     */
308
    protected function doFirst()
309
    {
310
        $builder = $this->builder();
311

312
        $useCast = $this->useCasts();
313
        if ($useCast) {
314
            $returnType = $this->tempReturnType;
315
            $this->asArray();
316
        }
317

318
        if ($this->tempUseSoftDeletes) {
319
            $builder->where($this->table . '.' . $this->deletedField, null);
320
        } elseif ($this->useSoftDeletes && ($builder->QBGroupBy === []) && $this->primaryKey) {
321
            $builder->groupBy($this->table . '.' . $this->primaryKey);
322
        }
323

324
        // Some databases, like PostgreSQL, need order
325
        // information to consistently return correct results.
326
        if ($builder->QBGroupBy && ($builder->QBOrderBy === []) && $this->primaryKey) {
327
            $builder->orderBy($this->table . '.' . $this->primaryKey, 'asc');
328
        }
329

330
        $row = $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType);
331

332
        if ($useCast && $row !== null) {
333
            $row = $this->convertToReturnType($row, $returnType);
334

335
            $this->tempReturnType = $returnType;
336
        }
337

338
        return $row;
339
    }
340

341
    /**
342
     * Inserts data into the current table.
343
     * This method works only with dbCalls.
344
     *
345
     * @param         array     $row Row data
346
     * @phpstan-param row_array $row
347
     *
348
     * @return bool
349
     */
350
    protected function doInsert(array $row)
351
    {
352
        $escape       = $this->escape;
353
        $this->escape = [];
354

355
        // Require non-empty primaryKey when
356
        // not using auto-increment feature
357
        if (! $this->useAutoIncrement && ! isset($row[$this->primaryKey])) {
358
            throw DataException::forEmptyPrimaryKey('insert');
359
        }
360

361
        $builder = $this->builder();
362

363
        // Must use the set() method to ensure to set the correct escape flag
364
        foreach ($row as $key => $val) {
365
            $builder->set($key, $val, $escape[$key] ?? null);
366
        }
367

368
        if ($this->allowEmptyInserts && $row === []) {
369
            $table = $this->db->protectIdentifiers($this->table, true, null, false);
370
            if ($this->db->getPlatform() === 'MySQLi') {
371
                $sql = 'INSERT INTO ' . $table . ' VALUES ()';
372
            } elseif ($this->db->getPlatform() === 'OCI8') {
373
                $allFields = $this->db->protectIdentifiers(
374
                    array_map(
375
                        static fn ($row) => $row->name,
376
                        $this->db->getFieldData($this->table)
377
                    ),
378
                    false,
379
                    true
380
                );
381

382
                $sql = sprintf(
383
                    'INSERT INTO %s (%s) VALUES (%s)',
384
                    $table,
385
                    implode(',', $allFields),
386
                    substr(str_repeat(',DEFAULT', count($allFields)), 1)
387
                );
388
            } else {
389
                $sql = 'INSERT INTO ' . $table . ' DEFAULT VALUES';
390
            }
391

392
            $result = $this->db->query($sql);
393
        } else {
394
            $result = $builder->insert();
395
        }
396

397
        // If insertion succeeded then save the insert ID
398
        if ($result) {
399
            $this->insertID = ! $this->useAutoIncrement ? $row[$this->primaryKey] : $this->db->insertID();
400
        }
401

402
        return $result;
403
    }
404

405
    /**
406
     * Compiles batch insert strings and runs the queries, validating each row prior.
407
     * This method works only with dbCalls.
408
     *
409
     * @param array|null $set       An associative array of insert values
410
     * @param bool|null  $escape    Whether to escape values
411
     * @param int        $batchSize The size of the batch to run
412
     * @param bool       $testing   True means only number of records is returned, false will execute the query
413
     *
414
     * @return bool|int Number of rows inserted or FALSE on failure
415
     */
416
    protected function doInsertBatch(?array $set = null, ?bool $escape = null, int $batchSize = 100, bool $testing = false)
417
    {
418
        if (is_array($set)) {
419
            foreach ($set as $row) {
420
                // Require non-empty primaryKey when
421
                // not using auto-increment feature
422
                if (! $this->useAutoIncrement && ! isset($row[$this->primaryKey])) {
423
                    throw DataException::forEmptyPrimaryKey('insertBatch');
424
                }
425
            }
426
        }
427

428
        return $this->builder()->testMode($testing)->insertBatch($set, $escape, $batchSize);
429
    }
430

431
    /**
432
     * Updates a single record in $this->table.
433
     * This method works only with dbCalls.
434
     *
435
     * @param         array|int|string|null $id
436
     * @param         array|null            $row Row data
437
     * @phpstan-param row_array|null        $row
438
     */
439
    protected function doUpdate($id = null, $row = null): bool
440
    {
441
        $escape       = $this->escape;
442
        $this->escape = [];
443

444
        $builder = $this->builder();
445

446
        if ($id) {
447
            $builder = $builder->whereIn($this->table . '.' . $this->primaryKey, $id);
448
        }
449

450
        // Must use the set() method to ensure to set the correct escape flag
451
        foreach ($row as $key => $val) {
452
            $builder->set($key, $val, $escape[$key] ?? null);
453
        }
454

455
        if ($builder->getCompiledQBWhere() === []) {
456
            throw new DatabaseException(
457
                'Updates are not allowed unless they contain a "where" or "like" clause.'
458
            );
459
        }
460

461
        return $builder->update();
462
    }
463

464
    /**
465
     * Compiles an update string and runs the query
466
     * This method works only with dbCalls.
467
     *
468
     * @param array|null  $set       An associative array of update values
469
     * @param string|null $index     The where key
470
     * @param int         $batchSize The size of the batch to run
471
     * @param bool        $returnSQL True means SQL is returned, false will execute the query
472
     *
473
     * @return false|int|list<string> Number of rows affected or FALSE on failure, SQL array when testMode
474
     *
475
     * @throws DatabaseException
476
     */
477
    protected function doUpdateBatch(?array $set = null, ?string $index = null, int $batchSize = 100, bool $returnSQL = false)
478
    {
479
        return $this->builder()->testMode($returnSQL)->updateBatch($set, $index, $batchSize);
480
    }
481

482
    /**
483
     * Deletes a single record from $this->table where $id matches
484
     * the table's primaryKey
485
     * This method works only with dbCalls.
486
     *
487
     * @param array|int|string|null $id    The rows primary key(s)
488
     * @param bool                  $purge Allows overriding the soft deletes setting.
489
     *
490
     * @return bool|string SQL string when testMode
491
     *
492
     * @throws DatabaseException
493
     */
494
    protected function doDelete($id = null, bool $purge = false)
495
    {
496
        $set     = [];
497
        $builder = $this->builder();
498

499
        if ($id) {
500
            $builder = $builder->whereIn($this->primaryKey, $id);
501
        }
502

503
        if ($this->useSoftDeletes && ! $purge) {
504
            if ($builder->getCompiledQBWhere() === []) {
505
                throw new DatabaseException(
506
                    'Deletes are not allowed unless they contain a "where" or "like" clause.'
507
                );
508
            }
509

510
            $builder->where($this->deletedField);
511

512
            $set[$this->deletedField] = $this->setDate();
513

514
            if ($this->useTimestamps && $this->updatedField !== '') {
515
                $set[$this->updatedField] = $this->setDate();
516
            }
517

518
            return $builder->update($set);
519
        }
520

521
        return $builder->delete();
522
    }
523

524
    /**
525
     * Permanently deletes all rows that have been marked as deleted
526
     * through soft deletes (deleted = 1)
527
     * This method works only with dbCalls.
528
     *
529
     * @return bool|string Returns a SQL string if in test mode.
530
     */
531
    protected function doPurgeDeleted()
532
    {
533
        return $this->builder()
534
            ->where($this->table . '.' . $this->deletedField . ' IS NOT NULL')
535
            ->delete();
536
    }
537

538
    /**
539
     * Works with the find* methods to return only the rows that
540
     * have been deleted.
541
     * This method works only with dbCalls.
542
     *
543
     * @return void
544
     */
545
    protected function doOnlyDeleted()
546
    {
547
        $this->builder()->where($this->table . '.' . $this->deletedField . ' IS NOT NULL');
548
    }
549

550
    /**
551
     * Compiles a replace into string and runs the query
552
     * This method works only with dbCalls.
553
     *
554
     * @param         array|null     $row       Data
555
     * @phpstan-param row_array|null $row
556
     * @param         bool           $returnSQL Set to true to return Query String
557
     *
558
     * @return BaseResult|false|Query|string
559
     */
560
    protected function doReplace(?array $row = null, bool $returnSQL = false)
561
    {
562
        return $this->builder()->testMode($returnSQL)->replace($row);
563
    }
564

565
    /**
566
     * Grabs the last error(s) that occurred from the Database connection.
567
     * The return array should be in the following format:
568
     *  ['source' => 'message']
569
     * This method works only with dbCalls.
570
     *
571
     * @return array<string, string>
572
     */
573
    protected function doErrors()
574
    {
575
        // $error is always ['code' => string|int, 'message' => string]
576
        $error = $this->db->error();
577

578
        if ((int) $error['code'] === 0) {
579
            return [];
580
        }
581

582
        return [$this->db::class => $error['message']];
583
    }
584

585
    /**
586
     * Returns the id value for the data array or object
587
     *
588
     * @param         array|object     $row Row data
589
     * @phpstan-param row_array|object $row
590
     *
591
     * @return array|int|string|null
592
     */
593
    public function getIdValue($row)
594
    {
595
        if (is_object($row) && isset($row->{$this->primaryKey})) {
596
            // Get the raw primary key value of the Entity.
597
            if ($row instanceof Entity) {
598
                $cast = $row->cast();
599

600
                // Disable Entity casting, because raw primary key value is needed for database.
601
                $row->cast(false);
602

603
                $primaryKey = $row->{$this->primaryKey};
604

605
                // Restore Entity casting setting.
606
                $row->cast($cast);
607

608
                return $primaryKey;
609
            }
610

611
            return $row->{$this->primaryKey};
612
        }
613

614
        if (is_array($row) && isset($row[$this->primaryKey])) {
615
            return $row[$this->primaryKey];
616
        }
617

618
        return null;
619
    }
620

621
    /**
622
     * Loops over records in batches, allowing you to operate on them.
623
     * Works with $this->builder to get the Compiled select to
624
     * determine the rows to operate on.
625
     * This method works only with dbCalls.
626
     *
627
     * @return void
628
     *
629
     * @throws DataException
630
     */
631
    public function chunk(int $size, Closure $userFunc)
632
    {
633
        $total  = $this->builder()->countAllResults(false);
634
        $offset = 0;
635

636
        while ($offset <= $total) {
637
            $builder = clone $this->builder();
638
            $rows    = $builder->get($size, $offset);
639

640
            if (! $rows) {
641
                throw DataException::forEmptyDataset('chunk');
642
            }
643

644
            $rows = $rows->getResult($this->tempReturnType);
645

646
            $offset += $size;
647

648
            if ($rows === []) {
649
                continue;
650
            }
651

652
            foreach ($rows as $row) {
653
                if ($userFunc($row) === false) {
654
                    return;
655
                }
656
            }
657
        }
658
    }
659

660
    /**
661
     * Override countAllResults to account for soft deleted accounts.
662
     *
663
     * @return int|string
664
     */
665
    public function countAllResults(bool $reset = true, bool $test = false)
666
    {
667
        if ($this->tempUseSoftDeletes) {
668
            $this->builder()->where($this->table . '.' . $this->deletedField, null);
669
        }
670

671
        // When $reset === false, the $tempUseSoftDeletes will be
672
        // dependent on $useSoftDeletes value because we don't
673
        // want to add the same "where" condition for the second time
674
        $this->tempUseSoftDeletes = $reset
675
            ? $this->useSoftDeletes
676
            : ($this->useSoftDeletes ? false : $this->useSoftDeletes);
677

678
        return $this->builder()->testMode($test)->countAllResults($reset);
679
    }
680

681
    /**
682
     * Provides a shared instance of the Query Builder.
683
     *
684
     * @param non-empty-string|null $table
685
     *
686
     * @return BaseBuilder
687
     *
688
     * @throws ModelException
689
     */
690
    public function builder(?string $table = null)
691
    {
692
        // Check for an existing Builder
693
        if ($this->builder instanceof BaseBuilder) {
694
            // Make sure the requested table matches the builder
695
            if ($table && $this->builder->getTable() !== $table) {
696
                return $this->db->table($table);
697
            }
698

699
            return $this->builder;
700
        }
701

702
        // We're going to force a primary key to exist
703
        // so we don't have overly convoluted code,
704
        // and future features are likely to require them.
705
        if ($this->primaryKey === '') {
706
            throw ModelException::forNoPrimaryKey(static::class);
707
        }
708

709
        $table = ($table === null || $table === '') ? $this->table : $table;
710

711
        // Ensure we have a good db connection
712
        if (! $this->db instanceof BaseConnection) {
713
            $this->db = Database::connect($this->DBGroup);
714
        }
715

716
        $builder = $this->db->table($table);
717

718
        // Only consider it "shared" if the table is correct
719
        if ($table === $this->table) {
720
            $this->builder = $builder;
721
        }
722

723
        return $builder;
724
    }
725

726
    /**
727
     * Captures the builder's set() method so that we can validate the
728
     * data here. This allows it to be used with any of the other
729
     * builder methods and still get validated data, like replace.
730
     *
731
     * @param array|object|string               $key    Field name, or an array of field/value pairs, or an object
732
     * @param bool|float|int|object|string|null $value  Field value, if $key is a single field
733
     * @param bool|null                         $escape Whether to escape values
734
     *
735
     * @return $this
736
     */
737
    public function set($key, $value = '', ?bool $escape = null)
738
    {
739
        if (is_object($key)) {
740
            $key = $key instanceof stdClass ? (array) $key : $this->objectToArray($key);
741
        }
742

743
        $data = is_array($key) ? $key : [$key => $value];
744

745
        foreach (array_keys($data) as $k) {
746
            $this->tempData['escape'][$k] = $escape;
747
        }
748

749
        $this->tempData['data'] = array_merge($this->tempData['data'] ?? [], $data);
750

751
        return $this;
752
    }
753

754
    /**
755
     * This method is called on save to determine if entry have to be updated
756
     * If this method return false insert operation will be executed
757
     *
758
     * @param array|object $row Data
759
     */
760
    protected function shouldUpdate($row): bool
761
    {
762
        if (parent::shouldUpdate($row) === false) {
763
            return false;
764
        }
765

766
        if ($this->useAutoIncrement === true) {
767
            return true;
768
        }
769

770
        // When useAutoIncrement feature is disabled, check
771
        // in the database if given record already exists
772
        return $this->where($this->primaryKey, $this->getIdValue($row))->countAllResults() === 1;
773
    }
774

775
    /**
776
     * Inserts data into the database. If an object is provided,
777
     * it will attempt to convert it to an array.
778
     *
779
     * @param         array|object|null     $row
780
     * @phpstan-param row_array|object|null $row
781
     * @param         bool                  $returnID Whether insert ID should be returned or not.
782
     *
783
     * @return         bool|int|string
784
     * @phpstan-return ($returnID is true ? int|string|false : bool)
785
     *
786
     * @throws ReflectionException
787
     */
788
    public function insert($row = null, bool $returnID = true)
789
    {
790
        if (isset($this->tempData['data'])) {
791
            if ($row === null) {
792
                $row = $this->tempData['data'];
793
            } else {
794
                $row = $this->transformDataToArray($row, 'insert');
795
                $row = array_merge($this->tempData['data'], $row);
796
            }
797
        }
798

799
        $this->escape   = $this->tempData['escape'] ?? [];
800
        $this->tempData = [];
801

802
        return parent::insert($row, $returnID);
803
    }
804

805
    /**
806
     * Ensures that only the fields that are allowed to be inserted are in
807
     * the data array.
808
     *
809
     * @used-by insert() to protect against mass assignment vulnerabilities.
810
     * @used-by insertBatch() to protect against mass assignment vulnerabilities.
811
     *
812
     * @param         array     $row Row data
813
     * @phpstan-param row_array $row
814
     *
815
     * @throws DataException
816
     */
817
    protected function doProtectFieldsForInsert(array $row): array
818
    {
819
        if (! $this->protectFields) {
820
            return $row;
821
        }
822

823
        if ($this->allowedFields === []) {
824
            throw DataException::forInvalidAllowedFields(static::class);
825
        }
826

827
        foreach (array_keys($row) as $key) {
828
            // Do not remove the non-auto-incrementing primary key data.
829
            if ($this->useAutoIncrement === false && $key === $this->primaryKey) {
830
                continue;
831
            }
832

833
            if (! in_array($key, $this->allowedFields, true)) {
834
                unset($row[$key]);
835
            }
836
        }
837

838
        return $row;
839
    }
840

841
    /**
842
     * Updates a single record in the database. If an object is provided,
843
     * it will attempt to convert it into an array.
844
     *
845
     * @param         array|int|string|null $id
846
     * @param         array|object|null     $row
847
     * @phpstan-param row_array|object|null $row
848
     *
849
     * @throws ReflectionException
850
     */
851
    public function update($id = null, $row = null): bool
852
    {
853
        if (isset($this->tempData['data'])) {
854
            if ($row === null) {
855
                $row = $this->tempData['data'];
856
            } else {
857
                $row = $this->transformDataToArray($row, 'update');
858
                $row = array_merge($this->tempData['data'], $row);
859
            }
860
        }
861

862
        $this->escape   = $this->tempData['escape'] ?? [];
863
        $this->tempData = [];
864

865
        return parent::update($id, $row);
866
    }
867

868
    /**
869
     * Takes a class and returns an array of its public and protected
870
     * properties as an array with raw values.
871
     *
872
     * @param object $object    Object
873
     * @param bool   $recursive If true, inner entities will be cast as array as well
874
     *
875
     * @return array<string, mixed> Array with raw values.
876
     *
877
     * @throws ReflectionException
878
     */
879
    protected function objectToRawArray($object, bool $onlyChanged = true, bool $recursive = false): array
880
    {
881
        return parent::objectToRawArray($object, $onlyChanged);
882
    }
883

884
    /**
885
     * Provides/instantiates the builder/db connection and model's table/primary key names and return type.
886
     *
887
     * @param string $name Name
888
     *
889
     * @return array|BaseBuilder|bool|float|int|object|string|null
890
     */
891
    public function __get(string $name)
892
    {
893
        if (parent::__isset($name)) {
894
            return parent::__get($name);
895
        }
896

897
        return $this->builder()->{$name} ?? null;
898
    }
899

900
    /**
901
     * Checks for the existence of properties across this model, builder, and db connection.
902
     *
903
     * @param string $name Name
904
     */
905
    public function __isset(string $name): bool
906
    {
907
        if (parent::__isset($name)) {
908
            return true;
909
        }
910

911
        return isset($this->builder()->{$name});
912
    }
913

914
    /**
915
     * Provides direct access to method in the builder (if available)
916
     * and the database connection.
917
     *
918
     * @return $this|array|BaseBuilder|bool|float|int|object|string|null
919
     */
920
    public function __call(string $name, array $params)
921
    {
922
        $builder = $this->builder();
923
        $result  = null;
924

925
        if (method_exists($this->db, $name)) {
926
            $result = $this->db->{$name}(...$params);
927
        } elseif (method_exists($builder, $name)) {
928
            $this->checkBuilderMethod($name);
929

930
            $result = $builder->{$name}(...$params);
931
        } else {
932
            throw new BadMethodCallException('Call to undefined method ' . static::class . '::' . $name);
933
        }
934

935
        if ($result instanceof BaseBuilder) {
936
            return $this;
937
        }
938

939
        return $result;
940
    }
941

942
    /**
943
     * Checks the Builder method name that should not be used in the Model.
944
     */
945
    private function checkBuilderMethod(string $name): void
946
    {
947
        if (in_array($name, $this->builderMethodsNotAvailable, true)) {
948
            throw ModelException::forMethodNotAvailable(static::class, $name . '()');
949
        }
950
    }
951
}
952

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.