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