Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
196 / 196
84.58% covered (warning)
84.58%
203 / 240
8.84% covered (danger)
8.84%
55 / 622
54.84% covered (warning)
54.84%
17 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
Debug
100.00% covered (success)
100.00%
196 / 196
84.58% covered (warning)
84.58%
203 / 240
8.84% covered (danger)
8.84%
55 / 622
100.00% covered (success)
100.00%
31 / 31
8943.39
100.00% covered (success)
100.00%
1 / 1
 query
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
22 / 22
1.23% covered (danger)
1.23%
4 / 324
100.00% covered (success)
100.00%
1 / 1
127.57
 interpolate
100.00% covered (success)
100.00%
5 / 5
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
 enabled
100.00% covered (success)
100.00%
2 / 2
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
 prints
100.00% covered (success)
100.00%
2 / 2
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
 writesTranslated
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
 writesInterpolated
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
 printQuery
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 value
100.00% covered (success)
100.00%
10 / 10
86.67% covered (warning)
86.67%
13 / 15
71.43% covered (warning)
71.43%
5 / 7
100.00% covered (success)
100.00%
1 / 1
8.14
 prepareQuery
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
8 / 8
10.00% covered (danger)
10.00%
1 / 10
100.00% covered (success)
100.00%
1 / 1
9.56
 restoreQuery
100.00% covered (success)
100.00%
3 / 3
85.71% covered (warning)
85.71%
6 / 7
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
2.50
 interpolateNamed
100.00% covered (success)
100.00%
9 / 9
75.00% covered (warning)
75.00%
3 / 4
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
4.12
 interpolatePositional
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
1
 flag
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
40.00% covered (danger)
40.00%
2 / 5
100.00% covered (success)
100.00%
1 / 1
4.94
 env
100.00% covered (success)
100.00%
7 / 7
81.82% covered (warning)
81.82%
9 / 11
20.00% covered (danger)
20.00%
2 / 10
100.00% covered (success)
100.00%
1 / 1
12.19
 dir
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
8 / 8
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
7.33
 writeEnv
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 write
100.00% covered (success)
100.00%
4 / 4
80.00% covered (warning)
80.00%
8 / 10
25.00% covered (danger)
25.00%
2 / 8
100.00% covered (success)
100.00%
1 / 1
15.55
 relativeToRoots
100.00% covered (success)
100.00%
10 / 10
73.91% covered (warning)
73.91%
17 / 23
1.56% covered (danger)
1.56%
1 / 64
100.00% covered (success)
100.00%
1 / 1
69.05
 sessionPath
100.00% covered (success)
100.00%
5 / 5
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
 sourceName
100.00% covered (success)
100.00%
8 / 8
75.00% covered (warning)
75.00%
9 / 12
22.22% covered (danger)
22.22%
2 / 9
100.00% covered (success)
100.00%
1 / 1
11.53
 session
100.00% covered (success)
100.00%
6 / 6
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
 sessionInfo
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
7 / 7
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
5.02
 requestTime
100.00% covered (success)
100.00%
9 / 9
92.31% covered (success)
92.31%
12 / 13
11.11% covered (danger)
11.11%
3 / 27
100.00% covered (success)
100.00%
1 / 1
31.28
 argv
100.00% covered (success)
100.00%
1 / 1
75.00% covered (warning)
75.00%
3 / 4
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
1.12
 sessionLabel
100.00% covered (success)
100.00%
2 / 2
42.86% covered (danger)
42.86%
3 / 7
33.33% covered (danger)
33.33%
1 / 3
100.00% covered (success)
100.00%
1 / 1
3.19
 uriLabel
100.00% covered (success)
100.00%
9 / 9
65.22% covered (warning)
65.22%
15 / 23
1.92% covered (danger)
1.92%
2 / 104
100.00% covered (success)
100.00%
1 / 1
39.96
 hash
100.00% covered (success)
100.00%
1 / 1
75.00% covered (warning)
75.00%
3 / 4
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
1.12
 server
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
10 / 10
40.00% covered (danger)
40.00%
4 / 10
100.00% covered (success)
100.00%
1 / 1
10.40
 counter
100.00% covered (success)
100.00%
2 / 2
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
 safeRelativePath
100.00% covered (success)
100.00%
8 / 8
85.71% covered (warning)
85.71%
12 / 14
n/a
0 / 0
100.00% covered (success)
100.00%
1 / 1
7
 safeSegment
