Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.13% covered (success)
93.13%
312 / 335
75.56% covered (success)
75.56%
34 / 45
CRAP
0.00% covered (danger)
0.00%
0 / 1
Record
93.13% covered (success)
93.13%
312 / 335
75.56% covered (success)
75.56%
34 / 45
176.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
13
 hasDb
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDb
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setDefaultDb
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDb
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 db
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSql
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sql
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 table
80.00% covered (success)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 start
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
3.47
 commit
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 rollback
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
4.84
 transaction
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 findById
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findOne
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findOneOrCreate
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 findLatest
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 findBy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findByOrCreate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 findIn
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findAll
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 query
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 getTotal
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 getTableInfo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 with
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 getById
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getOne
88.89% covered (success)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
7.07
 getBy
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 getIn
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 getAll
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasOne
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 hasOneOf
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 hasMany
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 belongsTo
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 increment
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 decrement
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 replicate
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 copy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDirty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDirty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resetDirty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save
70.59% covered (success)
70.59%
12 / 17
0.00% covered (danger)
0.00%
0 / 1
12.54
 delete
77.78% covered (success)
77.78%
14 / 18
0.00% covered (danger)
0.00%
0 / 1
8.70
 __callStatic
100.00% covered (success)
100.00%
69 / 69
100.00% covered (success)
100.00%
1 / 1
24
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Db;
15
16use Pop\Db\Record\Collection;
17use Pop\Utils\CallableObject;
18
19/**
20 * Record class
21 *
22 * @category   Pop
23 * @package    Pop\Db
24 * @author     Nick Sagona, III <dev@nolainteractive.com>
25 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
26 * @license    http://www.popphp.org/license     New BSD License
27 * @version    6.5.0
28 * @method     static findWhereEquals($column, $value, array $options = null, bool|array $toArray = false)
29 * @method     static findWhereNotEquals($column, $value, array $options = null, bool|array $toArray = false)
30 * @method     static findWhereGreaterThan($column, $value, array $options = null, bool|array $toArray = false)
31 * @method     static findWhereGreaterThanOrEqual($column, $value, array $options = null, bool|array $toArray = false)
32 * @method     static findWhereLessThan($column, $value, array $options = null, bool|array $toArray = false)
33 * @method     static findWhereLessThanOrEqual($column, $value, array $options = null, bool|array $toArray = false)
34 * @method     static findWhereLike($column, $value, array $options = null, bool|array $toArray = false)
35 * @method     static findWhereNotLike($column, $value, array $options = null, bool|array $toArray = false)
36 * @method     static findWhereIn($column, $values, array $options = null, bool|array $toArray = false)
37 * @method     static findWhereNotIn($column, $values, array $options = null, bool|array $toArray = false)
38 * @method     static findWhereBetween($column, $values, array $options = null, bool|array $toArray = false)
39 * @method     static findWhereNotBetween($column, $values, array $options = null, bool|array $toArray = false)
40 * @method     static findWhereNull($column, array $options = null, bool|array $toArray = false)
41 * @method     static findWhereNotNull($column, array $options = null, bool|array $toArray = false)
42 */
43class Record extends Record\AbstractRecord
44{
45
46    /**
47     * Constructor
48     *
49     * Instantiate the database record object
50     *
51     * Optional parameters are an array of column values, db adapter, or a table name
52
53     * @throws Exception|Record\Exception
54     */
55    public function __construct()
56    {
57        $args    = func_get_args();
58        $columns = null;
59        $table   = null;
60        $db      = null;
61        $class   = get_class($this);
62
63        foreach ($args as $arg) {
64            if (is_array($arg) || ($arg instanceof \ArrayAccess) || ($arg instanceof \ArrayObject)) {
65                $columns = $arg;
66            } else if ($arg instanceof Adapter\AbstractAdapter) {
67                $db = $arg;
68            } else if (is_string($arg)) {
69                $table = $arg;
70            }
71        }
72
73        if ($table !== null) {
74            $this->setTable($table);
75        } else if ($this->table !== null) {
76            $this->setTable($this->table);
77        } else {
78            $this->setTableFromClassName($class);
79        }
80
81        if ($db !== null) {
82            Db::setDb($db, $class, null, ($class === __CLASS__));
83        }
84
85        if (!Db::hasDb($class)) {
86            throw new Exception('Error: A database connection has not been set.');
87        } else if (!Db::hasClassToTable($class)) {
88            Db::addClassToTable($class, $this->getFullTable());
89        }
90
91        $this->tableGateway = new Gateway\Table($this->getFullTable());
92        $this->rowGateway   = new Gateway\Row($this->getFullTable(), $this->primaryKeys);
93
94        if ($columns !== null) {
95            $this->isNew = true;
96            $this->setColumns($columns);
97        }
98    }
99
100/*
101 * Static methods
102 */
103
104    /**
105     * Check for a DB adapter
106     *
107     * @return bool
108     */
109    public static function hasDb(): bool
110    {
111        return Db::hasDb(get_called_class());
112    }
113
114    /**
115     * Set DB adapter
116     *
117     * @param  Adapter\AbstractAdapter $db
118     * @param  ?string                 $prefix
119     * @param  bool                    $isDefault
120     * @return void
121     */
122    public static function setDb(Adapter\AbstractAdapter $db, ?string $prefix = null, bool $isDefault = false): void
123    {
124        $class = get_called_class();
125        if ($class == 'Pop\Db\Record') {
126            Db::setDefaultDb($db);
127        } else {
128            Db::setDb($db, $class, $prefix, $isDefault);
129        }
130    }
131
132    /**
133     * Set DB adapter
134     *
135     * @param  Adapter\AbstractAdapter $db
136     * @return void
137     */
138    public static function setDefaultDb(Adapter\AbstractAdapter $db): void
139    {
140        Db::setDb($db, null, null, true);
141    }
142
143    /**
144     * Get DB adapter
145     *
146     * @return Adapter\AbstractAdapter
147     */
148    public static function getDb(): Adapter\AbstractAdapter
149    {
150        return Db::getDb(get_called_class());
151    }
152
153    /**
154     * Get DB adapter (alias)
155     *
156     * @return Adapter\AbstractAdapter
157     */
158    public static function db(): Adapter\AbstractAdapter
159    {
160        return Db::db(get_called_class());
161    }
162
163    /**
164     * Get SQL builder
165     *
166     * @return Sql
167     */
168    public static function getSql(): Sql
169    {
170        return Db::db(get_called_class())->createSql();
171    }
172
173    /**
174     * Get SQL builder (alias)
175     *
176     * @return Sql
177     */
178    public static function sql(): Sql
179    {
180        return Db::db(get_called_class())->createSql();
181    }
182
183    /**
184     * Get table name
185     *
186     * @param  bool $quotes
187     * @return string
188     */
189    public static function table(bool $quotes = false): string
190    {
191        $table = (new static())->getFullTable();
192        $sql   = static::sql();
193        if ($quotes) {
194            $table = $sql->quoteId($table);
195        }
196        return $table;
197    }
198
199    /**
200     * Start transaction with the DB adapter. When called on a descendent class, construct
201     * a new object and use it for transaction management.
202     *
203     * @param mixed ...$constructorArgs Arguments passed to descendent class constructor
204     * @return static|null
205     * @throws Exception|Record\Exception
206     */
207    public static function start(mixed ...$constructorArgs): static|null
208    {
209        $class = get_called_class();
210
211        if ($class !== Record::class) {
212            $record = new static(...$constructorArgs);
213            $record->startTransaction();
214            return $record;
215        } else {
216            if (Db::hasDb($class)) {
217                Db::db($class)->beginTransaction();
218            }
219            return null;
220        }
221    }
222
223    /**
224     * Commit transaction with the DB adapter
225     *
226     * @throws Exception
227     * @return void
228     */
229    public static function commit(): void
230    {
231        $class = get_called_class();
232        if (Db::hasDb($class)) {
233            Db::db($class)->commit();
234        }
235    }
236
237    /**
238     * Rollback transaction with the DB adapter
239     *
240     * @param  \Exception|null $exception
241     * @throws Exception
242     * @return \Exception|null
243     */
244    public static function rollback(\Exception $exception = null): \Exception|null
245    {
246        $class = get_called_class();
247
248        if (Db::hasDb($class)) {
249            if (Db::db($class)->getTransactionDepth() == 1) {
250                Db::db($class)->rollback();
251            } else {
252                if ($exception == null) {
253                    $exception = new Exception('Error: A rollback has been executed from within a nested transaction.');
254                }
255                return $exception;
256            }
257        }
258
259        return null;
260    }
261
262    /**
263     * Execute complete transaction with the DB adapter
264     *
265     * @param  mixed $callable
266     * @param  mixed $params
267     * @throws \Exception
268     * @return void
269     */
270    public static function transaction(mixed $callable, mixed $params = null): void
271    {
272        if (!($callable instanceof CallableObject)) {
273            $callable = new CallableObject($callable, $params);
274        }
275
276        try {
277            static::start();
278            $callable->call();
279            static::commit();
280        } catch (\Exception $e) {
281            $result = static::rollback($e);
282            throw (!empty($result)) ? $result : $e;
283        }
284    }
285
286    /**
287     * Find by ID static method
288     *
289     * @param  mixed  $id
290     * @param  ?array $options
291     * @param  bool   $toArray
292     * @return static|array
293     */
294    public static function findById(mixed $id, ?array $options = null, bool $toArray = false): array|static
295    {
296        return (new static())->getById($id, $options, $toArray);
297    }
298
299    /**
300     * Find one static method
301     *
302     * @param  ?array $columns
303     * @param  ?array $options
304     * @param  bool   $toArray
305     * @return static|array
306     */
307    public static function findOne(?array $columns = null, ?array $options = null, bool $toArray = false): array|static
308    {
309        return (new static())->getOne($columns, $options, $toArray);
310    }
311
312    /**
313     * Find one or create static method
314     *
315     * @param  ?array $columns
316     * @param  ?array $options
317     * @param  bool   $toArray
318     * @return static|array
319     */
320    public static function findOneOrCreate(?array $columns = null, ?array $options = null, bool $toArray = false): array|static
321    {
322        $result = (new static())->getOne($columns, $options);
323
324        if (empty($result->toArray())) {
325            $newRecord = new static($columns);
326            $newRecord->save();
327            $result = $newRecord;
328        }
329
330        return ($toArray) ? $result->toArray() : $result;
331    }
332
333    /**
334     * Find latest static method
335     *
336     * @param  ?string $by
337     * @param  ?array  $columns
338     * @param  ?array  $options
339     * @param  bool    $toArray
340     * @return static|array
341     */
342    public static function findLatest(?string $by = null, ?array $columns = null, ?array $options = null, bool $toArray = false): array|static
343    {
344        $record = new static();
345
346        if (($by === null) && (count($record->getPrimaryKeys()) == 1)) {
347            $by = $record->getPrimaryKeys()[0];
348        }
349
350        if ($by !== null) {
351            if ($options === null) {
352                $options = ['order' => $by . ' DESC'];
353            } else {
354                $options['order'] = $by . ' DESC';
355            }
356        }
357
358        return $record->getOne($columns, $options, $toArray);
359    }
360
361    /**
362     * Find by static method
363     *
364     * @param  ?array     $columns
365     * @param  ?array     $options
366     * @param  bool|array $toArray
367     * @return Collection|array
368     */
369    public static function findBy(?array $columns = null, ?array $options = null, bool|array $toArray = false): Collection|array
370    {
371        return (new static())->getBy($columns, $options, $toArray);
372    }
373
374    /**
375     * Find by or create static method
376     *
377     * @param  ?array     $columns
378     * @param  ?array     $options
379     * @param  bool|array $toArray
380     * @return static|Collection|array
381     */
382    public static function findByOrCreate(?array $columns = null, ?array $options = null, bool|array $toArray = false): Collection|array|static
383    {
384        $result = (new static())->getBy($columns, $options);
385
386        if ($result->count() == 0) {
387            $newRecord = new static($columns);
388            $newRecord->save();
389            $result = $newRecord;
390            return ($toArray !== false) ? $result->toArray() : $result;
391        } else {
392            return ($toArray !== false) ? $result->toArray($toArray) : $result;
393        }
394    }
395
396    /**
397     * Find in static method
398     *
399     * @param  string  $key
400     * @param  array   $values
401     * @param  ?array  $columns
402     * @param  ?array  $options
403     * @param  bool    $toArray
404     * @return array
405     */
406    public static function findIn(string $key, array $values, ?array $columns = null, ?array $options = null, bool|array $toArray = false): array
407    {
408        return (new static())->getIn($key, $values, $columns, $options, $toArray);
409    }
410
411    /**
412     * Find all static method
413     *
414     * @param  ?array $options
415     * @param  bool   $toArray
416     * @return Collection|array|static
417     */
418    public static function findAll(?array $options = null, bool|array $toArray = false): Collection|array|static
419    {
420        return static::findBy(null, $options, $toArray);
421    }
422
423    /**
424     * Static method to execute a custom prepared SQL statement.
425     *
426     * @param  mixed $sql
427     * @param  array $params
428     * @param  bool  $toArray
429     * @return Collection|array|null
430     */
431    public static function execute(mixed $sql, array $params = [], bool|array $toArray = false): Collection|array|null
432    {
433        $record = new static();
434
435        if ($sql instanceof Sql) {
436            $sql = (string)$sql;
437        }
438
439        $db = Db::getDb($record->getFullTable());
440        $db->prepare($sql);
441        if (!empty($params)) {
442            $db->bindParams($params);
443        }
444        $db->execute();
445
446        $rows     = [];
447        $isSelect = false;
448
449        if (strtoupper(substr($sql, 0, 6)) == 'SELECT') {
450            $isSelect = true;
451            $rows     = $db->fetchAll();
452            foreach ($rows as $i => $row) {
453                $rows[$i] = $record->processRow($row, $toArray);
454            }
455        }
456
457        if ($isSelect) {
458            $collection = new Record\Collection($rows);
459            return ($toArray !== false) ? $collection->toArray($toArray) : $collection;
460        } else {
461            return null;
462        }
463    }
464
465    /**
466     * Static method to execute a custom SQL query.
467     *
468     * @param  mixed $sql
469     * @param  bool  $toArray
470     * @return Collection|array|null
471     */
472    public static function query(mixed $sql, bool|array $toArray = false): Collection|array|null
473    {
474        $record = new static();
475
476        if ($sql instanceof Sql) {
477            $sql = (string)$sql;
478        }
479
480        $db = Db::getDb($record->getFullTable());
481        $db->query($sql);
482
483        $rows     = [];
484        $isSelect = false;
485
486        if (strtoupper(substr($sql, 0, 6)) == 'SELECT') {
487            $isSelect = true;
488            while (($row = $db->fetch())) {
489                $rows[] = $record->processRow($row, $toArray);
490            }
491        }
492
493        if ($isSelect) {
494            $collection = new Record\Collection($rows);
495            return ($toArray !== false) ? $collection->toArray($toArray) : $collection;
496        } else {
497            return null;
498        }
499    }
500
501    /**
502     * Static method to get the total count of a set from the DB table
503     *
504     * @param  ?array $columns
505     * @param  ?array $options
506     * @return int
507     */
508    public static function getTotal(?array $columns = null, ?array $options = null): int
509    {
510        $record      = new static();
511        $expressions = null;
512        $params      = null;
513
514        if ($columns !== null) {
515            $db            = Db::getDb($record->getFullTable());
516            $sql           = $db->createSql();
517            ['expressions' => $expressions, 'params' => $params] =
518                Sql\Parser\Expression::parseShorthand($columns, $sql->getPlaceholder());
519        }
520
521        $rows = $record->getTableGateway()->select(['total_count' => 'COUNT(1)'], $expressions, $params, $options);
522
523        return (isset($rows[0]) && isset($rows[0]['total_count'])) ? (int)$rows[0]['total_count'] : 0;
524    }
525
526    /**
527     * Static method to get the total count of a set from the DB table
528     *
529     * @return array
530     */
531    public static function getTableInfo(): array
532    {
533        return (new static())->getTableGateway()->getTableInfo();
534    }
535
536    /**
537     * With a 1:many relationship (eager-loading)
538     *
539     * @param  mixed  $name
540     * @param  ?array $options
541     * @return static
542     */
543    public static function with($name, ?array $options = null): static
544    {
545        $record = new static();
546
547        if (is_array($name)) {
548            foreach ($name as $key => $value) {
549                if (is_numeric($key) && is_string($value)) {
550                    $record->addWith($value);
551                } else if (!is_numeric($key) && is_array($value)) {
552                    $record->addWith($key, $value);
553                }
554            }
555        } else {
556            $record->addWith($name, $options);
557        }
558
559        return $record;
560    }
561
562/*
563 * Instance methods
564 */
565
566    /**
567     * Get by ID method
568     *
569     * @param  mixed  $id
570     * @param  ?array $options
571     * @param  bool   $toArray
572     * @return static|array
573     */
574    public function getById($id, ?array $options = null, bool $toArray = false): Record|array|static
575    {
576        $this->setColumns($this->getRowGateway()->find($id, [], $options));
577        if ($this->hasWiths()) {
578            $this->getWithRelationships(false);
579        }
580        return ($toArray) ? $this->toArray() : $this;
581    }
582
583    /**
584     * Get one method
585     *
586     * @param  ?array $columns
587     * @param  ?array $options
588     * @param  bool   $toArray
589     * @return static|array
590     */
591    public function getOne(?array $columns = null, ?array $options = null, bool $toArray = false): Record|array|static
592    {
593        if ($options === null) {
594            $options = ['limit' => 1];
595        } else {
596            $options['limit'] = 1;
597        }
598
599        $expressions = null;
600        $params      = null;
601        $select      = $options['select'] ?? null;
602
603        if ($columns !== null) {
604            $db            = Db::getDb($this->getFullTable());
605            $sql           = $db->createSql();
606            ['expressions' => $expressions, 'params' => $params] =
607                Sql\Parser\Expression::parseShorthand($columns, $sql->getPlaceholder());
608        }
609
610        $rows = $this->getTableGateway()->select($select, $expressions, $params, $options);
611
612        if ($this->hasWiths() && !empty($rows)) {
613            $this->getWithRelationships();
614            $this->processWithRelationships($rows);
615        }
616
617        if (isset($rows[0])) {
618            $this->setColumns($rows[0]);
619        }
620
621        return ($toArray) ? $this->toArray() : $this;
622    }
623
624    /**
625     * Get by method
626     *
627     * @param  ?array $columns
628     * @param  ?array $options
629     * @param  bool   $toArray
630     * @return Collection|array
631     */
632    public function getBy(?array $columns = null, ?array $options = null, bool|array $toArray = false): Collection|array
633    {
634        $expressions = null;
635        $params      = null;
636        $select      = $options['select'] ?? null;
637
638        if ($columns !== null) {
639            $db            = Db::getDb($this->getFullTable());
640            $sql           = $db->createSql();
641            ['expressions' => $expressions, 'params' => $params] =
642                Sql\Parser\Expression::parseShorthand($columns, $sql->getPlaceholder());
643        }
644
645        $rows = $this->getTableGateway()->select($select, $expressions, $params, $options);
646
647        foreach ($rows as $i => $row) {
648            $rows[$i] = $this->processRow($row);
649        }
650
651        if ($this->hasWiths() && !empty($rows)) {
652            $this->getWithRelationships();
653            $this->processWithRelationships($rows);
654        }
655
656        $collection = new Record\Collection($rows);
657        return ($toArray !== false) ? $collection->toArray($toArray) : $collection;
658    }
659
660    /**
661     * Get in method
662     *
663     * @param  string $key
664     * @param  array  $values
665     * @param  ?array $columns
666     * @param  ?array $options
667     * @param  bool   $toArray
668     * @return array
669     */
670    public function getIn(string $key, array $values, array $columns = null, array $options = null, bool|array $toArray = false): array
671    {
672        $columns = ($columns !== null) ? array_merge([$key => $values], $columns) : [$key => $values];
673        $results = $this->getBy($columns, $options, $toArray);
674        $rows    = [];
675
676        foreach ($results as $row) {
677            if (isset($row[$key])) {
678                $rows[$row[$key]] = (($toArray !== false) && ($row instanceof Record)) ? $row->toArray() : $row;
679            }
680        }
681
682        return $rows;
683    }
684
685    /**
686     * Get all method
687     *
688     * @param  ?array $options
689     * @param  bool   $toArray
690     * @return Collection|array
691     */
692    public function getAll(?array $options = null, bool|array $toArray = false): Collection|array
693    {
694        return $this->getBy(null, $options, $toArray);
695    }
696
697    /**
698     * Has one relationship
699     *
700     * @param  string $foreignTable
701     * @param  string $foreignKey
702     * @param  ?array $options
703     * @param  bool   $eager
704     * @return Record|Record\Relationships\HasOne
705     */
706    public function hasOne(string $foreignTable, string $foreignKey, ?array $options = null, bool $eager = false): Record|Record\Relationships\HasOne
707    {
708        $relationship = new Record\Relationships\HasOne($this, $foreignTable, $foreignKey, $options);
709        if (!empty($this->withChildren)) {
710            $relationship->setChildRelationships($this->withChildren);
711        }
712        return ($eager) ? $relationship : $relationship->getChild($options);
713    }
714
715    /**
716     * Has one of relationship
717     *
718     * @param  string $foreignTable
719     * @param  string $foreignKey
720     * @param  ?array $options
721     * @param  bool   $eager
722     * @return Record|Record\Relationships\HasOneOf
723     */
724    public function hasOneOf(string $foreignTable, string $foreignKey, ?array $options = null, bool $eager = false): Record|Record\Relationships\HasOneOf
725    {
726        $relationship = new Record\Relationships\HasOneOf($this, $foreignTable, $foreignKey, $options);
727        if (!empty($this->withChildren)) {
728            $relationship->setChildRelationships($this->withChildren);
729        }
730        return ($eager) ? $relationship : $relationship->getChild();
731    }
732
733    /**
734     * Has many relationship
735     *
736     * @param  string $foreignTable
737     * @param  string $foreignKey
738     * @param  ?array $options
739     * @param  bool   $eager
740     * @return Collection|Record\Relationships\HasMany
741     */
742    public function hasMany(string $foreignTable, string $foreignKey, ?array $options = null, bool $eager = false): Collection|Record\Relationships\HasMany
743    {
744        $relationship = new Record\Relationships\HasMany($this, $foreignTable, $foreignKey, $options);
745        if (!empty($this->withChildren)) {
746            $relationship->setChildRelationships($this->withChildren);
747        }
748        return ($eager) ? $relationship : $relationship->getChildren($options);
749    }
750
751    /**
752     * Belongs to relationship
753     *
754     * @param  string $foreignTable
755     * @param  string $foreignKey
756     * @param  ?array $options
757     * @param  bool   $eager
758     * @return Record|Record\Relationships\BelongsTo
759     */
760    public function belongsTo(string $foreignTable, string $foreignKey, ?array $options = null, bool $eager = false): Record|Record\Relationships\BelongsTo
761    {
762        $relationship = new Record\Relationships\BelongsTo($this, $foreignTable, $foreignKey, $options);
763        if (!empty($this->withChildren)) {
764            $relationship->setChildRelationships($this->withChildren);
765        }
766        return ($eager) ? $relationship : $relationship->getParent($options);
767    }
768
769    /**
770     * Increment the record column and save
771     *
772     * @param  string $column
773     * @param  int    $amount
774     * @return void
775     */
776    public function increment(string $column, int $amount = 1): void
777    {
778        $this->{$column} += (int)$amount;
779        $this->save();
780    }
781
782    /**
783     * Decrement the record column and save
784     *
785     * @param  string $column
786     * @param  int    $amount
787     * @return void
788     */
789    public function decrement(string $column, int $amount = 1): void
790    {
791        $this->{$column} -= (int)$amount;
792        $this->save();
793    }
794
795    /**
796     * Replicate the record
797     *
798     * @param  array $replace
799     * @return static
800     */
801    public function replicate(array $replace = []): static
802    {
803        $fields = $this->toArray();
804
805        foreach ($this->primaryKeys as $key) {
806            if (isset($fields[$key])) {
807                unset($fields[$key]);
808            }
809        }
810
811        if (!empty($replace)) {
812            foreach ($replace as $key => $value) {
813                if (array_key_exists($key, $fields)) {
814                    $fields[$key] = $value;
815                }
816            }
817        }
818
819        $newRecord = new static($fields);
820        $newRecord->save();
821
822        return $newRecord;
823    }
824
825    /**
826     * Copy the record (alias to replicate)
827     *
828     * @param  array $replace
829     * @return static
830     */
831    public function copy(array $replace = []): static
832    {
833        return $this->replicate($replace);
834    }
835
836    /**
837     * Check if row is dirty
838     *
839     * @return bool
840     */
841    public function isDirty(): bool
842    {
843        return $this->rowGateway->isDirty();
844    }
845
846    /**
847     * Get row's dirty columns
848     *
849     * @return array
850     */
851    public function getDirty(): array
852    {
853        return $this->rowGateway->getDirty();
854    }
855
856    /**
857     * Reset row's dirty columns
858     *
859     * @return void
860     */
861    public function resetDirty(): void
862    {
863        $this->rowGateway->resetDirty();
864    }
865
866    /**
867     * Save or update the record
868     *
869     * @param  ?array $columns
870     * @param  bool   $commit
871     * @throws \Exception
872     * @return void
873     */
874    public function save(array $columns = null, bool $commit = true): void
875    {
876        try {
877            // Save or update the record
878            if ($columns === null) {
879                if ($this->isNew) {
880                    $this->rowGateway->save();
881                    $this->isNew = false;
882                } else {
883                    $this->rowGateway->update();
884                    $record = $this->getById($this->rowGateway->getPrimaryValues());
885                    if (isset($record[0])) {
886                        $this->setColumns($record[0]);
887                    }
888                }
889                // Else, save multiple rows
890            } else {
891                if (isset($columns[0])) {
892                    $this->tableGateway->insertRows($columns);
893                } else {
894                    $this->tableGateway->insert($columns);
895                }
896            }
897            if (($this->isTransaction()) && ($commit)) {
898                $this->commitTransaction();
899            }
900        } catch (\Exception $e) {
901            if (($this->isTransaction()) && ($commit)) {
902                $this->rollbackTransaction();
903            }
904            throw $e;
905        }
906    }
907
908    /**
909     * Delete the record
910     *
911     * @param  ?array $columns
912     * @param  bool   $commit
913     * @return void
914     */
915    public function delete(array $columns = null, bool $commit = true): void
916    {
917        try {
918            // Delete the record
919            if ($columns === null) {
920                $this->rowGateway->delete();
921            // Delete multiple rows
922            } else {
923                $expressions = null;
924                $params      = [];
925
926                if ($columns !== null) {
927                    $db            = Db::getDb($this->getFullTable());
928                    $sql           = $db->createSql();
929                    ['expressions' => $expressions, 'params' => $params] =
930                        Sql\Parser\Expression::parseShorthand($columns, $sql->getPlaceholder());
931                }
932
933                $this->tableGateway->delete($expressions, $params);
934            }
935
936            $this->setRows();
937            $this->setColumns();
938
939            if (($this->isTransaction()) && ($commit)) {
940                $this->commitTransaction();
941            }
942        } catch (\Exception $e) {
943            if (($this->isTransaction()) && ($commit)) {
944                $this->rollbackTransaction();
945            }
946            throw $e;
947        }
948
949    }
950
951    /**
952     * Call static method for 'findWhere'
953     *
954     *     $users = Users::findWhereUsername($value);
955     *
956     *     $users = Users::findWhereEquals($column, $value);
957     *     $users = Users::findWhereNotEquals($column, $value);
958     *     $users = Users::findWhereGreaterThan($column, $value);
959     *     $users = Users::findWhereGreaterThanOrEqual($column, $value);
960     *     $users = Users::findWhereLessThan($column, $value);
961     *     $users = Users::findWhereLessThanOrEqual($column, $value);
962     *
963     *     $users = Users::findWhereLike($column, $value);
964     *     $users = Users::findWhereNotLike($column, $value);
965     *
966     *     $users = Users::findWhereIn($column, $values);
967     *     $users = Users::findWhereNotIn($column, $values);
968     *
969     *     $users = Users::findWhereBetween($column, $values);
970     *     $users = Users::findWhereNotBetween($column, $values);
971     *
972     *     $users = Users::findWhereNull($column);
973     *     $users = Users::findWhereNotNull($column);
974     *
975     * @param  string $name
976     * @param  array  $arguments
977     * @return Collection|array|null
978     */
979    public static function __callStatic(string $name, array $arguments): Collection|array|null
980    {
981        $columns    = null;
982        $options    = null;
983        $toArray    = false;
984        $conditions = [
985            'Equals', 'NotEquals', 'GreaterThan', 'GreaterThanOrEqual', 'LessThan', 'LessThanOrEqual',
986            'Like', 'NotLike', 'In', 'NotIn', 'Between', 'NotBetween', 'Null', 'NotNull'
987        ];
988
989        if (str_starts_with($name, 'findWhere')) {
990            if (in_array(substr($name, 9), $conditions)) {
991                $condition = substr($name, 9);
992                $column    = $arguments[0];
993
994                if (str_contains($condition, 'Null')) {
995                    $value     = null;
996                    $options   = $arguments[1] ?? null;
997                    $toArray   = $arguments[2] ?? false;
998                } else {
999                    $value     = $arguments[1];
1000                    $options   = $arguments[2] ?? null;
1001                    $toArray   = $arguments[3] ?? false;
1002                }
1003
1004                switch ($condition) {
1005                    case 'Equals':
1006                    case 'In':
1007                    case 'Between':
1008                    case 'Null':
1009                        $columns = [$column => $value];
1010                        break;
1011                    case 'NotEquals':
1012                        $columns = [$column . '!=' => $value];
1013                        break;
1014                    case 'GreaterThan':
1015                        $columns = [$column . '>' => $value];
1016                        break;
1017                    case 'GreaterThanOrEqual':
1018                        $columns = [$column . '>=' => $value];
1019                        break;
1020                    case 'LessThan':
1021                        $columns = [$column . '<' => $value];
1022                        break;
1023                    case 'LessThanOrEqual':
1024                        $columns = [$column . '<=' => $value];
1025                        break;
1026                    case 'Like':
1027                        if (str_starts_with($value, '%')) {
1028                            $column = '%' . $column;
1029                            $value  = substr($value, 1);
1030                        }
1031                        if (str_ends_with($value, '%')) {
1032                            $column .= '%';
1033                            $value   = substr($value, 0, -1);
1034                        }
1035                        $columns = [$column => $value];
1036                        break;
1037                    case 'NotLike':
1038                        if (str_starts_with($value, '%')) {
1039                            $column = '-%' . $column;
1040                            $value  = substr($value, 1);
1041                        }
1042                        if (str_ends_with($value, '%')) {
1043                            $column .= '%-';
1044                            $value   = substr($value, 0, -1);
1045                        }
1046                        $columns = [$column => $value];
1047                        break;
1048                    case 'NotIn':
1049                    case 'NotBetween':
1050                    case 'NotNull':
1051                        $columns = [$column . '-' => $value];
1052                        break;
1053                }
1054            } else {
1055                $column  = Sql\Parser\Table::parse(substr($name, 9));
1056                $value   = $arguments[0] ?? null;
1057                $options = $arguments[1] ?? null;
1058                $toArray = $arguments[2] ?? false;
1059
1060                if ($value !== null) {
1061                    $columns = [$column => $value];
1062                }
1063            }
1064        }
1065
1066        return ($columns !== null) ? static::findBy($columns, $options, $toArray) : null;
1067    }
1068
1069}