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