Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.11% covered (success)
93.11%
311 / 334
75.56% covered (success)
75.56%
34 / 45
CRAP
0.00% covered (danger)
0.00%
0 / 1
Record
93.11% covered (success)
93.11%
311 / 334
75.56% covered (success)
75.56%
34 / 45
175.00
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%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 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 $asArray = false)
29 * @method     static findWhereNotEquals($column, $value, array $options = null, bool $asArray = false)
30 * @method     static findWhereGreaterThan($column, $value, array $options = null, bool $asArray = false)
31 * @method     static findWhereGreaterThanOrEqual($column, $value, array $options = null, bool $asArray = false)
32 * @method     static findWhereLessThan($column, $value, array $options = null, bool $asArray = false)
33 * @method     static findWhereLessThanOrEqual($column, $value, array $options = null, bool $asArray = false)
34 * @method     static findWhereLike($column, $value, array $options = null, bool $asArray = false)
35 * @method     static findWhereNotLike($column, $value, array $options = null, bool $asArray = false)
36 * @method     static findWhereIn($column, $values, array $options = null, bool $asArray = false)
37 * @method     static findWhereNotIn($column, $values, array $options = null, bool $asArray = false)
38 * @method     static findWhereBetween($column, $values, array $options = null, bool $asArray = false)
39 * @method     static findWhereNotBetween($column, $values, array $options = null, bool $asArray = false)
40 * @method     static findWhereNull($column, array $options = null, bool $asArray = false)
41 * @method     static findWhereNotNull($column, array $options = null, bool $asArray = 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   $asArray
292     * @return static|array
293     */
294    public static function findById(mixed $id, ?array $options = null, bool $asArray = false): array|static
295    {
296        return (new static())->getById($id, $options, $asArray);
297    }
298
299    /**
300     * Find one static method
301     *
302     * @param  ?array $columns
303     * @param  ?array $options
304     * @param  bool   $asArray
305     * @return static|array
306     */
307    public static function findOne(?array $columns = null, ?array $options = null, bool $asArray = false): array|static
308    {
309        return (new static())->getOne($columns, $options, $asArray);
310    }
311
312    /**
313     * Find one or create static method
314     *
315     * @param  ?array $columns
316     * @param  ?array $options
317     * @param  bool   $asArray
318     * @return static|array
319     */
320    public static function findOneOrCreate(?array $columns = null, ?array $options = null, bool $asArray = 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 ($asArray) ? $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    $asArray
340     * @return static|array
341     */
342    public static function findLatest(?string $by = null, ?array $columns = null, ?array $options = null, bool $asArray = 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, $asArray);
359    }
360
361    /**
362     * Find by static method
363     *
364     * @param  ?array $columns
365     * @param  ?array $options
366     * @param  bool   $asArray
367     * @return Collection|array
368     */
369    public static function findBy(?array $columns = null, ?array $options = null, bool $asArray = false): Collection|array
370    {
371        return (new static())->getBy($columns, $options, $asArray);
372    }
373
374    /**
375     * Find by or create static method
376     *
377     * @param  ?array $columns
378     * @param  ?array $options
379     * @param  bool   $asArray
380     * @return static|Collection|array
381     */
382    public static function findByOrCreate(?array $columns = null, ?array $options = null, bool $asArray = 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        }
391
392        return ($asArray) ? $result->toArray() : $result;
393    }
394
395    /**
396     * Find in static method
397     *
398     * @param  string  $key
399     * @param  array   $values
400     * @param  ?array  $columns
401     * @param  ?array  $options
402     * @param  bool    $asArray
403     * @return array
404     */
405    public static function findIn(string $key, array $values, ?array $columns = null, ?array $options = null, bool $asArray = false): array
406    {
407        return (new static())->getIn($key, $values, $columns, $options, $asArray);
408    }
409
410    /**
411     * Find all static method
412     *
413     * @param  ?array $options
414     * @param  bool   $asArray
415     * @return Collection|array|static
416     */
417    public static function findAll(?array $options = null, bool $asArray = false): Collection|array|static
418    {
419        return static::findBy(null, $options, $asArray);
420    }
421
422    /**
423     * Static method to execute a custom prepared SQL statement.
424     *
425     * @param  mixed $sql
426     * @param  array $params
427     * @param  bool  $asArray
428     * @return Collection|array|null
429     */
430    public static function execute(mixed $sql, array $params = [], bool $asArray = false): Collection|array|null
431    {
432        $record = new static();
433
434        if ($sql instanceof Sql) {
435            $sql = (string)$sql;
436        }
437
438        $db = Db::getDb($record->getFullTable());
439        $db->prepare($sql);
440        if (!empty($params)) {
441            $db->bindParams($params);
442        }
443        $db->execute();
444
445        $rows     = [];
446        $isSelect = false;
447
448        if (strtoupper(substr($sql, 0, 6)) == 'SELECT') {
449            $isSelect = true;
450            $rows     = $db->fetchAll();
451            foreach ($rows as $i => $row) {
452                $rows[$i] = $record->processRow($row, $asArray);
453            }
454        }
455
456        if ($isSelect) {
457            $collection = new Record\Collection($rows);
458            return ($asArray) ? $collection->toArray() : $collection;
459        } else {
460            return null;
461        }
462    }
463
464    /**
465     * Static method to execute a custom SQL query.
466     *
467     * @param  mixed $sql
468     * @param  bool  $asArray
469     * @return Collection|array|null
470     */
471    public static function query(mixed $sql, bool $asArray = false): Collection|array|null
472    {
473        $record = new static();
474
475        if ($sql instanceof Sql) {
476            $sql = (string)$sql;
477        }
478
479        $db = Db::getDb($record->getFullTable());
480        $db->query($sql);
481
482        $rows     = [];
483        $isSelect = false;
484
485        if (strtoupper(substr($sql, 0, 6)) == 'SELECT') {
486            $isSelect = true;
487            while (($row = $db->fetch())) {
488                $rows[] = $record->processRow($row, $asArray);
489            }
490        }
491
492        if ($isSelect) {
493            $collection = new Record\Collection($rows);
494            return ($asArray) ? $collection->toArray() : $collection;
495        } else {
496            return null;
497        }
498    }
499
500    /**
501     * Static method to get the total count of a set from the DB table
502     *
503     * @param  ?array $columns
504     * @param  ?array $options
505     * @return int
506     */
507    public static function getTotal(?array $columns = null, ?array $options = null): int
508    {
509        $record      = new static();
510        $expressions = null;
511        $params      = null;
512
513        if ($columns !== null) {
514            $db            = Db::getDb($record->getFullTable());
515            $sql           = $db->createSql();
516            ['expressions' => $expressions, 'params' => $params] =
517                Sql\Parser\Expression::parseShorthand($columns, $sql->getPlaceholder());
518        }
519
520        $rows = $record->getTableGateway()->select(['total_count' => 'COUNT(1)'], $expressions, $params, $options);
521
522        return (isset($rows[0]) && isset($rows[0]['total_count'])) ? (int)$rows[0]['total_count'] : 0;
523    }
524
525    /**
526     * Static method to get the total count of a set from the DB table
527     *
528     * @return array
529     */
530    public static function getTableInfo(): array
531    {
532        return (new static())->getTableGateway()->getTableInfo();
533    }
534
535    /**
536     * With a 1:many relationship (eager-loading)
537     *
538     * @param  mixed  $name
539     * @param  ?array $options
540     * @return static
541     */
542    public static function with($name, ?array $options = null): static
543    {
544        $record = new static();
545
546        if (is_array($name)) {
547            foreach ($name as $key => $value) {
548                if (is_numeric($key) && is_string($value)) {
549                    $record->addWith($value);
550                } else if (!is_numeric($key) && is_array($value)) {
551                    $record->addWith($key, $value);
552                }
553            }
554        } else {
555            $record->addWith($name, $options);
556        }
557
558        return $record;
559    }
560
561/*
562 * Instance methods
563 */
564
565    /**
566     * Get by ID method
567     *
568     * @param  mixed  $id
569     * @param  ?array $options
570     * @param  bool   $asArray
571     * @return static|array
572     */
573    public function getById($id, ?array $options = null, bool $asArray = false): Record|array|static
574    {
575        $this->setColumns($this->getRowGateway()->find($id, [], $options));
576        if ($this->hasWiths()) {
577            $this->getWithRelationships(false);
578        }
579        return ($asArray) ? $this->toArray() : $this;
580    }
581
582    /**
583     * Get one method
584     *
585     * @param  ?array $columns
586     * @param  ?array $options
587     * @param  bool   $asArray
588     * @return static|array
589     */
590    public function getOne(?array $columns = null, ?array $options = null, bool $asArray = false): Record|array|static
591    {
592        if ($options === null) {
593            $options = ['limit' => 1];
594        } else {
595            $options['limit'] = 1;
596        }
597
598        $expressions = null;
599        $params      = null;
600        $select      = $options['select'] ?? null;
601
602        if ($columns !== null) {
603            $db            = Db::getDb($this->getFullTable());
604            $sql           = $db->createSql();
605            ['expressions' => $expressions, 'params' => $params] =
606                Sql\Parser\Expression::parseShorthand($columns, $sql->getPlaceholder());
607        }
608
609        $rows = $this->getTableGateway()->select($select, $expressions, $params, $options);
610
611        if ($this->hasWiths() && !empty($rows)) {
612            $this->getWithRelationships();
613            $this->processWithRelationships($rows);
614        }
615
616        if (isset($rows[0])) {
617            $this->setColumns($rows[0]);
618        }
619
620        return ($asArray) ? $this->toArray() : $this;
621    }
622
623    /**
624     * Get by method
625     *
626     * @param  ?array $columns
627     * @param  ?array $options
628     * @param  bool   $asArray
629     * @return Collection|array
630     */
631    public function getBy(?array $columns = null, ?array $options = null, bool $asArray = false): Collection|array
632    {
633        $expressions = null;
634        $params      = null;
635        $select      = $options['select'] ?? null;
636
637        if ($columns !== null) {
638            $db            = Db::getDb($this->getFullTable());
639            $sql           = $db->createSql();
640            ['expressions' => $expressions, 'params' => $params] =
641                Sql\Parser\Expression::parseShorthand($columns, $sql->getPlaceholder());
642        }
643
644        $rows = $this->getTableGateway()->select($select, $expressions, $params, $options);
645
646        foreach ($rows as $i => $row) {
647            $rows[$i] = $this->processRow($row);
648        }
649
650        if ($this->hasWiths() && !empty($rows)) {
651            $this->getWithRelationships();
652            $this->processWithRelationships($rows);
653        }
654
655        $collection = new Record\Collection($rows);
656        return ($asArray) ? $collection->toArray() : $collection;
657    }
658
659    /**
660     * Get in method
661     *
662     * @param  string $key
663     * @param  array  $values
664     * @param  ?array $columns
665     * @param  ?array $options
666     * @param  bool   $asArray
667     * @return array
668     */
669    public function getIn(string $key, array $values, array $columns = null, array $options = null, bool $asArray = false): array
670    {
671        $columns = ($columns !== null) ? array_merge([$key => $values], $columns) : [$key => $values];
672        $results = $this->getBy($columns, $options, $asArray);
673        $rows    = [];
674
675        foreach ($results as $row) {
676            if (isset($row[$key])) {
677                $rows[$row[$key]] = (($asArray) && ($row instanceof Record)) ? $row->toArray() : $row;
678            }
679        }
680
681        return $rows;
682    }
683
684    /**
685     * Get all method
686     *
687     * @param  ?array $options
688     * @param  bool   $asArray
689     * @return Collection|array
690     */
691    public function getAll(?array $options = null, bool $asArray = false): Collection|array
692    {
693        return $this->getBy(null, $options, $asArray);
694    }
695
696    /**
697     * Has one relationship
698     *
699     * @param  string $foreignTable
700     * @param  string $foreignKey
701     * @param  ?array $options
702     * @param  bool   $eager
703     * @return Record|Record\Relationships\HasOne
704     */
705    public function hasOne(string $foreignTable, string $foreignKey, ?array $options = null, bool $eager = false): Record|Record\Relationships\HasOne
706    {
707        $relationship = new Record\Relationships\HasOne($this, $foreignTable, $foreignKey, $options);
708        if (!empty($this->withChildren)) {
709            $relationship->setChildRelationships($this->withChildren);
710        }
711        return ($eager) ? $relationship : $relationship->getChild($options);
712    }
713
714    /**
715     * Has one of relationship
716     *
717     * @param  string $foreignTable
718     * @param  string $foreignKey
719     * @param  ?array $options
720     * @param  bool   $eager
721     * @return Record|Record\Relationships\HasOneOf
722     */
723    public function hasOneOf(string $foreignTable, string $foreignKey, ?array $options = null, bool $eager = false): Record|Record\Relationships\HasOneOf
724    {
725        $relationship = new Record\Relationships\HasOneOf($this, $foreignTable, $foreignKey, $options);
726        if (!empty($this->withChildren)) {
727            $relationship->setChildRelationships($this->withChildren);
728        }
729        return ($eager) ? $relationship : $relationship->getChild();
730    }
731
732    /**
733     * Has many relationship
734     *
735     * @param  string $foreignTable
736     * @param  string $foreignKey
737     * @param  ?array $options
738     * @param  bool   $eager
739     * @return Collection|Record\Relationships\HasMany
740     */
741    public function hasMany(string $foreignTable, string $foreignKey, ?array $options = null, bool $eager = false): Collection|Record\Relationships\HasMany
742    {
743        $relationship = new Record\Relationships\HasMany($this, $foreignTable, $foreignKey, $options);
744        if (!empty($this->withChildren)) {
745            $relationship->setChildRelationships($this->withChildren);
746        }
747        return ($eager) ? $relationship : $relationship->getChildren($options);
748    }
749
750    /**
751     * Belongs to relationship
752     *
753     * @param  string $foreignTable
754     * @param  string $foreignKey
755     * @param  ?array $options
756     * @param  bool   $eager
757     * @return Record|Record\Relationships\BelongsTo
758     */
759    public function belongsTo(string $foreignTable, string $foreignKey, ?array $options = null, bool $eager = false): Record|Record\Relationships\BelongsTo
760    {
761        $relationship = new Record\Relationships\BelongsTo($this, $foreignTable, $foreignKey, $options);
762        if (!empty($this->withChildren)) {
763            $relationship->setChildRelationships($this->withChildren);
764        }
765        return ($eager) ? $relationship : $relationship->getParent($options);
766    }
767
768    /**
769     * Increment the record column and save
770     *
771     * @param  string $column
772     * @param  int    $amount
773     * @return void
774     */
775    public function increment(string $column, int $amount = 1): void
776    {
777        $this->{$column} += (int)$amount;
778        $this->save();
779    }
780
781    /**
782     * Decrement the record column and save
783     *
784     * @param  string $column
785     * @param  int    $amount
786     * @return void
787     */
788    public function decrement(string $column, int $amount = 1): void
789    {
790        $this->{$column} -= (int)$amount;
791        $this->save();
792    }
793
794    /**
795     * Replicate the record
796     *
797     * @param  array $replace
798     * @return static
799     */
800    public function replicate(array $replace = []): static
801    {
802        $fields = $this->toArray();
803
804        foreach ($this->primaryKeys as $key) {
805            if (isset($fields[$key])) {
806                unset($fields[$key]);
807            }
808        }
809
810        if (!empty($replace)) {
811            foreach ($replace as $key => $value) {
812                if (array_key_exists($key, $fields)) {
813                    $fields[$key] = $value;
814                }
815            }
816        }
817
818        $newRecord = new static($fields);
819        $newRecord->save();
820
821        return $newRecord;
822    }
823
824    /**
825     * Copy the record (alias to replicate)
826     *
827     * @param  array $replace
828     * @return static
829     */
830    public function copy(array $replace = []): static
831    {
832        return $this->replicate($replace);
833    }
834
835    /**
836     * Check if row is dirty
837     *
838     * @return bool
839     */
840    public function isDirty(): bool
841    {
842        return $this->rowGateway->isDirty();
843    }
844
845    /**
846     * Get row's dirty columns
847     *
848     * @return array
849     */
850    public function getDirty(): array
851    {
852        return $this->rowGateway->getDirty();
853    }
854
855    /**
856     * Reset row's dirty columns
857     *
858     * @return void
859     */
860    public function resetDirty(): void
861    {
862        $this->rowGateway->resetDirty();
863    }
864
865    /**
866     * Save or update the record
867     *
868     * @param  ?array $columns
869     * @param  bool   $commit
870     * @throws \Exception
871     * @return void
872     */
873    public function save(array $columns = null, bool $commit = true): void
874    {
875        try {
876            // Save or update the record
877            if ($columns === null) {
878                if ($this->isNew) {
879                    $this->rowGateway->save();
880                    $this->isNew = false;
881                } else {
882                    $this->rowGateway->update();
883                    $record = $this->getById($this->rowGateway->getPrimaryValues());
884                    if (isset($record[0])) {
885                        $this->setColumns($record[0]);
886                    }
887                }
888                // Else, save multiple rows
889            } else {
890                if (isset($columns[0])) {
891                    $this->tableGateway->insertRows($columns);
892                } else {
893                    $this->tableGateway->insert($columns);
894                }
895            }
896            if (($this->isTransaction()) && ($commit)) {
897                $this->commitTransaction();
898            }
899        } catch (\Exception $e) {
900            if (($this->isTransaction()) && ($commit)) {
901                $this->rollbackTransaction();
902            }
903            throw $e;
904        }
905    }
906
907    /**
908     * Delete the record
909     *
910     * @param  ?array $columns
911     * @param  bool   $commit
912     * @return void
913     */
914    public function delete(array $columns = null, bool $commit = true): void
915    {
916        try {
917            // Delete the record
918            if ($columns === null) {
919                $this->rowGateway->delete();
920            // Delete multiple rows
921            } else {
922                $expressions = null;
923                $params      = [];
924
925                if ($columns !== null) {
926                    $db            = Db::getDb($this->getFullTable());
927                    $sql           = $db->createSql();
928                    ['expressions' => $expressions, 'params' => $params] =
929                        Sql\Parser\Expression::parseShorthand($columns, $sql->getPlaceholder());
930                }
931
932                $this->tableGateway->delete($expressions, $params);
933            }
934
935            $this->setRows();
936            $this->setColumns();
937
938            if (($this->isTransaction()) && ($commit)) {
939                $this->commitTransaction();
940            }
941        } catch (\Exception $e) {
942            if (($this->isTransaction()) && ($commit)) {
943                $this->rollbackTransaction();
944            }
945            throw $e;
946        }
947
948    }
949
950    /**
951     * Call static method for 'findWhere'
952     *
953     *     $users = Users::findWhereUsername($value);
954     *
955     *     $users = Users::findWhereEquals($column, $value);
956     *     $users = Users::findWhereNotEquals($column, $value);
957     *     $users = Users::findWhereGreaterThan($column, $value);
958     *     $users = Users::findWhereGreaterThanOrEqual($column, $value);
959     *     $users = Users::findWhereLessThan($column, $value);
960     *     $users = Users::findWhereLessThanOrEqual($column, $value);
961     *
962     *     $users = Users::findWhereLike($column, $value);
963     *     $users = Users::findWhereNotLike($column, $value);
964     *
965     *     $users = Users::findWhereIn($column, $values);
966     *     $users = Users::findWhereNotIn($column, $values);
967     *
968     *     $users = Users::findWhereBetween($column, $values);
969     *     $users = Users::findWhereNotBetween($column, $values);
970     *
971     *     $users = Users::findWhereNull($column);
972     *     $users = Users::findWhereNotNull($column);
973     *
974     * @param  string $name
975     * @param  array  $arguments
976     * @return Collection|array|null
977     */
978    public static function __callStatic(string $name, array $arguments): Collection|array|null
979    {
980        $columns    = null;
981        $options    = null;
982        $asArray    = false;
983        $conditions = [
984            'Equals', 'NotEquals', 'GreaterThan', 'GreaterThanOrEqual', 'LessThan', 'LessThanOrEqual',
985            'Like', 'NotLike', 'In', 'NotIn', 'Between', 'NotBetween', 'Null', 'NotNull'
986        ];
987
988        if (str_starts_with($name, 'findWhere')) {
989            if (in_array(substr($name, 9), $conditions)) {
990                $condition = substr($name, 9);
991                $column    = $arguments[0];
992
993                if (str_contains($condition, 'Null')) {
994                    $value     = null;
995                    $options   = $arguments[1] ?? null;
996                    $asArray   = $arguments[2] ?? false;
997                } else {
998                    $value     = $arguments[1];
999                    $options   = $arguments[2] ?? null;
1000                    $asArray   = $arguments[3] ?? false;
1001                }
1002
1003                switch ($condition) {
1004                    case 'Equals':
1005                    case 'In':
1006                    case 'Between':
1007                    case 'Null':
1008                        $columns = [$column => $value];
1009                        break;
1010                    case 'NotEquals':
1011                        $columns = [$column . '!=' => $value];
1012                        break;
1013                    case 'GreaterThan':
1014                        $columns = [$column . '>' => $value];
1015                        break;
1016                    case 'GreaterThanOrEqual':
1017                        $columns = [$column . '>=' => $value];
1018                        break;
1019                    case 'LessThan':
1020                        $columns = [$column . '<' => $value];
1021                        break;
1022                    case 'LessThanOrEqual':
1023                        $columns = [$column . '<=' => $value];
1024                        break;
1025                    case 'Like':
1026                        if (str_starts_with($value, '%')) {
1027                            $column = '%' . $column;
1028                            $value  = substr($value, 1);
1029                        }
1030                        if (str_ends_with($value, '%')) {
1031                            $column .= '%';
1032                            $value   = substr($value, 0, -1);
1033                        }
1034                        $columns = [$column => $value];
1035                        break;
1036                    case 'NotLike':
1037                        if (str_starts_with($value, '%')) {
1038                            $column = '-%' . $column;
1039                            $value  = substr($value, 1);
1040                        }
1041                        if (str_ends_with($value, '%')) {
1042                            $column .= '%-';
1043                            $value   = substr($value, 0, -1);
1044                        }
1045                        $columns = [$column => $value];
1046                        break;
1047                    case 'NotIn':
1048                    case 'NotBetween':
1049                    case 'NotNull':
1050                        $columns = [$column . '-' => $value];
1051                        break;
1052                }
1053            } else {
1054                $column  = Sql\Parser\Table::parse(substr($name, 9));
1055                $value   = $arguments[0] ?? null;
1056                $options = $arguments[1] ?? null;
1057                $asArray = $arguments[2] ?? false;
1058
1059                if ($value !== null) {
1060                    $columns = [$column => $value];
1061                }
1062            }
1063        }
1064
1065        return ($columns !== null) ? static::findBy($columns, $options, $asArray) : null;
1066    }
1067
1068}