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