Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
109 / 109
98.81% covered (success)
98.81%
83 / 84
43.75% covered (danger)
43.75%
28 / 64
94.74% covered (success)
94.74%
18 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Query
100.00% covered (success)
100.00%
109 / 109
98.81% covered (success)
98.81%
83 / 84
43.75% covered (danger)
43.75%
28 / 64
100.00% covered (success)
100.00%
19 / 19
372.08
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 one
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
7 / 7
25.00% covered (danger)
25.00%
1 / 4
100.00% covered (success)
100.00%
1 / 1
6.80
 first
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
6 / 6
0.00% covered (danger)
0.00%
0 / 4
100.00% covered (success)
100.00%
1 / 1
2
 fetch
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 all
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
9 / 9
0.00% covered (danger)
0.00%
0 / 8
100.00% covered (success)
100.00%
1 / 1
3
 lazy
100.00% covered (success)
100.00%
9 / 9
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 6
100.00% covered (success)
100.00%
1 / 1
3
 terminalOptions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
8 / 8
25.00% covered (danger)
25.00%
2 / 8
100.00% covered (success)
100.00%
1 / 1
10.75
 executeFresh
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 executeForFetch
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 hydrateRecord
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 hydrator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 len
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
0.00% covered (danger)
0.00%
0 / 2
100.00% covered (success)
100.00%
1 / 1
1
 interpolate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 bindArgs
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 bindValue
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
17 / 17
53.85% covered (warning)
53.85%
7 / 13
100.00% covered (success)
100.00%
1 / 1
14.29
 fetchArrayRecord
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 nullIfNot
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Quma;
6
7use Celemas\Quma\Exception\UnexpectedResultCount;
8use Celemas\Quma\Hydration\Hydrator;
9use Closure;
10use Generator;
11use InvalidArgumentException;
12use JsonException;
13use PDO;
14use PDOStatement;
15
16/** @api */
17class Query
18{
19    // Matches multi line single and double quotes and handles \' \" escapes
20    public const string PATTERN_STRING = '/([\'"])(?:\\\1|[\s\S])*?\1/';
21
22    // PostgreSQL blocks delimited with $$
23    public const string PATTERN_BLOCK = '/(\$\$)[\s\S]*?\1/';
24
25    // Multi line comments /* */
26    public const string PATTERN_COMMENT_MULTI = '/\/\*([\s\S]*?)\*\//';
27
28    // Single line comments --
29    public const string PATTERN_COMMENT_SINGLE = '/--.*$/m';
30
31    protected PDOStatement $stmt;
32    protected bool $executed = false;
33    protected ?Hydrator $hydrator = null;
34
35    public function __construct(
36        protected Database $db,
37        protected string $query,
38        protected Args $args,
39        protected ?string $sourcePath = null,
40    ) {
41        $this->stmt = $this->db->getConn()->prepare($query);
42
43        if ($args->count() > 0) {
44            $this->bindArgs($args->get(), $args->type());
45        }
46
47        if ($this->db->debug) {
48            Debug::query($this->db, $this->query, $this->args, $this->sourcePath);
49        }
50    }
51
52    public function __toString(): string
53    {
54        return $this->interpolate();
55    }
56
57    /**
58     * @template T of object
59     *
60     * @param class-string<T>|Closure(array<string, mixed>):class-string<T>|null $map
61     * @return ($map is null ? array<array-key, mixed> : T)
62     */
63    public function one(string|Closure|null $map = null, ?int $fetchMode = null): array|object
64    {
65        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
66        $this->executeFresh();
67
68        try {
69            $record = $this->fetchArrayRecord($fetchMode);
70
71            if ($record === null) {
72                throw UnexpectedResultCount::none();
73            }
74
75            if ($this->fetchArrayRecord($fetchMode) !== null) {
76                throw UnexpectedResultCount::multiple();
77            }
78
79            return $this->hydrateRecord($record, $map);
80        } finally {
81            $this->stmt->closeCursor();
82        }
83    }
84
85    /**
86     * @template T of object
87     *
88     * @param class-string<T>|Closure(array<string, mixed>):class-string<T>|null $map
89     * @return ($map is null ? array<array-key, mixed>|null : T|null)
90     */
91    public function first(string|Closure|null $map = null, ?int $fetchMode = null): array|object|null
92    {
93        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
94        $this->executeFresh();
95
96        try {
97            $record = $this->fetchArrayRecord($fetchMode);
98
99            return $record === null ? null : $this->hydrateRecord($record, $map);
100        } finally {
101            $this->stmt->closeCursor();
102        }
103    }
104
105    /**
106     * @template T of object
107     *
108     * @param class-string<T>|Closure(array<string, mixed>):class-string<T>|null $map
109     * @return ($map is null ? array<array-key, mixed>|null : T|null)
110     */
111    public function fetch(string|Closure|null $map = null, ?int $fetchMode = null): array|object|null
112    {
113        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
114        $this->executeForFetch();
115
116        $record = $this->fetchArrayRecord($fetchMode);
117
118        return $record === null ? null : $this->hydrateRecord($record, $map);
119    }
120
121    /**
122     * @template T of object
123     *
124     * @param class-string<T>|Closure(array<string, mixed>):class-string<T>|null $map
125     * @return ($map is null ? list<array<array-key, mixed>> : list<T>)
126     */
127    public function all(string|Closure|null $map = null, ?int $fetchMode = null): array
128    {
129        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
130        $this->executeFresh();
131
132        try {
133            if ($map === null) {
134                /**
135                 * @mago-expect lint:inline-variable-return Psalm makes this necessary
136                 * @var list<array<array-key, mixed>> $records
137                 */
138                $records = $this->stmt->fetchAll($fetchMode);
139
140                return $records;
141            }
142
143            /** @var list<T> $result */
144            $result = [];
145            /** @var list<array<array-key, mixed>> $records */
146            $records = $this->stmt->fetchAll($fetchMode);
147
148            foreach ($records as $record) {
149                /** @var T $object */
150                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
151                $result[] = $object;
152            }
153
154            return $result;
155        } finally {
156            $this->stmt->closeCursor();
157        }
158    }
159
160    /**
161     * @template T of object
162     *
163     * @param class-string<T>|Closure(array<string, mixed>):class-string<T>|null $map
164     * @return ($map is null
165     *     ? Generator<int, array<array-key, mixed>, mixed, void>
166     *     : Generator<int, T, mixed, void>)
167     */
168    public function lazy(string|Closure|null $map = null, ?int $fetchMode = null): Generator
169    {
170        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
171        $this->executeFresh();
172
173        try {
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
175                if ($map === null) {
176                    yield $record;
177
178                    continue;
179                }
180
181                /** @var T $object */
182                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
183
184                yield $object;
185            }
186        } finally {
187            $this->stmt->closeCursor();
188        }
189    }
190
191    /**
192     * @return array{0: string|Closure|null, 1: int}
193     */
194    private function terminalOptions(string|Closure|null $map, ?int $fetchMode): array
195    {
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
197
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
199            throw new InvalidArgumentException('Hydration requires PDO::FETCH_ASSOC.');
200        }
201
202        return [$map, $mode];
203    }
204
205    private function executeFresh(): void
206    {
207        $this->db->connect();
208        $this->stmt->closeCursor();
209        $this->stmt->execute();
210        $this->executed = false;
211    }
212
213    private function executeForFetch(): void
214    {
215        $this->db->connect();
216
217        if (!$this->executed) {
218            $this->stmt->closeCursor();
219            $this->stmt->execute();
220            $this->executed = true;
221        }
222    }
223
224    /**
225     * @template T of object
226     *
227     * @param array<array-key, mixed> $record
228     * @param string|Closure(array<string, mixed>):class-string<T>|null $map
229     * @return ($map is null ? array<array-key, mixed> : T)
230     */
231    private function hydrateRecord(array $record, string|Closure|null $map): array|object
232    {
233        if ($map === null) {
234            return $record;
235        }
236
237        /**
238         * @mago-expect lint:inline-variable-return Psalm makes this necessary
239         * @var T $object
240         */
241        $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
242
243        return $object;
244    }
245
246    private function hydrator(): Hydrator
247    {
248        return $this->hydrator ??= Hydrator::default();
249    }
250
251    public function run(): bool
252    {
253        $this->db->connect();
254        $this->stmt->closeCursor();
255        $this->executed = false;
256
257        return $this->stmt->execute();
258    }
259
260    public function len(): int
261    {
262        $this->executeFresh();
263
264        try {
265            return $this->stmt->rowCount();
266        } finally {
267            $this->stmt->closeCursor();
268        }
269    }
270
271    /**
272     * For debugging purposes only.
273     *
274     * Replaces any parameter placeholders in a query with the
275     * value of that parameter and returns the query as string.
276     *
277     * Covers most of the cases but is not perfect.
278     */
279    public function interpolate(): string
280    {
281        return Debug::interpolate($this->query, $this->args);
282    }
283
284    protected function bindArgs(array $args, ArgType $argType): void
285    {
286        array_walk(
287            $args,
288            function (mixed $value, int|string $index) use ($argType): void {
289                if ($argType === ArgType::Named) {
290                    $arg = ':' . $index;
291                } else {
292                    $arg = (int) $index + 1; // question mark placeholders are 1-indexed
293                }
294
295                $this->bindValue($arg, $value);
296            },
297        );
298    }
299
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
303            case 'boolean':
304                $this->stmt->bindValue($arg, $value, PDO::PARAM_BOOL);
305
306                break;
307
308            case 'integer':
309                $this->stmt->bindValue($arg, $value, PDO::PARAM_INT);
310
311                break;
312
313            case 'string':
314                $this->stmt->bindValue($arg, $value, PDO::PARAM_STR);
315
316                break;
317
318            case 'NULL':
319                $this->stmt->bindValue($arg, $value, PDO::PARAM_NULL);
320
321                break;
322
323            case 'array':
324                try {
325                    $json = json_encode($value, JSON_THROW_ON_ERROR);
326                } catch (JsonException $e) {
327                    throw new InvalidArgumentException(
328                        'Array parameters must be JSON-encodable.',
329                        previous: $e,
330                    );
331                }
332
333                $this->stmt->bindValue($arg, $json, PDO::PARAM_STR);
334
335                break;
336
337            default:
338                throw new InvalidArgumentException(
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
343
344    protected function fetchArrayRecord(int $fetchMode): ?array
345    {
346        return $this->nullIfNot($this->stmt->fetch($fetchMode));
347    }
348
349    protected function nullIfNot(mixed $value): ?array
350    {
351        if (is_array($value)) {
352            return $value;
353        }
354
355        return null;
356    }
357}