100.00% covered (success)
100.00%
3 / 3
70.00% covered (warning)
70.00%
7 / 10
12.50% covered (danger)
12.50%
1 / 8
100.00% covered (success)
100.00%
1 / 1
4.68
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Quma;
6
7use DateTimeImmutable;
8use RuntimeException;
9
10/**
11 * @internal
12 *
13 * Coverage ignores mark defensive filesystem/global-state race branches that are
14 * not deterministic or meaningful to exercise in tests.
15 */
16final class Debug
17{
18    public const string ENV_DEBUG = 'QUMA_DEBUG';
19    public const string ENV_PRINT = 'QUMA_DEBUG_PRINT';
20    public const string ENV_TRANSLATED = 'QUMA_DEBUG_TRANSLATED';
21    public const string ENV_INTERPOLATED = 'QUMA_DEBUG_INTERPOLATED';
22
23    private static ?string $sessionKey = null;
24    private static ?string $session = null;
25    private static ?string $fallbackTime = null;
26    private static int $counter = 0;
27
28    public static function query(Database $db, string $query, Args $args, ?string $sourcePath): void
29    {
30        $print = self::prints();
31        $writeTranslated = self::writesTranslated();
32        $writeInterpolated = self::writesInterpolated();
33
34        if (!$print && !$writeTranslated && !$writeInterpolated) {
35            return;
36        }
37
38        $path = $writeTranslated || $writeInterpolated
39            ? self::sessionPath($sourcePath, $db->getSqlDirs())
40            : null;
41
42        if ($writeTranslated) {
43            self::writeEnv(self::ENV_TRANSLATED, $path, $query);
44        }
45
46        if (!$print && !$writeInterpolated) {
47            return;
48        }
49
50        $interpolated = self::interpolate($query, $args);
51
52        if ($print) {
53            self::printQuery($interpolated);
54        }
55
56        if ($writeInterpolated) {
57            self::writeEnv(self::ENV_INTERPOLATED, $path, $interpolated);
58        }
59    }
60
61    public static function interpolate(string $query, Args $args): string
62    {
63        $prep = self::prepareQuery($query);
64
65        if ($args->type() === ArgType::Named) {
66            $interpolated = self::interpolateNamed($prep->query, $args->getNamed());
67        } else {
68            $interpolated = self::interpolatePositional($prep->query, $args->get());
69        }
70
71        return self::restoreQuery($interpolated, $prep);
72    }
73
74    public static function enabled(): bool
75    {
76        $value = self::env(self::ENV_DEBUG);
77
78        return $value !== null && self::flag($value);
79    }
80
81    public static function prints(): bool
82    {
83        $value = self::env(self::ENV_PRINT);
84
85        return $value !== null && self::flag($value);
86    }
87
88    public static function writesTranslated(): bool
89    {
90        return self::env(self::ENV_TRANSLATED) !== null;
91    }
92
93    public static function writesInterpolated(): bool
94    {
95        return self::env(self::ENV_INTERPOLATED) !== null;
96    }
97
98    private static function printQuery(string $query): void
99    {
100        $msg =
101            "\n\n-----------------------------------------------\n\n"
102            . $query
103            . "\n------------------------------------------------\n";
104
105        if (($_SERVER['SERVER_SOFTWARE'] ?? null) !== null) {
106            // @codeCoverageIgnoreStart
107            error_log($msg);
108
109            // @codeCoverageIgnoreEnd
110        } else {
111            echo $msg;
112        }
113    }
114
115    private static function value(mixed $value): string
116    {
117        if (is_string($value)) {
118            return "'" . $value . "'";
119        }
120
121        if (is_array($value)) {
122            $encoded = json_encode($value);
123
124            return "'" . ($encoded !== false ? $encoded : '[]') . "'";
125        }
126
127        if (is_null($value)) {
128            return 'NULL';
129        }
130
131        if (is_bool($value)) {
132            return $value ? 'true' : 'false';
133        }
134
135        return (string) $value;
136    }
137
138    private static function prepareQuery(string $query): PreparedQuery
139    {
140        $patterns = [
141            Query::PATTERN_BLOCK,
142            Query::PATTERN_STRING,
143            Query::PATTERN_COMMENT_MULTI,
144            Query::PATTERN_COMMENT_SINGLE,
145        ];
146
147        $swaps = [];
148        $i = 0;
149
150        do {
151            $found = false;
152
153            foreach ($patterns as $pattern) {
154                $matches = [];
155
156                if (preg_match($pattern, $query, $matches) === 1) {
157                    $match = $matches[0];
158                    $replacement = "___CHUCK_REPLACE_{$i}___";
159                    assert($match !== '', 'Query placeholder match must not be empty.');
160                    $swaps[$replacement] = $match;
161
162                    $query = preg_replace($pattern, $replacement, $query, limit: 1) ?? $query;
163                    $found = true;
164                    $i++;
165
166                    break;
167                }
168            }
169        } while ($found);
170
171        return new PreparedQuery($query, $swaps);
172    }
173
174    private static function restoreQuery(string $query, PreparedQuery $prep): string
175    {
176        foreach ($prep->swaps as $swap => $replacement) {
177            $query = str_replace($swap, $replacement, $query);
178        }
179
180        return $query;
181    }
182
183    /** @param array<array-key, mixed> $args */
184    private static function interpolateNamed(string $query, array $args): string
185    {
186        $map = [];
187
188        array_walk(
189            $args,
190            static function (mixed $value, int|string $key) use (&$map): void {
191                if (is_string($key) && $key !== '') {
192                    $map[':' . $key] = self::value($value);
193                }
194            },
195        );
196
197        return strtr($query, $map);
198    }
199
200    /** @param array<array-key, mixed> $args */
201    private static function interpolatePositional(string $query, array $args): string
202    {
203        $result = $query;
204
205        array_walk(
206            $args,
207            static function (mixed $value) use (&$result): void {
208                $replaced = preg_replace('/\\?/', self::value($value), $result, 1);
209                $result = $replaced ?? $result;
210            },
211        );
212
213        return $result;
214    }
215
216    private static function flag(string $value): bool
217    {
218        return match (strtolower($value)) {
219            '1', 'true', 'yes', 'on' => true,
220            default => false,
221        };
222    }
223
224    private static function env(string $name): ?string
225    {
226        $value = getenv($name);
227
228        if ($value === false) {
229            $value = $_SERVER[$name] ?? $_ENV[$name] ?? null;
230        }
231
232        if (!is_string($value)) {
233            return null;
234        }
235
236        $value = trim($value);
237
238        return $value === '' ? null : $value;
239    }
240
241    /** @return non-empty-string|null */
242    private static function dir(string $name): ?string
243    {
244        $dir = self::env($name);
245
246        if ($dir === null) {
247            return null; // @codeCoverageIgnore
248        }
249
250        if (!is_dir($dir)) {
251            throw new RuntimeException("Quma debug directory does not exist for {$name}{$dir}");
252        }
253
254        if (!is_writable($dir)) {
255            throw new RuntimeException("Quma debug directory is not writable for {$name}{$dir}"); // @codeCoverageIgnore
256        }
257
258        $path = realpath($dir);
259
260        if ($path === false || $path === '') {
261            throw new RuntimeException("Quma debug directory does not exist for {$name}{$dir}"); // @codeCoverageIgnore
262        }
263
264        return $path;
265    }
266
267    private static function writeEnv(string $name, ?string $relative, string $source): void
268    {
269        if ($relative === null) {
270            return; // @codeCoverageIgnore
271        }
272
273        $dir = self::dir($name);
274
275        if ($dir === null) {
276            return; // @codeCoverageIgnore
277        }
278
279        self::write($dir, $relative, $source);
280    }
281
282    private static function write(
283        string $dir,
284        string $relative,
285        string $source,
286    ): void {
287        $path = $dir . DIRECTORY_SEPARATOR . self::safeRelativePath($relative);
288        $targetDir = dirname($path);
289
290        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
291            throw new RuntimeException('Could not create Quma debug directory: ' . $targetDir); // @codeCoverageIgnore
292        }
293
294        if (file_put_contents($path, $source, LOCK_EX) === false) {
295            throw new RuntimeException('Could not write Quma debug file: ' . $path); // @codeCoverageIgnore
296        }
297    }
298
299    /** @param array<array-key, mixed> $roots */
300    private static function relativeToRoots(string $sourcePath, array $roots): string
301    {
302        $realSource = realpath($sourcePath);
303        $source = $realSource !== false ? $realSource : $sourcePath;
304
305        foreach ($roots as $root) {
306            if (!is_string($root) || $root === '') {
307                continue; // @codeCoverageIgnore
308            }
309
310            $realRoot = realpath($root);
311            $root = $realRoot !== false ? $realRoot : $root;
312            $prefix = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
313
314            if (str_starts_with($source, $prefix)) {
315                $relative = substr($source, strlen($prefix));
316
317                return $relative === '' ? basename($sourcePath) : $relative;
318            }
319        }
320
321        return basename($sourcePath); // @codeCoverageIgnore
322    }
323
324    /** @param array<array-key, mixed> $roots */
325    private static function sessionPath(?string $sourcePath, array $roots): string
326    {
327        return self::session()
328        . DIRECTORY_SEPARATOR
329        . self::counter()
330        . '--'
331        . self::sourceName($sourcePath, $roots);
332    }
333
334    /** @param array<array-key, mixed> $roots */
335    private static function sourceName(?string $sourcePath, array $roots): string
336    {
337        if ($sourcePath === null) {
338            return 'execute.sql';
339        }
340
341        $relative = self::relativeToRoots($sourcePath, $roots);
342        $dir = dirname($relative);
343        $name = pathinfo($relative, PATHINFO_FILENAME);
344        $name = $name !== '' ? $name : 'query';
345        $path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name . '.sql';
346
347        return preg_replace('/[\\/]+/', '--', $path) ?? 'query.sql';
348    }
349
350    private static function session(): string
351    {
352        [$key, $session] = self::sessionInfo();
353
354        if (self::$sessionKey !== $key) {
355            self::$sessionKey = $key;
356            self::$session = $session;
357            self::$counter = 0;
358        }
359
360        return self::$session ?? $session;
361    }
362
363    /** @return array{0: string, 1: string} */
364    private static function sessionInfo(): array
365    {
366        $explicit = self::env('QUMA_DEBUG_SESSION');
367
368        if ($explicit !== null) {
369            return ['env:' . $explicit, self::sessionLabel($explicit)];
370        }
371
372        $method = self::server('REQUEST_METHOD');
373        $requestUri = self::server('REQUEST_URI');
374
375        if ($method !== null || $requestUri !== null) {
376            $method = strtoupper($method ?? 'HTTP');
377            $uri = $requestUri ?? self::server('SCRIPT_NAME') ?? self::server('PHP_SELF') ?? '/';
378            $time = self::requestTime();
379            $label = self::uriLabel($uri);
380            $hash = self::hash([$time, $method, $uri, (string) getmypid()]);
381
382            return ["http:{$time}:{$method}:{$uri}", "{$time}--{$method}--{$label}--{$hash}"];
383        }
384
385        $time = self::requestTime();
386        $hash = self::hash([$time, (string) getmypid(), self::argv()]);
387
388        return ["cli:{$time}", "{$time}--cli--{$hash}"];
389    }
390
391    private static function requestTime(): string
392    {
393        $time = self::server('REQUEST_TIME_FLOAT');
394
395        if ($time !== null && is_numeric($time)) {
396            $date = DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', (float) $time));
397
398            if ($date instanceof DateTimeImmutable) {
399                return $date->format('Ymd-His-u');
400            }
401        }
402
403        $time = self::server('REQUEST_TIME');
404
405        if ($time !== null && ctype_digit($time)) {
406            return new DateTimeImmutable('@' . $time)->format('Ymd-His') . '-000000';
407        }
408
409        return self::$fallbackTime ??= new DateTimeImmutable()->format('Ymd-His-u');
410    }
411
412    private static function argv(): string
413    {
414        return implode(' ', $_SERVER['argv'] ?? []);
415    }
416
417    private static function sessionLabel(string $value): string
418    {
419        $label = self::safeSegment($value);
420
421        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
422    }
423
424    private static function uriLabel(string $uri): string
425    {
426        $path = parse_url($uri, PHP_URL_PATH);
427        $path = is_string($path) && $path !== '' ? $path : '/';
428        $label = trim($path, '/');
429
430        if ($label === '') {
431            return 'root';
432        }
433
434        $label = preg_replace('/[^A-Za-z0-9._:-]+/', '-', $label) ?? 'request';
435        $label = trim($label, '-.');
436        $label = $label !== '' ? $label : 'request';
437
438        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
439    }
440
441    /** @param list<string> $parts */
442    private static function hash(array $parts): string
443    {
444        return substr(hash('xxh128', implode("\0", $parts)), 0, 8);
445    }
446
447    private static function server(string $name): ?string
448    {
449        $value = $_SERVER[$name] ?? null;
450
451        if (is_float($value) || is_int($value)) {
452            return (string) $value;
453        }
454
455        return is_string($value) && $value !== '' ? $value : null;
456    }
457
458    private static function counter(): string
459    {
460        self::$counter++;
461
462        return str_pad((string) self::$counter, 4, '0', STR_PAD_LEFT);
463    }
464
465    private static function safeRelativePath(string $path): string
466    {
467        $parts = preg_split('/[\\/]+/', $path, -1, PREG_SPLIT_NO_EMPTY);
468
469        if (!is_array($parts) || count($parts) === 0) {
470            return 'query.sql'; // @codeCoverageIgnore
471        }
472
473        $result = [];
474
475        foreach ($parts as $part) {
476            if ($part === '.' || $part === '..') {
477                continue; // @codeCoverageIgnore
478            }
479
480            $result[] = self::safeSegment($part);
481        }
482
483        $path = implode(DIRECTORY_SEPARATOR, $result);
484
485        return $path !== '' ? $path : 'query.sql';
486    }
487
488    private static function safeSegment(string $segment): string
489    {
490        $result = preg_replace('/[^A-Za-z0-9._:-]+/', '_', $segment);
491        $result = trim($result ?? '', '.');
492
493        return $result === '' ? '_' : $result;
494    }
495}

