Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.58% |
173 / 191 |
|
92.31% |
24 / 26 |
CRAP | |
0.00% |
0 / 1 |
Row | |
90.58% |
173 / 191 |
|
92.31% |
24 / 26 |
101.40 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setPrimaryKeys | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getPrimaryKeys | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setPrimaryValues | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getPrimaryValues | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doesPrimaryCountMatch | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setColumns | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
getColumns | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDirty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDirty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
resetDirty | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
find | |
79.55% |
35 / 44 |
|
0.00% |
0 / 1 |
26.14 | |||
save | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
8 | |||
update | |
81.25% |
39 / 48 |
|
0.00% |
0 / 1 |
22.64 | |||
delete | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
7 | |||
count | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIterator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
toArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__set | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 | |||
__get | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
__isset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__unset | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
offsetExists | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
offsetGet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
offsetSet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
offsetUnset | |
100.00% |
1 / 1 |
|
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 | */ |
14 | namespace Pop\Db\Gateway; |
15 | |
16 | use Pop\Db\Db; |
17 | use 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 | */ |
29 | class 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 | } |