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

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
405        $argv = $_SERVER['argv'] ?? null;
406
407        return is_array($argv) ? implode(' ', $argv) : '';
407        return is_array($argv) ? implode(' ', $argv) : '';
407        return is_array($argv) ? implode(' ', $argv) : '';
407        return is_array($argv) ? implode(' ', $argv) : '';
407        return is_array($argv) ? implode(' ', $argv) : '';
407        return is_array($argv) ? implode(' ', $argv) : '';
407        return is_array($argv) ? implode(' ', $argv) : '';
408    }
Debug->counter
453        self::$counter++;
454
455        return str_pad((string) self::$counter, 4, '0', STR_PAD_LEFT);
456    }
Debug->dir
233    private static function dir(string $name): ?string
234    {
235        $dir = self::env($name);
236
237        if ($dir === null) {
241        if (!is_dir($dir)) {
242            throw new RuntimeException("Quma debug directory does not exist for {$name}: {$dir}");
245        if (!is_writable($dir)) {
249        $path = realpath($dir);
250
251        if ($path === false || $path === '') {
251        if ($path === false || $path === '') {
251        if ($path === false || $path === '') {
255        return $path;
256    }
Debug->enabled
71        $value = self::env(self::ENV_DEBUG);
72
73        return $value !== null && self::flag($value);
73        return $value !== null && self::flag($value);
73        return $value !== null && self::flag($value);
74    }
Debug->env
215    private static function env(string $name): ?string
216    {
217        $value = getenv($name);
218
219        if ($value === false) {
220            $value = $_SERVER[$name] ?? $_ENV[$name] ?? null;
221        }
222
223        if (!is_string($value)) {
223        if (!is_string($value)) {
224            return null;
227        $value = trim($value);
227        $value = trim($value);
227        $value = trim($value);
227        $value = trim($value);
228
229        return $value === '' ? null : $value;
229        return $value === '' ? null : $value;
229        return $value === '' ? null : $value;
229        return $value === '' ? null : $value;
230    }
Debug->flag
207    private static function flag(string $value): bool
208    {
209        return match (strtolower($value)) {
210            '1', 'true', 'yes', 'on' => true,
211            default => false,
211            default => false,
212        };
213    }
Debug->hash
435    private static function hash(array $parts): string
436    {
437        return substr(hash('xxh128', implode("\0", $parts)), 0, 8);
437        return substr(hash('xxh128', implode("\0", $parts)), 0, 8);
437        return substr(hash('xxh128', implode("\0", $parts)), 0, 8);
437        return substr(hash('xxh128', implode("\0", $parts)), 0, 8);
438    }
Debug->interpolate
56    public static function interpolate(string $query, Args $args): string
57    {
58        $prep = self::prepareQuery($query);
59
60        if ($args->type() === ArgType::Named) {
60        if ($args->type() === ArgType::Named) {
61            $interpolated = self::interpolateNamed($prep->query, $args->getNamed());
63            $interpolated = self::interpolatePositional($prep->query, $args->get());
64        }
65
66        return self::restoreQuery($interpolated, $prep);
66        return self::restoreQuery($interpolated, $prep);
67    }
Debug->interpolateNamed
175    private static function interpolateNamed(string $query, array $args): string
176    {
177        $map = [];
178
179        array_walk(
180            $args,
181            static function (mixed $value, int|string $key) use (&$map): void {
182                if (is_string($key) && $key !== '') {
183                    $map[':' . $key] = self::value($value);
184                }
185            },
186        );
187
188        return strtr($query, $map);
188        return strtr($query, $map);
188        return strtr($query, $map);
188        return strtr($query, $map);
189    }
Debug->interpolatePositional
192    private static function interpolatePositional(string $query, array $args): string
193    {
194        $result = $query;
195
196        array_walk(
197            $args,
198            static function (mixed $value) use (&$result): void {
199                $replaced = preg_replace('/\\?/', self::value($value), $result, 1);
200                $result = $replaced ?? $result;
201            },
202        );
203
204        return $result;
205    }
Debug->prepareQuery
129    private static function prepareQuery(string $query): PreparedQuery
130    {
131        $patterns = [
132            Query::PATTERN_BLOCK,
133            Query::PATTERN_STRING,
134            Query::PATTERN_COMMENT_MULTI,
135            Query::PATTERN_COMMENT_SINGLE,
136        ];
137
138        $swaps = [];
139        $i = 0;
140
141        do {
142            $found = false;
142            $found = false;
143
144            foreach ($patterns as $pattern) {
144            foreach ($patterns as $pattern) {
145                $matches = [];
146
147                if (preg_match($pattern, $query, $matches) === 1) {
148                    $match = $matches[0];
149                    $replacement = "___CHUCK_REPLACE_{$i}___";
150                    assert($match !== '', 'Query placeholder match must not be empty.');
151                    $swaps[$replacement] = $match;
152
153                    $query = preg_replace($pattern, $replacement, $query, limit: 1) ?? $query;
154                    $found = true;
155                    $i++;
156
157                    break;
144            foreach ($patterns as $pattern) {
144            foreach ($patterns as $pattern) {
145                $matches = [];
146
147                if (preg_match($pattern, $query, $matches) === 1) {
148                    $match = $matches[0];
149                    $replacement = "___CHUCK_REPLACE_{$i}___";
150                    assert($match !== '', 'Query placeholder match must not be empty.');
151                    $swaps[$replacement] = $match;
152
153                    $query = preg_replace($pattern, $replacement, $query, limit: 1) ?? $query;
154                    $found = true;
155                    $i++;
156
157                    break;
158                }
159            }
160        } while ($found);
162        return new PreparedQuery($query, $swaps);
163    }
Debug->printQuery
97    private static function printQuery(string $query): void
98    {
99        error_log(
100            "\n\n-----------------------------------------------\n\n"
101                . $query
102                . "\n------------------------------------------------\n",
103        );
104    }
Debug->prints
78        $value = self::env(self::ENV_PRINT);
79
80        return $value !== null && self::flag($value);
80        return $value !== null && self::flag($value);
80        return $value !== null && self::flag($value);
81    }
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        $path = $writeTranslated || $writeInterpolated
33        $path = $writeTranslated || $writeInterpolated
33        $path = $writeTranslated || $writeInterpolated
34            ? self::sessionPath($sourcePath, $db->getSqlDirs())
33        $path = $writeTranslated || $writeInterpolated
34            ? self::sessionPath($sourcePath, $db->getSqlDirs())
35            : null;
33        $path = $writeTranslated || $writeInterpolated
34            ? self::sessionPath($sourcePath, $db->getSqlDirs())
35            : null;
36
37        if ($writeTranslated) {
38            self::writeEnv(self::ENV_TRANSLATED, $path, $query);
39        }
40
41        if (!$print && !$writeInterpolated) {
41        if (!$print && !$writeInterpolated) {
41        if (!$print && !$writeInterpolated) {
41        if (!$print && !$writeInterpolated) {
42            return;
45        $interpolated = self::interpolate($query, $args);
46
47        if ($print) {
48            self::printQuery($interpolated);
49        }
50
51        if ($writeInterpolated) {
51        if ($writeInterpolated) {
52            self::writeEnv(self::ENV_INTERPOLATED, $path, $interpolated);
53        }
54    }
54    }
Debug->relativeToRoots
291    private static function relativeToRoots(string $sourcePath, array $roots): string
292    {
293        $realSource = realpath($sourcePath);
294        $source = $realSource !== false ? $realSource : $sourcePath;
294        $source = $realSource !== false ? $realSource : $sourcePath;
294        $source = $realSource !== false ? $realSource : $sourcePath;
294        $source = $realSource !== false ? $realSource : $sourcePath;
295
296        foreach ($roots as $root) {
296        foreach ($roots as $root) {
297            if (!is_string($root) || $root === '') {
297            if (!is_string($root) || $root === '') {
297            if (!is_string($root) || $root === '') {
301            $realRoot = realpath($root);
302            $root = $realRoot !== false ? $realRoot : $root;
302            $root = $realRoot !== false ? $realRoot : $root;
302            $root = $realRoot !== false ? $realRoot : $root;
302            $root = $realRoot !== false ? $realRoot : $root;
303            $prefix = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
304
305            if (str_starts_with($source, $prefix)) {
305            if (str_starts_with($source, $prefix)) {
305            if (str_starts_with($source, $prefix)) {
305            if (str_starts_with($source, $prefix)) {
306                $relative = substr($source, strlen($prefix));
306                $relative = substr($source, strlen($prefix));
306                $relative = substr($source, strlen($prefix));
306                $relative = substr($source, strlen($prefix));
307
308                return $relative === '' ? basename($sourcePath) : $relative;
308                return $relative === '' ? basename($sourcePath) : $relative;
308                return $relative === '' ? basename($sourcePath) : $relative;
308                return $relative === '' ? basename($sourcePath) : $relative;
296        foreach ($roots as $root) {
Debug->requestTime
384        $time = self::server('REQUEST_TIME_FLOAT');
385
386        if ($time !== null && is_numeric($time)) {
386        if ($time !== null && is_numeric($time)) {
386        if ($time !== null && is_numeric($time)) {
386        if ($time !== null && is_numeric($time)) {
386        if ($time !== null && is_numeric($time)) {
386        if ($time !== null && is_numeric($time)) {
387            $date = DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', (float) $time));
388
389            if ($date instanceof DateTimeImmutable) {
390                return $date->format('Ymd-His-u');
394        $time = self::server('REQUEST_TIME');
395
396        if ($time !== null && ctype_digit($time)) {
396        if ($time !== null && ctype_digit($time)) {
396        if ($time !== null && ctype_digit($time)) {
397            return new DateTimeImmutable('@' . $time)->format('Ymd-His') . '-000000';
400        return self::$fallbackTime ??= new DateTimeImmutable()->format('Ymd-His-u');
401    }
Debug->restoreQuery
165    private static function restoreQuery(string $query, PreparedQuery $prep): string
166    {
167        foreach ($prep->swaps as $swap => $replacement) {
167        foreach ($prep->swaps as $swap => $replacement) {
167        foreach ($prep->swaps as $swap => $replacement) {
168            $query = str_replace($swap, $replacement, $query);
168            $query = str_replace($swap, $replacement, $query);
168            $query = str_replace($swap, $replacement, $query);
167        foreach ($prep->swaps as $swap => $replacement) {
168            $query = str_replace($swap, $replacement, $query);
167        foreach ($prep->swaps as $swap => $replacement) {
168            $query = str_replace($swap, $replacement, $query);
169        }
170
171        return $query;
172    }
Debug->safeRelativePath
458    private static function safeRelativePath(string $path): string
459    {
460        $parts = preg_split('/[\\/]+/', $path, -1, PREG_SPLIT_NO_EMPTY);
461
462        if (!is_array($parts) || count($parts) === 0) {
462        if (!is_array($parts) || count($parts) === 0) {
462        if (!is_array($parts) || count($parts) === 0) {
466        $result = [];
467
468        foreach ($parts as $part) {
468        foreach ($parts as $part) {
469            if ($part === '.' || $part === '..') {
469            if ($part === '.' || $part === '..') {
469            if ($part === '.' || $part === '..') {
476        $path = implode(DIRECTORY_SEPARATOR, $result);
476        $path = implode(DIRECTORY_SEPARATOR, $result);
476        $path = implode(DIRECTORY_SEPARATOR, $result);
477
478        return $path !== '' ? $path : 'query.sql';
478        return $path !== '' ? $path : 'query.sql';
478        return $path !== '' ? $path : 'query.sql';
478        return $path !== '' ? $path : 'query.sql';
479    }
Debug->safeSegment
481    private static function safeSegment(string $segment): string
482    {
483        $result = preg_replace('/[^A-Za-z0-9._:-]+/', '_', $segment);
483        $result = preg_replace('/[^A-Za-z0-9._:-]+/', '_', $segment);
483        $result = preg_replace('/[^A-Za-z0-9._:-]+/', '_', $segment);
483        $result = preg_replace('/[^A-Za-z0-9._:-]+/', '_', $segment);
484        $result = trim($result ?? '', '.');
484        $result = trim($result ?? '', '.');
484        $result = trim($result ?? '', '.');
484        $result = trim($result ?? '', '.');
485
486        return $result === '' ? '_' : $result;
486        return $result === '' ? '_' : $result;
486        return $result === '' ? '_' : $result;
486        return $result === '' ? '_' : $result;
487    }
Debug->server
440    private static function server(string $name): ?string
441    {
442        $value = $_SERVER[$name] ?? null;
443
444        if (is_float($value) || is_int($value)) {
444        if (is_float($value) || is_int($value)) {
444        if (is_float($value) || is_int($value)) {
445            return (string) $value;
448        return is_string($value) && $value !== '' ? $value : null;
448        return is_string($value) && $value !== '' ? $value : null;
448        return is_string($value) && $value !== '' ? $value : null;
448        return is_string($value) && $value !== '' ? $value : null;
448        return is_string($value) && $value !== '' ? $value : null;
448        return is_string($value) && $value !== '' ? $value : null;
449    }
Debug->session
343        [$key, $session] = self::sessionInfo();
344
345        if (self::$sessionKey !== $key) {
346            self::$sessionKey = $key;
347            self::$session = $session;
348            self::$counter = 0;
349        }
350
351        return self::$session ?? $session;
351        return self::$session ?? $session;
352    }
Debug->sessionInfo
357        $explicit = self::env('QUMA_DEBUG_SESSION');
358
359        if ($explicit !== null) {
360            return ['env:' . $explicit, self::sessionLabel($explicit)];
363        $method = self::server('REQUEST_METHOD');
364        $requestUri = self::server('REQUEST_URI');
365
366        if ($method !== null || $requestUri !== null) {
366        if ($method !== null || $requestUri !== null) {
366        if ($method !== null || $requestUri !== null) {
367            $method = strtoupper($method ?? 'HTTP');
368            $uri = $requestUri ?? self::server('SCRIPT_NAME') ?? self::server('PHP_SELF') ?? '/';
369            $time = self::requestTime();
370            $label = self::uriLabel($uri);
371            $hash = self::hash([$time, $method, $uri, (string) getmypid()]);
372
373            return ["http:{$time}:{$method}:{$uri}", "{$time}--{$method}--{$label}--{$hash}"];
376        $time = self::requestTime();
377        $hash = self::hash([$time, (string) getmypid(), self::argv()]);
378
379        return ["cli:{$time}", "{$time}--cli--{$hash}"];
380    }
Debug->sessionLabel
410    private static function sessionLabel(string $value): string
411    {
412        $label = self::safeSegment($value);
413
414        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
414        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
414        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
414        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
414        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
414        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
414        return strlen($label) <= 96 ? $label : substr($label, 0, 96);
415    }
Debug->sessionPath
316    private static function sessionPath(?string $sourcePath, array $roots): string
317    {
318        return self::session()
319            . DIRECTORY_SEPARATOR
320            . self::counter()
321            . '--'
322            . self::sourceName($sourcePath, $roots);
323    }
Debug->sourceName
326    private static function sourceName(?string $sourcePath, array $roots): string
327    {
328        if ($sourcePath === null) {
329            return 'execute.sql';
332        $relative = self::relativeToRoots($sourcePath, $roots);
333        $dir = dirname($relative);
333        $dir = dirname($relative);
333        $dir = dirname($relative);
333        $dir = dirname($relative);
334        $name = pathinfo($relative, PATHINFO_FILENAME);
335        $name = $name !== '' ? $name : 'query';
335        $name = $name !== '' ? $name : 'query';
335        $name = $name !== '' ? $name : 'query';
335        $name = $name !== '' ? $name : 'query';
336        $path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name . '.sql';
336        $path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name . '.sql';
336        $path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name . '.sql';
336        $path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name . '.sql';
337
338        return preg_replace('/[\\/]+/', '--', $path) ?? 'query.sql';
339    }
Debug->uriLabel
417    private static function uriLabel(string $uri): string
418    {
419        $path = parse_url($uri, PHP_URL_PATH);
420        $path = is_string($path) && $path !== '' ? $path : '/';
420        $path = is_string($path) && $path !== '' ? $path : '/';
420        $path = is_string($path) && $path !== '' ? $path : '/';
420        $path = is_string($path) && $path !== '' ? $path : '/';
420        $path = is_string($path) && $path !== '' ? $path : '/';
420        $path = is_string($path) && $path !== '' ? $path : '/';
421        $label = trim($path, '/');
421        $label = trim($path, '/');
421        $label = trim($path, '/');
421        $label = trim($path, '/');
422
423        if ($label === '') {
424            return 'root';
427        $label = preg_replace('/[^A-Za-z0-9._:-]+/', '-', $label) ?? 'request';
428        $label = trim($label, '-.');
428        $label = trim($label, '-.');
428        $label = trim($label, '-.');
428        $label = trim($label, '-.');
429        $label = $label !== '' ? $label : 'request';
429        $label = $label !== '' ? $label : 'request';
429        $label = $label !== '' ? $label : 'request';
429        $label = $label !== '' ? $label : 'request';
430
431        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
431        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
431        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
431        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
431        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
431        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
431        return strlen($label) <= 64 ? $label : substr($label, 0, 64);
432    }
Debug->value
106    private static function value(mixed $value): string
107    {
108        if (is_string($value)) {
109            return "'" . $value . "'";
112        if (is_array($value)) {
113            $encoded = json_encode($value);
114
115            return "'" . ($encoded !== false ? $encoded : '[]') . "'";
115            return "'" . ($encoded !== false ? $encoded : '[]') . "'";
115            return "'" . ($encoded !== false ? $encoded : '[]') . "'";
115            return "'" . ($encoded !== false ? $encoded : '[]') . "'";
118        if (is_null($value)) {
119            return 'NULL';
122        if (is_bool($value)) {
123            return $value ? 'true' : 'false';
123            return $value ? 'true' : 'false';
123            return $value ? 'true' : 'false';
123            return $value ? 'true' : 'false';
126        return (string) $value;
127    }
Debug->write
274        string $dir,
275        string $relative,
276        string $source,
277    ): void {
278        $path = $dir . DIRECTORY_SEPARATOR . self::safeRelativePath($relative);
279        $targetDir = dirname($path);
279        $targetDir = dirname($path);
279        $targetDir = dirname($path);
279        $targetDir = dirname($path);
280
281        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
281        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
281        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
281        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
281        if (!is_dir($targetDir) && !mkdir($targetDir, 0o775, true) && !is_dir($targetDir)) {
285        if (file_put_contents($path, $source, LOCK_EX) === false) {
288    }
Debug->writeEnv
258    private static function writeEnv(string $name, ?string $relative, string $source): void
259    {
260        if ($relative === null) {
264        $dir = self::dir($name);
265
266        if ($dir === null) {
270        self::write($dir, $relative, $source);
271    }
Debug->writesInterpolated
90        return self::env(self::ENV_INTERPOLATED) !== null;
91    }
Debug->writesTranslated
85        return self::env(self::ENV_TRANSLATED) !== null;
86    }
{closure:/workspace/celema/quma/src/Debug.php:181-185}
181            static function (mixed $value, int|string $key) use (&$map): void {
182                if (is_string($key) && $key !== '') {
182                if (is_string($key) && $key !== '') {
182                if (is_string($key) && $key !== '') {
183                    $map[':' . $key] = self::value($value);
184                }
185            },
185            },
{closure:/workspace/celema/quma/src/Debug.php:198-201}
198            static function (mixed $value) use (&$result): void {
199                $replaced = preg_replace('/\\?/', self::value($value), $result, 1);
200                $result = $replaced ?? $result;
201            },