Passed
Pull Request — 2.x (#60)
by
unknown
17:00
created

Interpolator::normalizeParameters()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 7
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 11
ccs 0
cts 0
cp 0
crap 12
rs 10
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
use DateTimeInterface;
16
use Stringable;
17
18
/**
19
 * Simple helper class used to interpolate query with given values. To be used for profiling and
20
 * debug purposes only.
21
 */
22
final class Interpolator
23
{
24
    /**
25
     * Injects parameters into statement. For debug purposes only.
26
     *
27
     * @psalm-param non-empty-string $query
28
     *
29
     * @psalm-return non-empty-string
30 3730
     */
31
    public static function interpolate(string $query, iterable $parameters = []): string
32 3730
    {
33 3724
        if ($parameters === []) {
34
            return $query;
35
        }
36
37 2026
        ['named' => $named, 'unnamed' => $unnamed] = self::normalizeParameters($parameters);
38 2026
        $params = self::findParams($query);
39 20
40 20
        $caret = 0;
41 20
        $result = '';
42
        foreach ($params as $pos => $ph) {
43
            $result .= \substr($query, $caret, $pos - $caret);
44 20
            $caret = $pos + \strlen($ph);
45
            // find param
46
            $result .= match (true) {
47 2022
                $ph === '?' && \count($unnamed) > 0 => self::resolveValue(\array_shift($unnamed)),
48 2022
                \array_key_exists($ph, $named) => self::resolveValue($named[$ph]),
49 2022
                default => $ph,
50
            };
51
        }
52
        $result .= \substr($query, $caret);
53
54 2026
        return $result;
55
    }
56
57
    /**
58
     * Get parameter value.
59
     *
60
     * @psalm-return non-empty-string
61
     */
62 2026
    private static function resolveValue(mixed $parameter): string
63
    {
64 2026
        if ($parameter instanceof ParameterInterface) {
65 432
            return self::resolveValue($parameter->getValue());
66
        }
67
68 2026
        switch (\gettype($parameter)) {
69 2026
            case 'boolean':
70
                return $parameter ? 'TRUE' : 'FALSE';
71
72 2026
            case 'integer':
73 590
                return (string)($parameter + 0);
74
75 2026
            case 'NULL':
76 24
                return 'NULL';
77
78 2026
            case 'double':
79
                return \sprintf('%F', $parameter);
80
81 2026
            case 'string':
82 2026
                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

82
                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...
83
84 20
            case 'object':
85 12
                if ($parameter instanceof Stringable) {
86
                    return "'" . self::escapeStringValue((string)$parameter, "'") . "'";
87
                }
88
89 12
                if ($parameter instanceof DateTimeInterface) {
90 12
                    return "'" . $parameter->format(DateTimeInterface::ATOM) . "'";
91
                }
92
        }
93
94 8
        return '[UNRESOLVED]';
95
    }
96
97
    private static function escapeStringValue(string $value): string
98
    {
99
        return \strtr($value, [
100
            '\\%' => '\\%',
101
            '\\_' => '\\_',
102
            \chr(26) => '\\Z',
103
            \chr(0) => '\\0',
104
            "'" => "\\'",
105
            \chr(8) => '\\b',
106
            "\n" => '\\n',
107
            "\r" => '\\r',
108 2022
            "\t" => '\\t',
109
            '\\' => '\\\\',
110
        ]);
111
    }
112
113 2022
    /**
114 2022
     * @return array<int, string>
115 2022
     */
116
    private static function findParams(string $query): array
117
    {
118
        \preg_match_all(
119
            '/(?<dq>"(?:\\\\\"|[^"])*")|(?<sq>\'(?:\\\\\'|[^\'])*\')|(?<ph>\\?)|(?<named>:[a-z_\\d]+)/',
120
            $query,
121
            $placeholders,
122
            PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL
123
        );
124
        $result = [];
125
        foreach ([...$placeholders['named'], ...$placeholders['ph']] as $tuple) {
126
            if ($tuple[0] === null) {
127
                continue;
128
            }
129
            $result[$tuple[1]] = $tuple[0];
130
        }
131
        \ksort($result);
132
133
        return $result;
134
    }
135
136
    /**
137
     * @return array{named: array, unnamed: array}
138
     */
139
    private static function normalizeParameters(iterable $parameters): array
140
    {
141
        $result = ['named' => [], 'unnamed' => []];
142
        foreach ($parameters as $k => $v) {
143
            if (\is_int($k)) {
144
                $result['unnamed'][$k] = $v;
145
            } else {
146
                $result['named'][':' . \ltrim($k, ':')] = $v;
147
            }
148
        }
149
        return $result;
150
    }
151
}
152