Interpolator   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 134
Duplicated Lines 0 %

Test Coverage

Coverage 89.47%

Importance

Changes 0
Metric Value
eloc 68
dl 0
loc 134
ccs 34
cts 38
cp 0.8947
rs 10
c 0
b 0
f 0
wmc 24

4 Methods

Rating   Name   Duplication   Size   Complexity  
A escapeStringValue() 0 13 1
B interpolate() 0 33 7
A categorizeParameters() 0 14 3
C resolveValue() 0 42 13
1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Query;
13
14
use Cycle\Database\Injection\ParameterInterface;
15
16
/**
17
 * Simple helper class used to interpolate query with given values. To be used for profiling and
18
 * debug purposes only.
19
 */
20
final class Interpolator
21
{
22
    private const DEFAULT_DATETIME_FORMAT = \DateTimeInterface::ATOM;
23
    private const DATETIME_WITH_MICROSECONDS_FORMAT = 'Y-m-d H:i:s.u';
24
25
    /**
26
     * Injects parameters into statement. For debug purposes only.
27
     *
28
     * @param non-empty-string $query
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
29
     *
30 3730
     * @return non-empty-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
31
     */
32 3730
    public static function interpolate(string $query, iterable $parameters = [], array $options = []): string
33 3724
    {
34
        if ($parameters === []) {
35
            return $query;
36
        }
37 2026
38 2026
        ['named' => $named, 'unnamed' => $unnamed] = self::categorizeParameters($parameters);
39 20
40 20
        return \preg_replace_callback(
41 20
            '/(?<dq>"(?:\\\\\"|[^"])*")|(?<sq>\'(?:\\\\\'|[^\'])*\')|(?<ph>\\?)|(?<named>:[a-z_\\d]+)/',
42
            static function ($match) use (&$named, &$unnamed, $options) {
43
                $key = match (true) {
44 20
                    isset($match['named']) && $match['named'] !== '' => \ltrim($match['named'], ':'),
45
                    isset($match['ph']) => $match['ph'],
46
                    default => null,
47 2022
                };
48 2022
49 2022
                switch (true) {
50
                    case $key === '?':
51
                        if (\key($unnamed) === null) {
52
                            return $match[0];
53
                        }
54 2026
55
                        $value = \current($unnamed);
56
                        \next($unnamed);
57
                        return self::resolveValue($value, $options);
58
                    case isset($named[$key]) || \array_key_exists($key, $named):
59
                        return self::resolveValue($named[$key], $options);
60
                    default:
61
                        return $match[0];
62 2026
                }
63
            },
64 2026
            $query,
65 432
        );
66
    }
67
68 2026
    /**
69 2026
     * Get parameter value.
70
     *
71
     * @psalm-return non-empty-string
72 2026
     */
73 590
    public static function resolveValue(mixed $parameter, array $options): string
74
    {
75 2026
        if ($parameter instanceof ParameterInterface) {
76 24
            return self::resolveValue($parameter->getValue(), $options);
77
        }
78 2026
79
        /** @since PHP 8.1 */
80
        if ($parameter instanceof \BackedEnum) {
0 ignored issues
show
Bug introduced by
The type BackedEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
81 2026
            $parameter = $parameter->value;
82 2026
        }
83
84 20
        switch (\gettype($parameter)) {
85 12
            case 'boolean':
86
                return $parameter ? 'TRUE' : 'FALSE';
87
88
            case 'integer':
89 12
                return (string) $parameter;
90 12
91
            case 'NULL':
92
                return 'NULL';
93
94 8
            case 'double':
95
                return \sprintf('%F', $parameter);
96
97
            case 'string':
98
                return "'" . self::escapeStringValue($parameter, "'") . "'";
0 ignored issues
show
Unused Code introduced by
The call to Cycle\Database\Query\Int...or::escapeStringValue() has too many arguments starting with '''. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

98
                return "'" . self::/** @scrutinizer ignore-call */ escapeStringValue($parameter, "'") . "'";

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
99
100
            case 'object':
101
                if ($parameter instanceof \Stringable) {
102
                    return "'" . self::escapeStringValue((string) $parameter, "'") . "'";
103
                }
104
105
                if ($parameter instanceof \DateTimeInterface) {
106
                    $format = $options['withDatetimeMicroseconds'] ?? false
107
                        ? self::DATETIME_WITH_MICROSECONDS_FORMAT
108 2022
                        : self::DEFAULT_DATETIME_FORMAT;
109
110
                    return "'" . $parameter->format($format) . "'";
111
                }
112
        }
113 2022
114 2022
        return '[UNRESOLVED]';
115 2022
    }
116
117
    private static function escapeStringValue(string $value): string
118
    {
119
        return \strtr($value, [
120
            '\\%' => '\\%',
121
            '\\_' => '\\_',
122
            \chr(26) => '\\Z',
123
            \chr(0) => '\\0',
124
            "'" => "\\'",
125
            \chr(8) => '\\b',
126
            "\n" => '\\n',
127
            "\r" => '\\r',
128
            "\t" => '\\t',
129
            '\\' => '\\\\',
130
        ]);
131
    }
132
133
    /**
134
     * Categorizes parameters into named and unnamed.
135
     *
136
     * @param iterable $parameters Parameters to categorize.
137
     *
138
     * @return array{named: array<string, mixed>, unnamed: list<mixed>} An associative array with keys 'named' and 'unnamed'.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{named: array<strin..., unnamed: list<mixed>} at position 13 could not be parsed: Expected '}' at position 13, but found 'list'.
Loading history...
139
     */
140
    private static function categorizeParameters(iterable $parameters): array
141
    {
142
        $named = [];
143
        $unnamed = [];
144
145
        foreach ($parameters as $k => $v) {
146
            if (\is_int($k)) {
147
                $unnamed[] = $v;
148
            } else {
149
                $named[\ltrim($k, ':')] = $v;
150
            }
151
        }
152
153
        return ['named' => $named, 'unnamed' => $unnamed];
154
    }
155
}
156