Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.31% covered (success)
95.31%
183 / 192
96.15% covered (success)
96.15%
25 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
Row
95.31% covered (success)
95.31%
183 / 192
96.15% covered (success)
96.15%
25 / 26
93
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setPrimaryKeys
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getPrimaryKeys
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPrimaryValues
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getPrimaryValues
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doesPrimaryCountMatch
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setColumns
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getColumns
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDirty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDirty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resetDirty
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 find
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
22
 save
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
8
 update
81.25% covered (success)
81.25%
39 / 48
0.00% covered (danger)
0.00%
0 / 1
21.38
 delete
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
7
 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
 toArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __set
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 __get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __unset
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 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\Gateway;
16
17use Pop\Db\Db;
18use ArrayIterator;
19
20/**
21 * Row gateway class
22 *
23 * @category   Pop
24 * @package    Pop\Db
25 * @author     Nick Sagona, III <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    7.0.0
29 */
30class Row extends AbstractGateway implements \ArrayAccess, \Countable, \IteratorAggregate
31{
32
33    /**
34     * Primary keys
35     * @var array
36     */
37    protected array $primaryKeys = [];
38
39    /**
40     * Primary values
41     * @var array
42     */
43    protected array $primaryValues = [];
44
45    /**
46     * Row column values
47     * @var array
48     */
49    protected array $columns = [];
50
51    /**
52     * Row fields that have been changed
53     * @var array
54     */
55    protected array $dirty = [
56        'old' => [],
57        'new' => []
58    ];
59
60    /**
61     * Constructor
62     *
63     * Instantiate the row gateway object.
64     *
65     * @param  string $table
66     * @param  mixed  $primaryKeys
67     */
68    public function __construct(string $table, mixed $primaryKeys = null)
69    {
70        if ($primaryKeys !== null) {
71            $this->setPrimaryKeys($primaryKeys);
72        }
73        parent::__construct($table);
74    }
75
76    /**
77     * Set the primary keys
78     *
79     * @param  mixed $keys
80     * @return Row
81     */
82    public function setPrimaryKeys(mixed $keys): Row
83    {
84        $this->primaryKeys = (is_array($keys)) ? $keys : [$keys];
85        return $this;
86    }
87
88    /**
89     * Get the primary keys
90     *
91     * @return array
92     */
93    public function getPrimaryKeys(): array
94    {
95        return $this->primaryKeys;
96    }
97
98    /**
99     * Set the primary values
100     *
101     * @param  mixed $values
102     * @return Row
103     */
104    public function setPrimaryValues(mixed $values): Row
105    {
106        $this->primaryValues = (is_array($values)) ? $values : [$values];
107        return $this;
108    }
109
110    /**
111     * Get the primary values
112     *
113     * @return array
114     */
115    public function getPrimaryValues(): array
116    {
117        return $this->primaryValues;
118    }
119
120    /**
121     * Determine if number of primary keys and primary values match
122     *
123     * @throws Exception
124     * @return bool
125     */
126    public function doesPrimaryCountMatch(): bool
127    {
128        if (count($this->primaryKeys) != count($this->primaryValues)) {
129            throw new Exception('Error: The number of primary keys and primary values do not match.');
130        } else {
131            return true;
132        }
133    }
134
135    /**
136     * Set the columns
137     *
138     * @param  array $columns
139     * @return Row
140     */
141    public function setColumns(array $columns = []): Row
142    {
143        $this->columns = $columns;
144        if (count($this->primaryValues) == 0) {
145            foreach ($this->primaryKeys as $primaryKey) {
146                if (isset($this->columns[$primaryKey])) {
147                    $this->primaryValues[] = $this->columns[$primaryKey];
148                }
149            }
150        }
151
152        return $this;
153    }
154
155    /**
156     * Get the columns
157     *
158     * @return array
159     */
160    public function getColumns(): array
161    {
162        return $this->columns;
163    }
164
165    /**
166     * Check if row data is dirty
167     *
168     * @return bool
169     */
170    public function isDirty(): bool
171    {
172        return ($this->dirty['old'] !== $this->dirty['new']);
173    }
174
175    /**
176     * Get dirty columns
177     *
178     * @return array
179     */
180    public function getDirty(): array
181    {
182        return $this->dirty;
183    }
184
185    /**
186     * Reset dirty columns
187     *
188     * @return Row
189     */
190    public function resetDirty(): Row
191    {
192        $this->dirty['old'] = [];
193        $this->dirty['new'] = [];
194        return $this;
195    }
196
197    /**
198     * Find row by primary key values
199     *
200     * @param  mixed  $values
201     * @param  array  $selectColumns
202     * @param  ?array $options
203     * @throws Exception|\Pop\Db\Exception
204     * @return array
205     */
206    public function find(mixed $values, array $selectColumns = [], ?array $options = null): array
207    {
208        if (count($this->primaryKeys) == 0) {
209            throw new Exception('Error: The primary key(s) have not been set.');
210        }
211
212        $this->checkOptions($options);
213
214        $db  = Db::getDb($this->table);
215        $sql = $db->createSql();
216
217        $this->setPrimaryValues($values);
218        $this->doesPrimaryCountMatch();
219
220        if (!empty($selectColumns)) {
221            $select = [];
222            foreach ($selectColumns as $selectColumn) {
223                $select[] = $this->table . '.' . $selectColumn;
224            }
225        } else if (($options !== null) && !empty($options['select'])) {
226            $select = $options['select'];
227        } else {
228            $select = [$this->table . '.*'];
229        }
230
231        $sql->select($select)->from($this->table);
232
233        $params = [];
234
235        foreach ($this->primaryKeys as $i => $primaryKey) {
236            $placeholder = $sql->getPlaceholder();
237
238            if ($placeholder == ':') {
239                $placeholder .= $primaryKey;
240            } else if ($placeholder == '$') {
241                $placeholder .= ($i + 1);
242            }
243
244            if ($this->primaryValues[$i] === null) {
245                $sql->select()->where->isNull($this->table . '.' . $primaryKey);
246            } else {
247                $sql->select()->where->equalTo($this->table . '.' . $primaryKey, $placeholder);
248                $params[$primaryKey] = $this->primaryValues[$i];
249            }
250        }
251
252        if (($options !== null) && isset($options['offset'])) {
253            $sql->select()->offset((int)$options['offset']);
254        }
255
256        if (($options !== null) && isset($options['join'])) {
257            $joins = (is_array($options['join']) && isset($options['join']['table'])) ?
258                [$options['join']] : $options['join'];
259
260            foreach ($joins as $join) {
261                if (isset($join['type']) && method_exists($sql->select(), $join['type'])) {
262                    $joinMethod = $join['type'];
263                    $sql->select()->{$joinMethod}($join['table'], $join['columns']);
264                } else {
265                    $sql->select()->leftJoin($join['table'], $join['columns']);
266                }
267            }
268        }
269
270        $sql->select()->limit(1);
271
272        $db->prepare((string)$sql);
273        if (!empty($params)) {
274            $db->bindParams($params);
275        }
276        $db->execute();
277
278        $row = $db->fetch();
279
280        if (($row !== false) && is_array($row)) {
281            $this->columns = $row;
282        }
283
284        return $this->columns;
285    }
286
287    /**
288     * Save a new row in the table
289     *
290     * @param  array $columns
291     * @return Row
292     */
293    public function save(array $columns = []): Row
294    {
295        $db     = Db::getDb($this->table);
296        $sql    = $db->createSql();
297        $values = [];
298        $params = [];
299
300        if (!empty($columns)) {
301            $this->setColumns($columns);
302        }
303
304        $i = 1;
305        foreach ($this->columns as $column => $value) {
306            $placeholder = $sql->getPlaceholder();
307
308            if ($placeholder == ':') {
309                $placeholder .= $column;
310            } else if ($placeholder == '$') {
311                $placeholder .= $i;
312            }
313            $values[$column] = $placeholder;
314            $params[$column] = $value;
315            $i++;
316        }
317
318        $sql->insert($this->table)->values($values);
319
320        $db->prepare((string)$sql);
321        if (!empty($params)) {
322            $db->bindParams($params);
323        }
324        $db->execute();
325
326        // Set the new ID created by the insert
327        if ((count($this->primaryKeys) == 1) && !isset($this->columns[$this->primaryKeys[0]])) {
328            $this->columns[$this->primaryKeys[0]] = $db->getLastId();
329            $this->primaryValues[] = $this->columns[$this->primaryKeys[0]];
330        }
331
332        $this->dirty['old'] = [];
333        $this->dirty['new'] = $this->columns;
334
335        return $this;
336    }
337
338    /**
339     * Update an existing row in the table
340     *
341     * @throws Exception|\Pop\Db\Exception
342     * @return Row
343     */
344    public function update(): Row
345    {
346        $db     = Db::getDb($this->table);
347        $sql    = $db->createSql();
348        $values = [];
349        $params = [];
350
351        $oldKeys     = array_keys($this->dirty['old']);
352        $newKeys     = array_keys($this->dirty['new']);
353        $columnNames = ($oldKeys == $newKeys) ? $newKeys : [];
354
355        $i = 1;
356        foreach ($this->columns as $column => $value) {
357            if (!in_array($column, $this->primaryKeys) &&
358                (empty($columnNames) || in_array($column, $columnNames))) {
359                $placeholder = $sql->getPlaceholder();
360
361                if ($placeholder == ':') {
362                    $placeholder .= $column;
363                } else if ($placeholder == '$') {
364                    $placeholder .= $i;
365                }
366                $values[$column] = $placeholder;
367                $params[$column] = $value;
368                $i++;
369            }
370        }
371
372        $sql->update($this->table)->values($values);
373
374        foreach ($this->primaryKeys as $key => $primaryKey) {
375            $placeholder = $sql->getPlaceholder();
376
377            if ($placeholder == ':') {
378                $placeholder .= $primaryKey;
379            } else if ($placeholder == '$') {
380                $placeholder .= $i;
381            }
382
383            if (array_key_exists($key, $this->primaryValues)) {
384                if ($this->primaryValues[$key] === null) {
385                    $sql->update()->where->isNull($primaryKey);
386                } else {
387                    $sql->update()->where->equalTo($primaryKey, $placeholder);
388                }
389            }
390
391            if (array_key_exists($key, $this->primaryValues)) {
392                if ($this->primaryValues[$key] !== null) {
393                    $params[$this->primaryKeys[$key]] = $this->primaryValues[$key];
394                    $values[$this->primaryKeys[$key]] = $placeholder;
395                }
396            } else if (array_key_exists($this->primaryKeys[$key], $this->columns)) {
397                if ($this->primaryValues[$key] !== null) {
398                    if (str_starts_with($placeholder, ':')) {
399                        $params[$this->primaryKeys[$key]] = $this->columns[$this->primaryKeys[$key]];
400                        $values[$this->primaryKeys[$key]] = $placeholder;
401                    } else {
402                        $params[$key] = $this->columns[$this->primaryKeys[$key]];
403                        $values[$key] = $placeholder;
404                    }
405                }
406            } else {
407                throw new Exception("Error: The value of '" . $key . "' is not set");
408            }
409            $i++;
410        }
411
412        $db->prepare((string)$sql);
413        if (!empty($params)) {
414            $db->bindParams($params);
415        }
416        $db->execute();
417
418        return $this;
419    }
420
421    /**
422     * Delete row from the table using the primary key(s)
423     *
424     * @throws Exception|\Pop\Db\Exception
425     * @return Row
426     */
427    public function delete(): Row
428    {
429        if (count($this->primaryKeys) == 0) {
430            throw new Exception('Error: The primary key(s) have not been set.');
431        }
432
433        $db  = Db::getDb($this->table);
434        $sql = $db->createSql();
435
436        $this->doesPrimaryCountMatch();
437
438        $sql->delete($this->table);
439
440        $params = [];
441        foreach ($this->primaryKeys as $i => $primaryKey) {
442            $placeholder = $sql->getPlaceholder();
443
444            if ($placeholder == ':') {
445                $placeholder .= $primaryKey;
446            } else if ($placeholder == '$') {
447                $placeholder .= ($i + 1);
448            }
449            if ($this->primaryValues[$i] === null) {
450                $sql->delete()->where->isNull($primaryKey);
451            } else {
452                $sql->delete()->where->equalTo($primaryKey, $placeholder);
453                $params[$primaryKey] = $this->primaryValues[$i];
454            }
455        }
456
457        $db->prepare((string)$sql);
458        if (!empty($params)) {
459            $db->bindParams($params);
460        }
461        $db->execute();
462
463        $this->dirty['old'] = $this->columns;
464        $this->dirty['new'] = [];
465
466        $this->columns       = [];
467        $this->primaryValues = [];
468
469        return $this;
470    }
471
472    /**
473     * Method to get the count of items in the row
474     *
475     * @return int
476     */
477    public function count(): int
478    {
479        return count($this->columns);
480    }
481
482    /**
483     * Method to iterate over the columns
484     *
485     * @return ArrayIterator
486     */
487    public function getIterator(): ArrayIterator
488    {
489        return new ArrayIterator($this->columns);
490    }
491
492    /**
493     * Method to convert row gateway to an array
494     *
495     * @return array
496     */
497    public function toArray(): array
498    {
499        return $this->columns;
500    }
501
502    /**
503     * Magic method to set the property to the value of $this->columns[$name].
504     *
505     * @param  string $name
506     * @param  mixed $value
507     * @return void
508     */
509    public function __set(string $name, mixed $value): void
510    {
511        if (!isset($this->dirty['old'][$name])) {
512            if (array_key_exists($name, $this->columns) && ($value !== $this->columns[$name])) {
513                $this->dirty['old'][$name] = $this->columns[$name];
514                $this->dirty['new'][$name] = $value;
515            } else if (!isset($this->columns[$name]) && isset($value)) {
516                $this->dirty['old'][$name] = null;
517                $this->dirty['new'][$name] = $value;
518            }
519        }
520        $this->columns[$name] = $value;
521    }
522
523    /**
524     * Magic method to return the value of $this->columns[$name].
525     *
526     * @param  string $name
527     * @return mixed
528     */
529    public function __get(string $name): mixed
530    {
531        return (isset($this->columns[$name])) ? $this->columns[$name] : null;
532    }
533
534    /**
535     * Magic method to return the isset value of $this->columns[$name].
536     *
537     * @param  string $name
538     * @return bool
539     */
540    public function __isset(string $name): bool
541    {
542        return isset($this->columns[$name]);
543    }
544
545    /**
546     * Magic method to unset $this->columns[$name].
547     *
548     * @param  string $name
549     * @return void
550     */
551    public function __unset(string $name): void
552    {
553        if (isset($this->columns[$name])) {
554            if (!isset($this->dirty['old'][$name])) {
555                $this->dirty['old'][$name] = $this->columns[$name];
556                $this->dirty['new'][$name] = null;
557            }
558            unset($this->columns[$name]);
559        }
560    }
561
562    /**
563     * ArrayAccess offsetExists
564     *
565     * @param  mixed $offset
566     * @return bool
567     */
568    public function offsetExists(mixed $offset): bool
569    {
570        return $this->__isset($offset);
571    }
572
573    /**
574     * ArrayAccess offsetGet
575     *
576     * @param  mixed $offset
577     * @return mixed
578     */
579    public function offsetGet(mixed $offset): mixed
580    {
581        return $this->__get($offset);
582    }
583
584    /**
585     * ArrayAccess offsetSet
586     *
587     * @param  mixed $offset
588     * @param  mixed $value
589     * @return void
590     */
591    public function offsetSet(mixed $offset, mixed $value): void
592    {
593        $this->__set($offset, $value);
594    }
595
596    /**
597     * ArrayAccess offsetUnset
598     *
599     * @param  mixed $offset
600     * @return void
601     */
602    public function offsetUnset(mixed $offset): void
603    {
604        $this->__unset($offset);
605    }
606
607}