Paths

Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Query->__construct
36        protected Database $db,
37        protected string $query,
38        protected Args $args,
39        protected ?string $sourcePath = null,
40    ) {
41        $this->stmt = $this->db->getConn()->prepare($query);
42
43        if ($args->count() > 0) {
 
44            $this->bindArgs($args->get(), $args->type());
45        }
46
47        if ($this->db->debug) {
 
47        if ($this->db->debug) {
 
48            Debug::query($this->db, $this->query, $this->args, $this->sourcePath);
49        }
50    }
 
50    }
36        protected Database $db,
37        protected string $query,
38        protected Args $args,
39        protected ?string $sourcePath = null,
40    ) {
41        $this->stmt = $this->db->getConn()->prepare($query);
42
43        if ($args->count() > 0) {
 
44            $this->bindArgs($args->get(), $args->type());
45        }
46
47        if ($this->db->debug) {
 
47        if ($this->db->debug) {
 
50    }
36        protected Database $db,
37        protected string $query,
38        protected Args $args,
39        protected ?string $sourcePath = null,
40    ) {
41        $this->stmt = $this->db->getConn()->prepare($query);
42
43        if ($args->count() > 0) {
 
47        if ($this->db->debug) {
 
48            Debug::query($this->db, $this->query, $this->args, $this->sourcePath);
49        }
50    }
 
50    }
36        protected Database $db,
37        protected string $query,
38        protected Args $args,
39        protected ?string $sourcePath = null,
40    ) {
41        $this->stmt = $this->db->getConn()->prepare($query);
42
43        if ($args->count() > 0) {
 
47        if ($this->db->debug) {
 
50    }
Query->__toString
54        return $this->interpolate();
55    }
Query->all
127    public function all(string|Closure|null $map = null, ?int $fetchMode = null): array
128    {
129        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
130        $this->executeFresh();
131
132        try {
133            if ($map === null) {
 
138                $records = $this->stmt->fetchAll($fetchMode);
139
140                return $records;
 
156            $this->stmt->closeCursor();
157        }
158    }
127    public function all(string|Closure|null $map = null, ?int $fetchMode = null): array
128    {
129        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
130        $this->executeFresh();
131
132        try {
133            if ($map === null) {
 
138                $records = $this->stmt->fetchAll($fetchMode);
139
140                return $records;
 
140                return $records;
127    public function all(string|Closure|null $map = null, ?int $fetchMode = null): array
128    {
129        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
130        $this->executeFresh();
131
132        try {
133            if ($map === null) {
 
144            $result = [];
145            /** @var list<array<array-key, mixed>> $records */
146            $records = $this->stmt->fetchAll($fetchMode);
147
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
149                /** @var T $object */
150                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
 
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
149                /** @var T $object */
150                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
151                $result[] = $object;
152            }
153
154            return $result;
 
156            $this->stmt->closeCursor();
157        }
158    }
127    public function all(string|Closure|null $map = null, ?int $fetchMode = null): array
128    {
129        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
130        $this->executeFresh();
131
132        try {
133            if ($map === null) {
 
144            $result = [];
145            /** @var list<array<array-key, mixed>> $records */
146            $records = $this->stmt->fetchAll($fetchMode);
147
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
149                /** @var T $object */
150                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
 
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
149                /** @var T $object */
150                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
151                $result[] = $object;
152            }
153
154            return $result;
 
154            return $result;
127    public function all(string|Closure|null $map = null, ?int $fetchMode = null): array
128    {
129        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
130        $this->executeFresh();
131
132        try {
133            if ($map === null) {
 
144            $result = [];
145            /** @var list<array<array-key, mixed>> $records */
146            $records = $this->stmt->fetchAll($fetchMode);
147
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
149                /** @var T $object */
150                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
151                $result[] = $object;
152            }
153
154            return $result;
 
156            $this->stmt->closeCursor();
157        }
158    }
127    public function all(string|Closure|null $map = null, ?int $fetchMode = null): array
128    {
129        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
130        $this->executeFresh();
131
132        try {
133            if ($map === null) {
 
144            $result = [];
145            /** @var list<array<array-key, mixed>> $records */
146            $records = $this->stmt->fetchAll($fetchMode);
147
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
149                /** @var T $object */
150                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
151                $result[] = $object;
152            }
153
154            return $result;
 
154            return $result;
127    public function all(string|Closure|null $map = null, ?int $fetchMode = null): array
128    {
129        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
130        $this->executeFresh();
131
132        try {
133            if ($map === null) {
 
144            $result = [];
145            /** @var list<array<array-key, mixed>> $records */
146            $records = $this->stmt->fetchAll($fetchMode);
147
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
149                /** @var T $object */
150                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
151                $result[] = $object;
152            }
153
154            return $result;
 
156            $this->stmt->closeCursor();
157        }
158    }
127    public function all(string|Closure|null $map = null, ?int $fetchMode = null): array
128    {
129        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
130        $this->executeFresh();
131
132        try {
133            if ($map === null) {
 
144            $result = [];
145            /** @var list<array<array-key, mixed>> $records */
146            $records = $this->stmt->fetchAll($fetchMode);
147
148            foreach ($records as $record) {
 
148            foreach ($records as $record) {
149                /** @var T $object */
150                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
151                $result[] = $object;
152            }
153
154            return $result;
 
154            return $result;
Query->bindArgs
284    protected function bindArgs(array $args, ArgType $argType): void
285    {
286        array_walk(
287            $args,
288            function (mixed $value, int|string $index) use ($argType): void {
289                if ($argType === ArgType::Named) {
290                    $arg = ':' . $index;
291                } else {
292                    $arg = (int) $index + 1; // question mark placeholders are 1-indexed
293                }
294
295                $this->bindValue($arg, $value);
296            },
297        );
298    }
Query->bindValue
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
304                $this->stmt->bindValue($arg, $value, PDO::PARAM_BOOL);
305
306                break;
 
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
309                $this->stmt->bindValue($arg, $value, PDO::PARAM_INT);
310
311                break;
 
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
314                $this->stmt->bindValue($arg, $value, PDO::PARAM_STR);
315
316                break;
 
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
319                $this->stmt->bindValue($arg, $value, PDO::PARAM_NULL);
320
321                break;
 
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
324                try {
325                    $json = json_encode($value, JSON_THROW_ON_ERROR);
 
333                $this->stmt->bindValue($arg, $json, PDO::PARAM_STR);
334
335                break;
 
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
338                throw new InvalidArgumentException(
339                    'Only the types bool, int, string, null and array are supported',
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
303            case 'boolean':
 
308            case 'integer':
 
313            case 'string':
 
318            case 'NULL':
 
323            case 'array':
 
323            case 'array':
 
338                throw new InvalidArgumentException(
339                    'Only the types bool, int, string, null and array are supported',
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
303            case 'boolean':
 
308            case 'integer':
 
313            case 'string':
 
318            case 'NULL':
 
323            case 'array':
 
324                try {
325                    $json = json_encode($value, JSON_THROW_ON_ERROR);
 
333                $this->stmt->bindValue($arg, $json, PDO::PARAM_STR);
334
335                break;
 
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
303            case 'boolean':
 
308            case 'integer':
 
313            case 'string':
 
318            case 'NULL':
 
319                $this->stmt->bindValue($arg, $value, PDO::PARAM_NULL);
320
321                break;
 
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
303            case 'boolean':
 
308            case 'integer':
 
313            case 'string':
 
314                $this->stmt->bindValue($arg, $value, PDO::PARAM_STR);
315
316                break;
 
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
303            case 'boolean':
 
308            case 'integer':
 
309                $this->stmt->bindValue($arg, $value, PDO::PARAM_INT);
310
311                break;
 
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
300    protected function bindValue(string|int $arg, mixed $value): void
301    {
302        switch (gettype($value)) {
 
303            case 'boolean':
 
304                $this->stmt->bindValue($arg, $value, PDO::PARAM_BOOL);
305
306                break;
 
339                    'Only the types bool, int, string, null and array are supported',
340                );
341        }
342    }
326                } catch (JsonException $e) {
 
327                    throw new InvalidArgumentException(
328                        'Array parameters must be JSON-encodable.',
Query->executeForFetch
215        $this->db->connect();
216
217        if (!$this->executed) {
 
218            $this->stmt->closeCursor();
219            $this->stmt->execute();
220            $this->executed = true;
221        }
222    }
 
222    }
215        $this->db->connect();
216
217        if (!$this->executed) {
 
222    }
Query->executeFresh
207        $this->db->connect();
208        $this->stmt->closeCursor();
209        $this->stmt->execute();
210        $this->executed = false;
211    }
Query->fetch
111    public function fetch(string|Closure|null $map = null, ?int $fetchMode = null): array|object|null
112    {
113        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
114        $this->executeForFetch();
115
116        $record = $this->fetchArrayRecord($fetchMode);
117
118        return $record === null ? null : $this->hydrateRecord($record, $map);
 
118        return $record === null ? null : $this->hydrateRecord($record, $map);
 
118        return $record === null ? null : $this->hydrateRecord($record, $map);
119    }
111    public function fetch(string|Closure|null $map = null, ?int $fetchMode = null): array|object|null
112    {
113        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
114        $this->executeForFetch();
115
116        $record = $this->fetchArrayRecord($fetchMode);
117
118        return $record === null ? null : $this->hydrateRecord($record, $map);
 
118        return $record === null ? null : $this->hydrateRecord($record, $map);
 
118        return $record === null ? null : $this->hydrateRecord($record, $map);
119    }
Query->fetchArrayRecord
344    protected function fetchArrayRecord(int $fetchMode): ?array
345    {
346        return $this->nullIfNot($this->stmt->fetch($fetchMode));
347    }
Query->first
91    public function first(string|Closure|null $map = null, ?int $fetchMode = null): array|object|null
92    {
93        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
94        $this->executeFresh();
95
96        try {
97            $record = $this->fetchArrayRecord($fetchMode);
98
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
101            $this->stmt->closeCursor();
102        }
103    }
91    public function first(string|Closure|null $map = null, ?int $fetchMode = null): array|object|null
92    {
93        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
94        $this->executeFresh();
95
96        try {
97            $record = $this->fetchArrayRecord($fetchMode);
98
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
99            return $record === null ? null : $this->hydrateRecord($record, $map);
91    public function first(string|Closure|null $map = null, ?int $fetchMode = null): array|object|null
92    {
93        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
94        $this->executeFresh();
95
96        try {
97            $record = $this->fetchArrayRecord($fetchMode);
98
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
101            $this->stmt->closeCursor();
102        }
103    }
91    public function first(string|Closure|null $map = null, ?int $fetchMode = null): array|object|null
92    {
93        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
94        $this->executeFresh();
95
96        try {
97            $record = $this->fetchArrayRecord($fetchMode);
98
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
99            return $record === null ? null : $this->hydrateRecord($record, $map);
 
99            return $record === null ? null : $this->hydrateRecord($record, $map);
Query->hydrateRecord
231    private function hydrateRecord(array $record, string|Closure|null $map): array|object
232    {
233        if ($map === null) {
 
234            return $record;
231    private function hydrateRecord(array $record, string|Closure|null $map): array|object
232    {
233        if ($map === null) {
 
241        $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
242
243        return $object;
244    }
Query->hydrator
248        return $this->hydrator ??= Hydrator::default();
249    }
Query->interpolate
281        return Debug::interpolate($this->query, $this->args);
282    }
Query->lazy
168    public function lazy(string|Closure|null $map = null, ?int $fetchMode = null): Generator
169    {
170        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
171        $this->executeFresh();
172
173        try {
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
186        } finally {
 
187            $this->stmt->closeCursor();
168    public function lazy(string|Closure|null $map = null, ?int $fetchMode = null): Generator
169    {
170        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
171        $this->executeFresh();
172
173        try {
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
186        } finally {
 
186        } finally {
 
189    }
168    public function lazy(string|Closure|null $map = null, ?int $fetchMode = null): Generator
169    {
170        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
171        $this->executeFresh();
172
173        try {
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
175                if ($map === null) {
 
176                    yield $record;
177
178                    continue;
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
186        } finally {
 
187            $this->stmt->closeCursor();
168    public function lazy(string|Closure|null $map = null, ?int $fetchMode = null): Generator
169    {
170        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
171        $this->executeFresh();
172
173        try {
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
175                if ($map === null) {
 
176                    yield $record;
177
178                    continue;
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
186        } finally {
 
186        } finally {
 
189    }
168    public function lazy(string|Closure|null $map = null, ?int $fetchMode = null): Generator
169    {
170        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
171        $this->executeFresh();
172
173        try {
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
175                if ($map === null) {
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
175                if ($map === null) {
176                    yield $record;
177
178                    continue;
179                }
180
181                /** @var T $object */
182                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
186        } finally {
 
187            $this->stmt->closeCursor();
168    public function lazy(string|Closure|null $map = null, ?int $fetchMode = null): Generator
169    {
170        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
171        $this->executeFresh();
172
173        try {
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
175                if ($map === null) {
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
175                if ($map === null) {
176                    yield $record;
177
178                    continue;
179                }
180
181                /** @var T $object */
182                $object = $this->hydrator()->hydrate($record, $map, $this->sourcePath);
 
174            while (($record = $this->fetchArrayRecord($fetchMode)) !== null) {
 
186        } finally {
 
186        } finally {
 
189    }
Query->len
262        $this->executeFresh();
263
264        try {
265            return $this->stmt->rowCount();
 
267            $this->stmt->closeCursor();
268        }
269    }
262        $this->executeFresh();
263
264        try {
265            return $this->stmt->rowCount();
 
265            return $this->stmt->rowCount();
Query->nullIfNot
349    protected function nullIfNot(mixed $value): ?array
350    {
351        if (is_array($value)) {
 
352            return $value;
349    protected function nullIfNot(mixed $value): ?array
350    {
351        if (is_array($value)) {
 
355        return null;
356    }
Query->one
63    public function one(string|Closure|null $map = null, ?int $fetchMode = null): array|object
64    {
65        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
66        $this->executeFresh();
67
68        try {
69            $record = $this->fetchArrayRecord($fetchMode);
70
71            if ($record === null) {
 
72                throw UnexpectedResultCount::none();
63    public function one(string|Closure|null $map = null, ?int $fetchMode = null): array|object
64    {
65        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
66        $this->executeFresh();
67
68        try {
69            $record = $this->fetchArrayRecord($fetchMode);
70
71            if ($record === null) {
 
75            if ($this->fetchArrayRecord($fetchMode) !== null) {
 
76                throw UnexpectedResultCount::multiple();
63    public function one(string|Closure|null $map = null, ?int $fetchMode = null): array|object
64    {
65        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
66        $this->executeFresh();
67
68        try {
69            $record = $this->fetchArrayRecord($fetchMode);
70
71            if ($record === null) {
 
75            if ($this->fetchArrayRecord($fetchMode) !== null) {
 
79            return $this->hydrateRecord($record, $map);
 
81            $this->stmt->closeCursor();
82        }
83    }
63    public function one(string|Closure|null $map = null, ?int $fetchMode = null): array|object
64    {
65        [$map, $fetchMode] = $this->terminalOptions($map, $fetchMode);
66        $this->executeFresh();
67
68        try {
69            $record = $this->fetchArrayRecord($fetchMode);
70
71            if ($record === null) {
 
75            if ($this->fetchArrayRecord($fetchMode) !== null) {
 
79            return $this->hydrateRecord($record, $map);
 
79            return $this->hydrateRecord($record, $map);
Query->run
253        $this->db->connect();
254        $this->stmt->closeCursor();
255        $this->executed = false;
256
257        return $this->stmt->execute();
258    }
Query->terminalOptions
194    private function terminalOptions(string|Closure|null $map, ?int $fetchMode): array
195    {
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
197
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
199            throw new InvalidArgumentException('Hydration requires PDO::FETCH_ASSOC.');
194    private function terminalOptions(string|Closure|null $map, ?int $fetchMode): array
195    {
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
197
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
202        return [$map, $mode];
203    }
194    private function terminalOptions(string|Closure|null $map, ?int $fetchMode): array
195    {
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
197
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
199            throw new InvalidArgumentException('Hydration requires PDO::FETCH_ASSOC.');
194    private function terminalOptions(string|Closure|null $map, ?int $fetchMode): array
195    {
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
197
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
202        return [$map, $mode];
203    }
194    private function terminalOptions(string|Closure|null $map, ?int $fetchMode): array
195    {
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
197
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
199            throw new InvalidArgumentException('Hydration requires PDO::FETCH_ASSOC.');
194    private function terminalOptions(string|Closure|null $map, ?int $fetchMode): array
195    {
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
197
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
202        return [$map, $mode];
203    }
194    private function terminalOptions(string|Closure|null $map, ?int $fetchMode): array
195    {
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
197
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
199            throw new InvalidArgumentException('Hydration requires PDO::FETCH_ASSOC.');
194    private function terminalOptions(string|Closure|null $map, ?int $fetchMode): array
195    {
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
 
196        $mode = $fetchMode ?? ($map === null ? $this->db->getFetchMode() : PDO::FETCH_ASSOC);
197
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
198        if ($map !== null && $mode !== PDO::FETCH_ASSOC) {
 
202        return [$map, $mode];
203    }
{closure:/workspace/celemas/quma/src/Query.php:288-296}
288            function (mixed $value, int|string $index) use ($argType): void {
289                if ($argType === ArgType::Named) {
 
289                if ($argType === ArgType::Named) {
290                    $arg = ':' . $index;
 
295                $this->bindValue($arg, $value);
296            },
288            function (mixed $value, int|string $index) use ($argType): void {
289                if ($argType === ArgType::Named) {
 
292                    $arg = (int) $index + 1; // question mark placeholders are 1-indexed
293                }
294
295                $this->bindValue($arg, $value);
 
295                $this->bindValue($arg, $value);
296            },