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