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