Passed
Push — 2.x ( 851e8e...386c26 )
by Maxim
20:16
created

Interpolator::categorizeParameters()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 0
loc 14
ccs 0
cts 0
cp 0
crap 12
rs 10
c 0
b 0
f 0
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 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...
15
use Cycle\Database\Injection\ParameterInterface;
16
use DateTimeInterface;
17
use Stringable;
18
19
/**
20
 * Simple helper class used to interpolate query with given values. To be used for profiling and
21
 * debug purposes only.
22
 */
23
final class Interpolator
24
{
25
    private const DEFAULT_DATETIME_FORMAT = DateTimeInterface::ATOM;
26
    private const DATETIME_WITH_MICROSECONDS_FORMAT = 'Y-m-d H:i:s.u';
27
28
    /**
29
     * Injects parameters into statement. For debug purposes only.
30 3730
     *
31
     * @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...
32 3730
     *
33 3724
     * @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...
34
     */
35
    public static function interpolate(string $query, iterable $parameters = [], array $options = []): string
36
    {
37 2026
        if ($parameters === []) {
38 2026
            return $query;
39 20
        }
40 20
41 20
        ['named' => $named, 'unnamed' => $unnamed] = self::categorizeParameters($parameters);
42
43
        return \preg_replace_callback(
44 20
            '/(?<dq>"(?:\\\\\"|[^"])*")|(?<sq>\'(?:\\\\\'|[^\'])*\')|(?<ph>\\?)|(?<named>:[a-z_\\d]+)/',
45
            static function ($match) use (&$named, &$unnamed, $options) {
46
                $key = match (true) {
47 2022
                    isset($match['named']) && '' !== $match['named'] => \ltrim($match['named'], ':'),
48 2022
                    isset($match['ph']) => $match['ph'],
49 2022
                    default => null
50
                };
51
52
                switch (true) {
53
                    case '?' === $key:
54 2026
                        if (null === \key($unnamed)) {
55
                            return $match[0];
56
                        }
57
58
                        $value = \current($unnamed);
59
                        \next($unnamed);
60
                        return self::resolveValue($value, $options);
61
                    case isset($named[$key]) || \array_key_exists($key, $named):
62 2026
                        return self::resolveValue($named[$key], $options);
63
                    default:
64 2026
                        return $match[0];
65 432
                }
66
            },
67
            $query
68 2026
        );
69 2026
    }
70
71
    /**
72 2026
     * Get parameter value.
73 590
     *
74
     * @psalm-return non-empty-string
75 2026
     */
76 24
    public static function resolveValue(mixed $parameter, array $options): string
77
    {
78 2026
        if ($parameter instanceof ParameterInterface) {
79
            return self::resolveValue($parameter->getValue(), $options);
80
        }
81 2026
82 2026
        /** @since PHP 8.1 */
83
        if ($parameter instanceof BackedEnum) {
84 20
            $parameter = $parameter->value;
85 12
        }
86
87
        switch (\gettype($parameter)) {
88
            case 'boolean':
89 12
                return $parameter ? 'TRUE' : 'FALSE';
90 12
91
            case 'integer':
92
                return (string)$parameter;
93
94 8
            case 'NULL':
95
                return 'NULL';
96
97
            case 'double':
98
                return \sprintf('%F', $parameter);
99
100
            case 'string':
101
                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

101
                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...
102
103
            case 'object':
104
                if ($parameter instanceof Stringable) {
105
                    return "'" . self::escapeStringValue((string)$parameter, "'") . "'";
106
                }
107
108 2022
                if ($parameter instanceof DateTimeInterface) {
109
                    $format = $options['withDatetimeMicroseconds'] ?? false
110
                        ? self::DATETIME_WITH_MICROSECONDS_FORMAT
111
                        : self::DEFAULT_DATETIME_FORMAT;
112
113 2022
                    return "'" . $parameter->format($format) . "'";
114 2022
                }
115 2022
        }
116
117
        return '[UNRESOLVED]';
118
    }
119
120
    private static function escapeStringValue(string $value): string
121
    {
122
        return \strtr($value, [
123
            '\\%' => '\\%',
124
            '\\_' => '\\_',
125
            \chr(26) => '\\Z',
126
            \chr(0) => '\\0',
127
            "'" => "\\'",
128
            \chr(8) => '\\b',
129
            "\n" => '\\n',
130
            "\r" => '\\r',
131
            "\t" => '\\t',
132
            '\\' => '\\\\',
133
        ]);
134
    }
135
136
    /**
137
     * Categorizes parameters into named and unnamed.
138
     *
139
     * @param iterable $parameters Parameters to categorize.
140
     *
141
     * @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...
142
     */
143
    private static function categorizeParameters(iterable $parameters): array
144
    {
145
        $named = [];
146
        $unnamed = [];
147
148
        foreach ($parameters as $k => $v) {
149
            if (\is_int($k)) {
150
                $unnamed[] = $v;
151
            } else {
152
                $named[\ltrim($k, ':')] = $v;
153
            }
154
        }
155
156
        return ['named' => $named, 'unnamed' => $unnamed];
157
    }
158
}
159