Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
198 / 198
100.00% covered (success)
100.00%
50 / 50
CRAP
100.00% covered (success)
100.00%
1 / 1
AbstractRecord
100.00% covered (success)
100.00%
198 / 198
100.00% covered (success)
100.00%
50 / 50
114
100.00% covered (success)
100.00%
1 / 1
 setTable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setTableFromClassName
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 setPrefix
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setPrimaryKeys
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 startTransaction
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isTransaction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 commitTransaction
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 rollbackTransaction
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getTable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFullTable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrimaryKeys
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrimaryValues
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getRowGateway
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTableGateway
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIterator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRows
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rows
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 countRows
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRows
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setColumns
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 toColumnsArray
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 isFillable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 fill
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 setRows
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 processRows
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 processRow
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 addWith
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 hasWith
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasWiths
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWiths
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWithRelationships
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 processWithRelationships
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
1 / 1
24
 setRelationship
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRelationship
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRelationship
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelationships
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRelationships
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 latest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 oldest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 __set
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 __isset
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 __unset
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 offsetExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetUnset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Db\Record;
16
17use Pop\Db\Db;
18use Pop\Db\Gateway;
19use Pop\Db\Sql\Parser;
20use ArrayIterator;
21
22/**
23 * Abstract record class
24 *
25 * @category   Pop
26 * @package    Pop\Db
27 * @author     Nick Sagona, III <nick@popphp.org>
28 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
29 * @license    https://www.popphp.org/license     New BSD License
30 * @version    7.0.0
31 */
32abstract class AbstractRecord implements \ArrayAccess, \Countable, \IteratorAggregate
33{
34
35    /**
36     * Table name
37     * @var ?string
38     */
39    protected ?string $table = null;
40
41    /**
42     * Table prefix
43     * @var ?string
44     */
45    protected ?string $prefix = null;
46
47    /**
48     * Primary keys
49     * @var array
50     */
51    protected array $primaryKeys = ['id'];
52
53    /**
54     * Fillable columns (mass-assignment allowlist) - if non-empty, only these columns are
55     * settable via fill(); $guarded is ignored when this is non-empty
56     * @var array
57     */
58    protected array $fillable = [];
59
60    /**
61     * Guarded columns (mass-assignment denylist) - ignored if $fillable is non-empty
62     * @var array
63     */
64    protected array $guarded = [];
65
66    /**
67     * Row gateway
68     * @var ?Gateway\Row
69     */
70    protected ?Gateway\Row $rowGateway = null;
71
72    /**
73     * Table gateway
74     * @var ?Gateway\Table
75     */
76    protected ?Gateway\Table $tableGateway = null;
77
78    /**
79     * Is new record flag
80     * @var bool
81     */
82    protected bool $isNew = false;
83
84    /**
85     * Is transaction flag
86     * @var bool
87     */
88    protected bool $isTransaction = false;
89
90    /**
91     * With relationships
92     * @var array
93     */
94    protected array $with = [];
95
96    /**
97     * With relationship options
98     * @var array
99     */
100    protected array $withOptions = [];
101
102    /**
103     * With relationship children (each entry is an array of dotted child paths for that relationship)
104     * @var array
105     */
106    protected array $withChildren = [];
107
108    /**
109     * Current with index
110     * @var int
111     */
112    protected int $currentWithIndex = 0;
113
114    /**
115     * Relationships
116     * @var array
117     */
118    protected array $relationships = [];
119
120    /**
121     * Relationship sort-by field
122     * @var string
123     */
124    protected string $relationshipSortBy = 'id';
125
126    /**
127     * Relationship latest flag
128     * @var bool
129     */
130    protected bool $latest = false;
131
132    /**
133     * Relationship oldest flag
134     * @var bool
135     */
136    protected bool $oldest = false;
137
138    /**
139     * Set the table
140     *
141     * @param  string $table
142     * @return AbstractRecord
143     */
144    public function setTable(string $table): AbstractRecord
145    {
146        $this->table = $table;
147        return $this;
148    }
149
150    /**
151     * Set the table from a class name
152     *
153     * @param  ?string $class
154     * @return AbstractRecord
155     */
156    public function setTableFromClassName(?string $class = null): AbstractRecord
157    {
158        if ($class === null) {
159            $class = get_class($this);
160        }
161
162        if (str_contains($class, '_')) {
163            $cls = substr($class, (strrpos($class, '_') + 1));
164        } else if (str_contains($class, '\\')) {
165            $cls = substr($class, (strrpos($class, '\\') + 1));
166        } else {
167            $cls = $class;
168        }
169
170        return $this->setTable(Parser\Table::parse($cls));
171    }
172
173    /**
174     * Set the table prefix
175     *
176     * @param  string $prefix
177     * @return AbstractRecord
178     */
179    public function setPrefix(string $prefix): AbstractRecord
180    {
181        $this->prefix = $prefix;
182        return $this;
183    }
184
185    /**
186     * Set the primary keys
187     *
188     * @param  array $keys
189     * @return AbstractRecord
190     */
191    public function setPrimaryKeys(array $keys): AbstractRecord
192    {
193        $this->primaryKeys = $keys;
194        return $this;
195    }
196
197    /**
198     * Start transaction
199     *
200     * @return AbstractRecord
201     */
202    public function startTransaction(): AbstractRecord
203    {
204        $class = get_called_class();
205        if (Db::hasDb($class)) {
206            Db::db($class)->beginTransaction();
207        }
208        $this->isTransaction = true;
209        return $this;
210    }
211
212    /**
213     * Is transaction
214     *
215     * @return bool
216     */
217    public function isTransaction(): bool
218    {
219        return $this->isTransaction;
220    }
221
222    /**
223     * Commit transaction
224     *
225     * @throws \Pop\Db\Exception
226     * @return AbstractRecord
227     */
228    public function commitTransaction(): AbstractRecord
229    {
230        $class = get_called_class();
231        if (($this->isTransaction) && (Db::hasDb($class))) {
232            Db::db($class)->commit();
233        }
234        $this->isTransaction = false;
235        return $this;
236    }
237
238    /**
239     * Rollback transaction
240     *
241     * @throws \Pop\Db\Exception
242     * @return AbstractRecord
243     */
244    public function rollbackTransaction(): AbstractRecord
245    {
246        $class = get_called_class();
247        if (($this->isTransaction) && (Db::hasDb($class))) {
248            Db::db($class)->rollback();
249        }
250        $this->isTransaction = false;
251        return $this;
252    }
253
254    /**
255     * Get the table
256     *
257     * @return ?string
258     */
259    public function getTable(): ?string
260    {
261        return $this->table;
262    }
263
264    /**
265     * Get the full table name (prefix + table)
266     *
267     * @return string
268     */
269    public function getFullTable(): string
270    {
271        return $this->prefix . $this->table;
272    }
273
274    /**
275     * Get the table prefix
276     *
277     * @return ?string
278     */
279    public function getPrefix(): ?string
280    {
281        return $this->prefix;
282    }
283
284    /**
285     * Get the primary keys
286     *
287     * @return array
288     */
289    public function getPrimaryKeys(): array
290    {
291        return $this->primaryKeys;
292    }
293
294    /**
295     * Get the primary values
296     *
297     * @return array
298     */
299    public function getPrimaryValues(): array
300    {
301        return ($this->rowGateway !== null) ?
302            array_intersect_key($this->rowGateway->getColumns(), array_flip($this->primaryKeys)) : [];
303    }
304
305    /**
306     * Get the row gateway
307     *
308     * @return ?Gateway\Row
309     */
310    public function getRowGateway(): ?Gateway\Row
311    {
312        return $this->rowGateway;
313    }
314
315    /**
316     * Get the table gateway
317     *
318     * @return ?Gateway\Table
319     */
320    public function getTableGateway(): ?Gateway\Table
321    {
322        return $this->tableGateway;
323    }
324
325    /**
326     * Get column values as array
327     *
328     * @return array
329     */
330    public function toArray(): array
331    {
332        $columns = $this->rowGateway->getColumns();
333
334        if ($this->hasRelationships()) {
335            $relationships = $this->getRelationships();
336            foreach ($relationships as $name => $relationship) {
337                $columns[$name] = (is_object($relationship) && method_exists($relationship, 'toArray')) ?
338                    $relationship->toArray() : $relationship;
339            }
340        }
341
342        return $columns;
343    }
344
345    /**
346     * Method to get count of fields in row gateway
347     *
348     * @return int
349     */
350    public function count(): int
351    {
352        return $this->rowGateway->count();
353    }
354
355    /**
356     * Method to iterate over the columns
357     *
358     * @return ArrayIterator
359     */
360    public function getIterator(): ArrayIterator
361    {
362        return $this->rowGateway->getIterator();
363    }
364
365    /**
366     * Get the rows
367     *
368     * @return Collection
369     */
370    public function getRows(): Collection
371    {
372        return new Collection($this->tableGateway->getRows());
373    }
374
375    /**
376     * Get the rows (alias method)
377     *
378     * @return Collection
379     */
380    public function rows(): Collection
381    {
382        return $this->getRows();
383    }
384
385    /**
386     * Get the count of rows returned in the result
387     *
388     * @return int
389     */
390    public function countRows(): int
391    {
392        return $this->tableGateway->getNumberOfRows();
393    }
394
395    /**
396     * Determine if the result has rows
397     *
398     * @return bool
399     */
400    public function hasRows(): bool
401    {
402        return ($this->tableGateway->getNumberOfRows() > 0);
403    }
404
405    /**
406     * Set all the table column values at once
407     *
408     * @param  mixed  $columns
409     * @throws Exception
410     * @return AbstractRecord
411     */
412    public function setColumns(mixed $columns = null): AbstractRecord
413    {
414        if ($columns !== null) {
415            $this->rowGateway->setColumns($this->toColumnsArray($columns));
416        }
417
418        return $this;
419    }
420
421    /**
422     * Normalize a columns argument (array, ArrayObject, AbstractRecord, or any ArrayAccess
423     * with a toArray() method) into a plain array
424     *
425     * @param  mixed $columns
426     * @throws Exception
427     * @return array
428     */
429    protected function toColumnsArray(mixed $columns): array
430    {
431        if (is_array($columns) || ($columns instanceof \ArrayObject)) {
432            return (array)$columns;
433        } else if ($columns instanceof AbstractRecord) {
434            return $columns->toArray();
435        } else if (($columns instanceof \ArrayAccess) && method_exists($columns, 'toArray')) {
436            return $columns->toArray();
437        } else {
438            throw new Exception('The parameter passed must be an arrayable object.');
439        }
440    }
441
442    /**
443     * Determine if a column is mass-assignable via fill()
444     *
445     * @param  string $column
446     * @return bool
447     */
448    public function isFillable(string $column): bool
449    {
450        if (!empty($this->fillable)) {
451            return in_array($column, $this->fillable, true);
452        } else if (!empty($this->guarded)) {
453            return !in_array($column, $this->guarded, true);
454        }
455
456        return true;
457    }
458
459    /**
460     * Mass-assign columns, filtered through $fillable/$guarded via isFillable()
461     *
462     * @param  mixed $columns
463     * @throws Exception
464     * @return AbstractRecord
465     */
466    public function fill(mixed $columns = null): AbstractRecord
467    {
468        if ($columns !== null) {
469            $filtered = array_filter(
470                $this->toColumnsArray($columns),
471                fn($value, $key) => $this->isFillable((string)$key),
472                ARRAY_FILTER_USE_BOTH
473            );
474            $this->setColumns($filtered);
475        }
476
477        return $this;
478    }
479
480    /**
481     * Set all the table rows at once
482     *
483     * @param  ?array $rows
484     * @param  bool   $toArray
485     * @return AbstractRecord
486     */
487    public function setRows(?array $rows = null, bool|array $toArray = false): AbstractRecord
488    {
489        $this->rowGateway->setColumns();
490        $this->tableGateway->setRows();
491
492        if ($rows !== null) {
493            $this->rowGateway->setColumns(((isset($rows[0])) ? (array)$rows[0] : []));
494            foreach ($rows as $i => $row) {
495                $rows[$i] = $this->processRow($row, $toArray);
496            }
497            $this->tableGateway->setRows($rows);
498        }
499
500        return $this;
501    }
502
503    /**
504     * Process table rows
505     *
506     * @param  array $rows
507     * @param  bool  $toArray
508     * @return array
509     */
510    public function processRows(array $rows, bool|array $toArray = false): array
511    {
512        foreach ($rows as $i => $row) {
513            $rows[$i] = $this->processRow($row, $toArray);
514        }
515        return $rows;
516    }
517
518    /**
519     * Process a table row
520     *
521     * @param  array $row
522     * @param  bool  $toArray
523     * @return mixed
524     */
525    public function processRow(array $row, bool|array $toArray = false): mixed
526    {
527        if ($toArray !== false) {
528            return (is_array($toArray)) ? (new Collection($row))->toArray($toArray): $row;
529        } else {
530            $record = new static();
531            $record->setColumns($row);
532            return $record;
533        }
534    }
535
536    /**
537     * Set with relationships
538     *
539     * @param  string $name
540     * @param  ?array $options
541     * @return AbstractRecord
542     */
543    public function addWith(string $name, ?array $options = null): AbstractRecord
544    {
545        $children = [];
546        if (str_contains($name, '.')) {
547            $names    = explode('.', $name);
548            $name     = array_shift($names);
549            $children = [implode('.', $names)];
550        }
551
552        $existingIndex = array_search($name, $this->with, true);
553
554        if ($existingIndex !== false) {
555            $this->withChildren[$existingIndex] = array_values(array_unique(
556                array_merge($this->withChildren[$existingIndex], $children)
557            ));
558            if ($options !== null) {
559                $this->withOptions[$existingIndex] = $options;
560            }
561        } else {
562            $this->with[]         = $name;
563            $this->withOptions[]  = $options;
564            $this->withChildren[] = $children;
565        }
566
567        return $this;
568    }
569
570    /**
571     * Determine if there is specific with relationship
572     *
573     * @param  string $name
574     * @return bool
575     */
576    public function hasWith(string $name): bool
577    {
578        return in_array($name, $this->with, true);
579    }
580
581    /**
582     * Determine if there are with relationships
583     *
584     * @return bool
585     */
586    public function hasWiths(): bool
587    {
588        return (count($this->with) > 0);
589    }
590
591    /**
592     * Get with relationships
593     *
594     * @return array
595     */
596    public function getWiths(): array
597    {
598        return $this->with;
599    }
600
601    /**
602     * Get with relationships
603     *
604     * @param  bool $eager
605     * @return AbstractRecord
606     */
607    public function getWithRelationships(bool $eager = true): AbstractRecord
608    {
609        foreach ($this->with as $i => $name) {
610            $options = (isset($this->withOptions[$i])) ? $this->withOptions[$i] : null;
611
612            $this->currentWithIndex = $i;
613            if (method_exists($this, $name)) {
614                $this->relationships[$name] = $this->{$name}($options, $eager);
615            }
616        }
617
618        return $this;
619    }
620
621    /**
622     * Process with relationships
623     *
624     * @param  array $rows
625     * @return AbstractRecord
626     */
627    public function processWithRelationships(array $rows): AbstractRecord
628    {
629        foreach ($this->relationships as $name => $relationship) {
630            $withIds = [];
631            if (($relationship instanceof \Pop\Db\Record\Relationships\HasOneOf) ||
632                ($relationship instanceof \Pop\Db\Record\Relationships\BelongsTo)) {
633                $primaryKey = $relationship->getForeignKey();
634                if (is_array($primaryKey)) {
635                    $seen = [];
636                    foreach ($rows as $i => $row) {
637                        $tuple = Relationships\AbstractRelationship::tupleFor($row, $primaryKey);
638                        if ($tuple === null) {
639                            continue;
640                        }
641                        $tupleKey = Relationships\AbstractRelationship::buildCompositeKey($tuple);
642                        if (!isset($seen[$tupleKey])) {
643                            $seen[$tupleKey] = true;
644                            $withIds[] = $tuple;
645                        }
646                    }
647                } else {
648                    $seen = [];
649                    foreach ($rows as $i => $row) {
650                        if (isset($row[$primaryKey]) && !isset($seen[$row[$primaryKey]])) {
651                            $seen[$row[$primaryKey]] = true;
652                            $withIds[] = $row[$primaryKey];
653                        }
654                    }
655                }
656                $results = !empty($withIds) ? $relationship->getEagerRelationships($withIds) : [];
657            } else {
658                $primaryKey = $this->getPrimaryKeys();
659                if (count($primaryKey) == 1) {
660                    $primaryKey = reset($primaryKey);
661                    $seen       = [];
662                    foreach ($rows as $i => $row) {
663                        $primaryValues = $rows[$i]->getPrimaryValues();
664                        if (count($primaryValues) == 1) {
665                            $withId = reset($primaryValues);
666                            if (!isset($seen[$withId])) {
667                                $seen[$withId] = true;
668                                $withIds[] = $withId;
669                            }
670                        }
671                    }
672                } else {
673                    foreach ($rows as $i => $row) {
674                        $tuple = Relationships\AbstractRelationship::tupleFor($row, $primaryKey);
675                        if ($tuple !== null) {
676                            $withIds[] = $tuple;
677                        }
678                    }
679                }
680                $results = !empty($withIds) ? $relationship->getEagerRelationships($withIds) : [];
681            }
682            foreach ($rows as $i => $row) {
683                if (is_array($primaryKey)) {
684                    $tuple       = Relationships\AbstractRelationship::tupleFor($row, $primaryKey);
685                    $lookupValue = ($tuple !== null) ?
686                        Relationships\AbstractRelationship::buildCompositeKey($tuple) : null;
687                } else {
688                    $lookupValue = $row[$primaryKey] ?? null;
689                }
690                if ($lookupValue !== null && isset($results[$lookupValue])) {
691                    $row->setRelationship($name, $results[$lookupValue]);
692                } else {
693                    $row->setRelationship($name, $relationship->getEmptyRelationshipValue());
694                }
695            }
696        }
697
698        return $this;
699    }
700
701    /**
702     * Set relationship
703     *
704     * @param  string $name
705     * @param  mixed  $relationship
706     * @return AbstractRecord
707     */
708    public function setRelationship(string $name, mixed $relationship): AbstractRecord
709    {
710        $this->relationships[$name] = $relationship;
711        return $this;
712    }
713
714    /**
715     * Get relationship
716     *
717     * @param  string $name
718     * @return mixed
719     */
720    public function getRelationship(string $name): mixed
721    {
722        return $this->relationships[$name] ?? null;
723    }
724
725    /**
726     * Has relationship
727     *
728     * @param  string $name
729     * @return bool
730     */
731    public function hasRelationship(string $name): bool
732    {
733        return (array_key_exists($name, $this->relationships));
734    }
735
736    /**
737     * Get relationships
738     *
739     * @return array
740     */
741    public function getRelationships(): array
742    {
743        return $this->relationships;
744    }
745
746    /**
747     * Get relationships
748     *
749     * @return bool
750     */
751    public function hasRelationships(): bool
752    {
753        return (count($this->relationships) > 0);
754    }
755
756    /**
757     * Method to set latest flag for relationships
758     *
759     * @param  string $sortBy
760     * @return static
761     */
762    public function latest(string $sortBy = 'id'): static
763    {
764        $this->relationshipSortBy = $sortBy;
765        $this->latest             = true;
766        $this->oldest             = false;
767
768        return $this;
769    }
770
771    /**
772     * Method to set oldest flag for relationships
773     *
774     * @param  string $sortBy
775     * @return static
776     */
777    public function oldest(string $sortBy = 'id'): static
778    {
779        $this->relationshipSortBy = $sortBy;
780        $this->latest             = false;
781        $this->oldest             = true;
782
783        return $this;
784    }
785
786    /**
787     * Magic method to set the property to the value of $this->rowGateway[$name]
788     *
789     * @param  string $name
790     * @param  mixed $value
791     * @return void
792     */
793    public function __set(string $name, mixed $value)
794    {
795        $this->rowGateway[$name] = $value;
796    }
797
798    /**
799     * Magic method to return the value of $this->rowGateway[$name]
800     *
801     * @param  string $name
802     * @return mixed
803     */
804    public function __get(string $name): mixed
805    {
806        $result = null;
807
808        if (array_key_exists($name, $this->relationships)) {
809            $result = $this->relationships[$name];
810        } else if (isset($this->rowGateway[$name])) {
811            $result = $this->rowGateway[$name];
812        } else if (method_exists($this, $name)) {
813            $result = $this->{$name}();
814        }
815
816        return $result;
817    }
818
819    /**
820     * Magic method to return the isset value of $this->rowGateway[$name]
821     *
822     * @param  string $name
823     * @return bool
824     */
825    public function __isset(string $name): bool
826    {
827        if (array_key_exists($name, $this->relationships)) {
828            return true;
829        } else if (isset($this->rowGateway[$name])) {
830            return true;
831        } else if (method_exists($this, $name)) {
832            return true;
833        } else {
834            return false;
835        }
836    }
837
838    /**
839     * Magic method to unset $this->rowGateway[$name]
840     *
841     * @param  string $name
842     * @return void
843     */
844    public function __unset(string $name): void
845    {
846        if (isset($this->rowGateway[$name])) {
847            unset($this->rowGateway[$name]);
848        }
849    }
850
851    /**
852     * ArrayAccess offsetExists
853     *
854     * @param  mixed $offset
855     * @return bool
856     */
857    public function offsetExists(mixed $offset): bool
858    {
859        return $this->__isset($offset);
860    }
861
862    /**
863     * ArrayAccess offsetGet
864     *
865     * @param  mixed $offset
866     * @return mixed
867     */
868    public function offsetGet(mixed $offset): mixed
869    {
870        return $this->__get($offset);
871    }
872
873    /**
874     * ArrayAccess offsetSet
875     *
876     * @param  mixed $offset
877     * @param  mixed $value
878     * @return void
879     */
880    public function offsetSet(mixed $offset, mixed $value): void
881    {
882        $this->__set($offset, $value);
883    }
884
885    /**
886     * ArrayAccess offsetUnset
887     *
888     * @param  mixed $offset
889     * @return void
890     */
891    public function offsetUnset(mixed $offset): void
892    {
893        $this->__unset($offset);
894    }
895
896}