packages/ztd-query-pdo-adapter/src/Session/BufferedRow.php
1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Adapter\Pdo\Session;
6
7use PDO;
8use ReflectionException;
9use ReflectionObject;
10use stdClass;
11
12/**
13 * Shapes a row ZTD buffered into what a fetch mode asks for.
14 *
15 * A simulated statement never reaches the driver, so nothing shapes its rows
16 * the way PDO's own fetch modes would. The rows come back keyed by column; this
17 * turns one of them into the array, list or object the caller asked to fetch.
18 *
19 *
20 * @visibility ZtdQuery\Adapter\Pdo
21 */
22final class BufferedRow
23{
24 /**
25 * Answers the row in the shape a fetch mode asks for.
26 *
27 * A mode this does not know is read as PDO reads an unknown one: both ways
28 * at once, which is what PDO::FETCH_BOTH means.
29 *
30 * @template TValue
31 * @param array<string, TValue> $row Row as ZTD buffered it, keyed by column
32 * @param int $mode One of PDO::FETCH_*
33 *
34 * @return array<int|string, TValue>|TValue|stdClass|false The row or column selected by the fetch mode
35 */
36 public function inMode(array $row, int $mode): mixed
37 {
38 return match ($mode) {
39 PDO::FETCH_ASSOC, PDO::FETCH_NAMED => $row,
40 PDO::FETCH_NUM => array_values($row),
41 PDO::FETCH_OBJ => (object) $row,
42 PDO::FETCH_COLUMN => array_values($row)[0] ?? false,
43 default => $this->keyedBothWays($row),
44 };
45 }
46
47 /**
48 * Answers the row keyed by column name and by position at once.
49 *
50 * @template TValue
51 * @param array<string, TValue> $row Row as ZTD buffered it, keyed by column
52 *
53 * @return array<int|string, TValue> The same values, reachable under either key
54 */
55 public function keyedBothWays(array $row): array
56 {
57 $both = [];
58 $index = 0;
59 foreach ($row as $column => $value) {
60 $both[$column] = $value;
61 $both[$index] = $value;
62 $index++;
63 }
64
65 return $both;
66 }
67 /**
68 * Resolve FETCH_DEFAULT using statement options before connection options.
69 */
70 public function resolveMode(int $mode, int $connectionMode, ?int $statementMode): int
71 {
72 return $mode === PDO::FETCH_DEFAULT ? ($statementMode ?? $connectionMode) : $mode;
73 }
74
75 /**
76 * Shape a buffered row while preserving the exhausted-cursor marker.
77 *
78 * @template TValue
79 * @param array<string, TValue>|false $row
80 * @return array<int|string, TValue>|TValue|stdClass|false
81 */
82 public function fetch(array|false $row, int $mode): mixed
83 {
84 return $row === false ? false : $this->inMode($row, $mode);
85 }
86
87 /**
88 * Shape all remaining rows, including column selection arguments.
89 *
90 * @template TValue
91 * @template TArgument
92 * @param array<int, array<string, TValue>> $rows
93 * @param array<TArgument> $arguments
94 * @return list<array<int|string, TValue>|TValue|stdClass|false>
95 */
96 public function all(array $rows, int $mode, array $arguments): array
97 {
98 $shaped = [];
99 $column = is_int($arguments[0] ?? null) ? $arguments[0] : 0;
100 foreach ($rows as $row) {
101 $shaped[] = $mode === PDO::FETCH_COLUMN
102 ? $this->column($row, $column)
103 : $this->inMode($row, $mode);
104 }
105 return $shaped;
106 }
107
108 /**
109 * Read one positional column while preserving the exhausted-cursor marker.
110 *
111 * @template TValue
112 * @param array<string, TValue>|false $row
113 * @return TValue|false
114 */
115 public function column(array|false $row, int $column): mixed
116 {
117 return $row === false ? false : (array_values($row)[$column] ?? false);
118 }
119
120 /**
121 * Hydrate a buffered row after invoking its constructor.
122 *
123 * @template TValue
124 * @template TObject of object
125 * @template TArgument
126 * @param array<string, TValue>|false $row
127 * @param class-string<TObject>|null $class
128 * @param array<TArgument> $constructorArgs
129 * @return ($class is null ? stdClass : TObject)|false
130 * @throws ReflectionException When a declared property cannot be written.
131 */
132 public function object(array|false $row, ?string $class, array $constructorArgs): object|false
133 {
134 if ($row === false) {
135 return false;
136 }
137 $resolvedClass = $class ?? stdClass::class;
138 $object = new $resolvedClass(...$constructorArgs);
139 if ($object instanceof stdClass) {
140 foreach ($row as $property => $value) {
141 $object->{$property} = $value;
142 }
143 return $object;
144 }
145 $reflection = new ReflectionObject($object);
146 foreach ($row as $property => $value) {
147 if ($reflection->hasProperty($property)) {
148 $reflection->getProperty($property)->setValue($object, $value);
149 }
150 }
151 return $object;
152 }
153}
154