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