Branches

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

Debug->argv
414        return implode(' ', $_SERVER['argv'] ?? []);
414        return implode(' ', $_SERVER['argv'] ?? []);
414        return implode(' ', $_SERVER['argv'] ?? []);
414        return implode(' ', $_SERVER['argv'] ?? []);
415    }
Debug->counter
460        self::$counter++;
461
462        return str_pad((string) self::$counter, 4, '0', STR_PAD_LEFT);
463    }
Debug->dir
242    private static function dir(string $name): ?string
243    {
244        $dir = self::env($name);
245
246        if ($dir === null) {
250        if (!is_dir($dir)) {
251            throw new RuntimeException("Quma debug directory does not exist for {$name}{$dir}");
254        if (!is_writable($dir)) {
258        $path = realpath($dir);
259
260        if ($path === false || $path === '') {
260        if ($path === false || $path === '') {
260        if ($path === false || $path === '') {
264        return $path;
265    }
Debug->enabled
76        $value = self::env(self::ENV_DEBUG);
77
78        return $value !== null && self::flag($value);
78        return $value !== null && self::flag($value);
78        return $value !== null && self::flag($value);
79    }
Debug->env
224    private static function env(string $name): ?string
225    {
226        $value = getenv($name);
227
228        if ($value === false) {
229            $value = $_SERVER[$name] ?? $_ENV[$name] ?? null;
230        }
231
232        if (!is_string($value)) {
232        if (!is_string($value)) {
233            return null;
236        $value = trim($value);
236        $value = trim($value);
236        $value = trim($value);
236        $value = trim($value);
237
238        return $value === '' ? null : $value;
238        return $value === '' ? null : $value;
238        return $value === '' ? null : $value;
238        return $value === '' ? null : $value;
239    }
Debug->flag
216    private static function flag(string $value): bool
217    {
218        return match (strtolower($value)) {
219            '1', 'true', 'yes', 'on' => true,
220            default => false,
220            default => false,
221        };
222    }
Debug->hash
442    private static function hash(array $parts): string
443    {
444        return substr(hash('xxh128', implode("\0", $parts)), 0, 8);
444        return substr(hash('xxh128', implode("\0", $parts)), 0, 8);
444        return substr(hash('xxh128', implode("\0", $parts)), 0, 8);
444        return substr(hash('xxh128', implode("\0", $parts)), 0, 8);
445    }
Debug->interpolate
61    public static function interpolate(string $query, Args $args): string
62    {
63        $prep = self::prepareQuery($query);
64
65        if ($args->type() === ArgType::Named) {
65        if ($args->type() === ArgType::Named) {
66            $interpolated = self::interpolateNamed($prep->query, $args->getNamed());
68            $interpolated = self::interpolatePositional($prep->query, $args->get());
69        }
70
71        return self::restoreQuery($interpolated, $prep);
71        return self::restoreQuery($interpolated, $prep);
72    }
Debug->interpolateNamed
184    private static function interpolateNamed(string $query, array $args): string
185    {
186        $map = [];
187
188        array_walk(
189            $args,
190            static function (mixed $value, int|string $key) use (&$map): void {
191                if (is_string($key) && $key !== '') {
192                    $map[':' . $key] = self::value($value);
193                }
194            },
195        );
196
197        return strtr($query, $map);
197        return strtr($query, $map);
197        return strtr($query, $map);
197        return strtr($query, $map);
198    }
Debug->interpolatePositional
201    private static function interpolatePositional(string $query, array $args): string
202    {
203        $result = $query;
204
205        array_walk(
206            $args,
207            static function (mixed $value) use (&$result): void {
208                $replaced = preg_replace('/\\?/', self::value($value), $result, 1);
209                $result = $replaced ?? $result;
210            },
211        );
212
213        return $result;
214    }
Debug->prepareQuery
138    private static function prepareQuery(string $query): PreparedQuery
139    {
140        $patterns = [
141            Query::PATTERN_BLOCK,
142            Query::PATTERN_STRING,
143            Query::PATTERN_COMMENT_MULTI,
144            Query::PATTERN_COMMENT_SINGLE,
145        ];
146
147        $swaps = [];
148        $i = 0;
149
150        do {
151            $found = false;
151            $found = false;
152
153            foreach ($patterns as $pattern) {
153            foreach ($patterns as $pattern) {
154                $matches = [];
155
156                if (preg_match($pattern, $query, $matches) === 1) {
157                    $match = $matches[0];
158                    $replacement = "___CHUCK_REPLACE_{$i}___";
159                    assert($match !== '', 'Query placeholder match must not be empty.');
160                    $swaps[$replacement] = $match;
161
162                    $query = preg_replace($pattern, $replacement, $query, limit: 1) ?? $query;
163                    $found = true;
164                    $i++;
165
166                    break;
153            foreach ($patterns as $pattern) {
153            foreach ($patterns as $pattern) {
154                $matches = [];
155
156                if (preg_match($pattern, $query, $matches) === 1) {
157                    $match = $matches[0];
158                    $replacement = "___CHUCK_REPLACE_{$i}___";
159                    assert($match !== '', 'Query placeholder match must not be empty.');
160                    $swaps[$replacement] = $match;
161
162                    $query = preg_replace($pattern, $replacement, $query, limit: 1) ?? $query;
163                    $found = true;
164                    $i++;
165
166                    break;
167                }
168            }
169        } while ($found);
171        return new PreparedQuery($query, $swaps);
172    }
Debug->printQuery
98    private static function printQuery(string $query): void
99    {
100        $msg =
101            "\n\n-----------------------------------------------\n\n"
102            . $query
103            . "\n------------------------------------------------\n";
104
105        if (($_SERVER['SERVER_SOFTWARE'] ?? null) !== null) {
111            echo $msg;
112        }
113    }
113    }
Debug->prints
83        $value = self::env(self::ENV_PRINT);
84
85        return $value !== null && self::flag($value);
85        return $value !== null && self::flag($value);
85        return $value !== null && self::flag($value);
86    }
Debug->query
28    public static function query(Database $db, string $query, Args $args, ?string $sourcePath): void
29    {
30        $print = self::prints();
31        $writeTranslated = self::writesTranslated();
32        $writeInterpolated = self::writesInterpolated();
33
34        if (!$print && !$writeTranslated && !$writeInterpolated) {
34        if (!$print && !$writeTranslated && !$writeInterpolated) {
34        if (!$print && !$writeTranslated && !$writeInterpolated) {
34        if (!$print && !$writeTranslated && !$writeInterpolated) {
34        if (!$print && !$writeTranslated && !$writeInterpolated) {
35            return;
38        $path = $writeTranslated || $writeInterpolated
38        $path = $writeTranslated || $writeInterpolated
38        $path = $writeTranslated || $writeInterpolated
39            ? self::sessionPath($sourcePath, $db->getSqlDirs())
38        $path = $writeTranslated || $writeInterpolated
39            ? self::sessionPath($sourcePath, $db->getSqlDirs())
40            : null;
38        $path = $writeTranslated || $writeInterpolated
39            ? self::sessionPath($sourcePath, $db->getSqlDirs())
40            : null;
41
42        if ($writeTranslated) {
43            self::writeEnv(self::ENV_TRANSLATED, $path, $query);
44        }
45
46        if (!$print && !$writeInterpolated) {
46        if (!$print && !$writeInterpolated) {
46        if (!$print && !$writeInterpolated) {
46        if (!$print && !$writeInterpolated) {
47            return;
50        $interpolated = self::interpolate($query, $args);
51
52        if ($print) {
53            self::printQuery($interpolated);
54        }
55
56        if ($writeInterpolated) {
56        if ($writeInterpolated) {
57            self::writeEnv(self::ENV_INTERPOLATED, $path, $interpolated);
58        }
59    }
59    }
Debug->relativeToRoots
300    private static function relativeToRoots(string $sourcePath, array $roots): string
301    {
302        $realSource = realpath($sourcePath);
303        $source = $realSource !== false ? $realSource : $sourcePath;
303        $source = $realSource !== false ? $realSource : $sourcePath;
303        $source = $realSource !== false ? $realSource : $sourcePath;
303        $source = $realSource !== false ? $realSource : $sourcePath;
304
305        foreach ($roots as $root) {
305        foreach ($roots as $root) {
306            if (!is_string($root) || $root === '') {
306            if (!is_string($root) || $root === '') {
306            if (!is_string($root) || $root === '') {
310            $realRoot = realpath($root);
311            $root = $realRoot !== false ? $realRoot : $root;
311            $root = $realRoot !== false ? $realRoot : $root;
311            $root = $realRoot !== false ? $realRoot : $root;
311            $root = $realRoot !== false ? $realRoot : $root;
312            $prefix = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
313
314            if (str_starts_with($source, $prefix)) {
314            if (str_starts_with($source, $prefix)) {
314            if (str_starts_with($source, $prefix)) {
314            if (str_starts_with($source, $prefix)) {
315                $relative = substr($source, strlen($prefix));
315                $relative = substr($source, strlen($prefix));
315                $relative = substr($source, strlen($prefix));
315                $relative = substr($source, strlen($prefix));
316
317                return $relative === '' ? basename($sourcePath) : $relative;
317                return $relative === '' ? basename($sourcePath) : $relative;
317                return $relative === '' ? basename($sourcePath) : $relative;
317                return $relative === '' ? basename($sourcePath) : $relative;
305        foreach ($roots as $root) {
Debug->requestTime
393        $time = self::server('REQUEST_TIME_FLOAT');
394
395        if ($time !== null && is_numeric($time)) {
395        if ($time !== null && is_numeric($time)) {
395        if ($time !== null && is_numeric($time)) {
395        if ($time !== null && is_numeric($time)) {
395        if ($time !== null && is_numeric($time)) {
395        if ($time !== null && is_numeric($time)) {
396            $date = DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', (float) $time));
397
398            if ($date instanceof DateTimeImmutable) {
399                return $date->format('Ymd-His-u');
403        $time = self::server('REQUEST_TIME');
404
405        if ($time !== null && ctype_digit($time)) {
405        if ($time !== null && ctype_digit($time)) {
405        if ($time !== null && ctype_digit($time)) {
406            return new DateTimeImmutable('@' . $time)->format('Ymd-His') . '-000000';
409        return self::$fallbackTime ??= new DateTimeImmutable()->format('Ymd-His-u');
410    }
Debug->restoreQuery
174    private static function restoreQuery(string $query, PreparedQuery $prep): string
175    {
176        foreach ($prep->swaps as $swap => $replacement) {
176        foreach ($prep->swaps as $swap => $replacement) {
176        foreach ($prep->swaps as $swap => $replacement) {
177            $query = str_replace($swap, $replacement, $query);
177            $query = str_replace($swap, $replacement, $query);
177            $query = str_replace($swap, $replacement, $query);
176        foreach ($prep->swaps as $swap => $replacement) {
177            $query = str_replace($swap, $replacement, $query);
176        foreach ($prep->swaps as $swap => $replacement) {
177            $query = str_replace($swap, $replacement, $query);
178        }
179
180        return $query;
181    }
Debug->safeRelativePath
465    private static function safeRelativePath(string $path): string
466    {
467        $parts = preg_split('/[\\/]+/', $path, -1, PREG_SPLIT_NO_EMPTY);
468
469        if (!is_array($parts) || count($parts) === 0) {
469        if (!is_array($parts) || count($parts) === 0) {
469        if (!is_array($parts) || count($parts) === 0) {
473        $result = [];
474
475        foreach ($parts as $part) {
475        foreach ($parts as $part) {
476            if ($part === '.' || $part === '..') {
476            if ($part === '.' || $part === '..') {
476            if ($part === '.' || $part === '..') {
483        $path = implode(DIRECTORY_SEPARATOR, $result);
483        $path = implode(DIRECTORY_SEPARATOR, $result);
483        $path = implode(DIRECTORY_SEPARATOR, $result);
484
485        return $path !== '' ? $path : 'query.sql';
485        return $path !== '' ? $path : 'query.sql';
485        return $path !== '' ? $path : 'query.sql';
485        return $path !== '' ? $path : 'query.sql';
486    }
Debug->safeSegment
488    private static function safeSegment(string $segment): string
489    {
490        $result = preg_replace('/[^A-Za-z0-9._:-]+/', '_', $segment);
490        $result = preg_replace('/[^A-Za-z0-9._:-]+/', '_', $segment);
490        $result = preg_replace('/[^A-Za-z0-9._:-]+/', '_', $segment);
490        $result = preg_replace('/[^A-Za-z0-9._:-]+/', '_', $segment);
491        $result = trim($result ?? '', '.');
491        $result = trim($result ?? '', '.');
491        $result = trim($result ?? '', '.');
491        $result = trim($result ?? '', '.');
492
493        return $result === '' ? '_' : $result;
493        return $result === '' ? '_' : $result;
493        return $result === '' ? '_' : $result;
493        return $result === '' ? '_' : $result;
494    }
Debug->server
447    private static function server(string $name): ?string
448    {
449        $value = $_SERVER[$name] ?? null;
450
451        if (is_float($value) || is_int($value)) {
451        if (is_float($value) || is_int($value)) {
451        if (is_float($value) || is_int($value)) {
452            return (string) $value;
455        return is_string($value) && $value !== '' ? $value : null;
455        return is_string($value) && $value !== '' ? $value : null;
455        return is_string($value) && $value !== '' ? $value : null;
455        return is_string($value) && $value !== '' ? $value : null;
455        return is_string($value) && $value !== '' ? $value : null;
455        return is_string($value) && $value !== '' ? $value : null;
456    }
Debug->session
352        [$key, $session] = self::sessionInfo();
353
354        if (self::$sessionKey !== $key) {
355            self::$sessionKey = $key;
356            self::$session = $session;
357            self::$counter = 0;
358        }
359
360        return self::$session ?? $session;
360        return self::$session ?? $session;
361    }
Debug->sessionInfo
366        $explicit = self::env('QUMA_DEBUG_SESSION');
367
368        if ($explicit !== null) {
369            return ['env:' . $explicit, self::sessionLabel($explicit)];
372        $method = self::server('REQUEST_METHOD');
373        $requestUri = self::server('REQUEST_URI');
374
375        if ($method !== null || $requestUri !== null) {
375        if ($method !== null || $requestUri !== null) {
375        if ($method !== null || $requestUri !== null) {
376            $method = strtoupper($method ?? 'HTTP');
377            $uri = $requestUri ?? self::server('SCRIPT_NAME') ?? self::server('PHP_SELF') ?? '/';
378            $time = self::requestTime();
379            $label = self::uriLabel($uri);
380            $hash = self::hash([$time, $method, $uri, (string) getmypid()]);
381
382            return ["http:{$time}:{$method}:{$uri}", "{$time}--{$method}--{$label}--{$hash}"];
385        $time = self::requestTime();
386        $hash = self::hash([$time, (string) getmypid(), self::argv()]);
387
388        return ["cli:{$time}", "{$time}--cli--{$hash}"];
389    }
Debug->sessionLabel
417    private static function sessionLabel(string $value): string
418    {
419        $label = self::safeSegment($value);
420
421        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
421        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
421        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
421        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
421        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
421        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
421        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
422    }
Debug->sessionPath
325    private static function sessionPath(?string $sourcePath, array $roots): string
326    {
327        return self::session()
328        . DIRECTORY_SEPARATOR
329        . self::counter()
330        . '--'
331        . self::sourceName($sourcePath, $roots);
332    }
Debug->sourceName
335    private static function sourceName(?string $sourcePath, array $roots): string
336    {
337        if ($sourcePath === null) {
338            return 'execute.sql';
341        $relative = self::relativeToRoots($sourcePath, $roots);
342        $dir = dirname($relative);
342        $dir = dirname($relative);
342        $dir = dirname($relative);
342        $dir = dirname($relative);
343        $name = pathinfo($relative, PATHINFO_FILENAME);
344        $name = $name !== '' ? $name : 'query';
344        $name = $name !== '' ? $name : 'query';
344        $name = $name !== '' ? $name : 'query';
344        $name = $name !== '' ? $name : 'query';
345        $path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name . '.sql';
345        $path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name . '.sql';
345        $path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name . '.sql';
345        $path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name . '.sql';
346
347        return preg_replace('/[\\/]+/', '--', $path) ?? 'query.sql';
348    }
Debug->uriLabel
424    private static function uriLabel(string $uri): string
425    {
426        $path = parse_url($uri, PHP_URL_PATH);
427        $path = is_string($path) && $path !== '' ? $path : '/';
427        $path = is_string($path) && $path !== '' ? $path : '/';
427        $path = is_string($path) && $path !== '' ? $path : '/';
427        $path = is_string($path) && $path !== '' ? $path : '/';
427        $path = is_string($path) && $path !== '' ? $path : '/';
427        $path = is_string($path) && $path !== '' ? $path : '/';
428        $label = trim($path, '/');
428        $label = trim($path, '/');
428        $label = trim($path, '/');
428        $label = trim($path, '/');
429
430        if ($label === '') {
431            return 'root';
434        $label = preg_replace('/[^A-Za-z0-9._:-]+/', '-', $label) ?? 'request';
435        $label = trim($label, '-.');
435        $label = trim($label, '-.');
435        $label = trim($label, '-.');
435        $label = trim($label, '-.');
436        $label = $label !== '' ? $label : 'request';
436        $label = $label !== '' ? $label : 'request';
436        $label = $label !== '' ? $label : 'request';
436        $label = $label !== '' ? $label : 'request';
437
438        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
438        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
438        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
438        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
438        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
438        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
438        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
439    }
Debug->value
115    private static function value(mixed $value): string
116    {
117        if (is_string($value)) {
118            return "'" . $value . "'";
121        if (is_array($value)) {
122            $encoded = json_encode($value);
123
124            return "'" . ($encoded !== false ? $encoded : '[]') . "'";
124            return "'" . ($encoded !== false ? $encoded : '[]') . "'";
124            return "'" . ($encoded !== false ? $encoded : '[]') . "'";
124            return "'" . ($encoded !== false ? $encoded : '[]') . "'";
127        if (is_null($value)) {
128            return 'NULL';
131        if (is_bool($value)) {
132            return $value ? 'true' : 'false';
132            return $value ? 'true' : 'false';
132            return $value ? 'true' : 'false';
132            return $value ? 'true' : 'false';
135        return (string) $value;
136    }
Debug->write
283        string $dir,
284        string $relative,
285        string $source,
286    ): void {
287        $path = $dir . DIRECTORY_SEPARATOR . self::safeRelativePath($relative);
288        $targetDir = dirname($path);
288        $targetDir = dirname($path);
288        $targetDir = dirname($path);
288        $targetDir = dirname($path);
289
290        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
290        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
290        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
290        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
290        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
294        if (file_put_contents($path, $source, LOCK_EX) === false) {
297    }
Debug->writeEnv
267    private static function writeEnv(string $name, ?string $relative, string $source): void
268    {
269        if ($relative === null) {
273        $dir = self::dir($name);
274
275        if ($dir === null) {
279        self::write($dir, $relative, $source);
280    }
Debug->writesInterpolated
95        return self::env(self::ENV_INTERPOLATED) !== null;
96    }
Debug->writesTranslated
90        return self::env(self::ENV_TRANSLATED) !== null;
91    }
{closure:/workspace/celemas/quma/src/Debug.php:190-194}
190            static function (mixed $value, int|string $key) use (&$map): void {
191                if (is_string($key) && $key !== '') {
191                if (is_string($key) && $key !== '') {
191                if (is_string($key) && $key !== '') {
192                    $map[':' . $key] = self::value($value);
193                }
194            },
194            },
{closure:/workspace/celemas/quma/src/Debug.php:207-210}
207            static function (mixed $value) use (&$result): void {
208                $replaced = preg_replace('/\\?/', self::value($value), $result, 1);
209                $result = $replaced ?? $result;
210            },