Passed
Pull Request — 2.x (#67)
by Aleksei
18:49
created

Interpolator::resolveValue()   C

Complexity

Conditions 12
Paths 21

Size

Total Lines 38
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 12.5669

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 21
nc 21
nop 1
dl 0
loc 38
ccs 16
cts 19
cp 0.8421
crap 12.5669
rs 6.9666
c 1
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    /**
26
     * Injects parameters into statement. For debug purposes only.
27
     *
28
     * @psalm-param non-empty-string $query
29
     *
30 3730
     * @psalm-return non-empty-string
31
     */
32 3730
    public static function interpolate(string $query, iterable $parameters = []): string
33 3724
    {
34
        if ($parameters === []) {
35
            return $query;
36
        }
37 2026
38 2026
        ['named' => $named, 'unnamed' => $unnamed] = self::normalizeParameters($parameters);
39 20
        $params = self::findParams($query);
40 20
41 20
        $caret = 0;
42
        $result = '';
43
        foreach ($params as $pos => $ph) {
44 20
            $result .= \substr($query, $caret, $pos - $caret);
45
            $caret = $pos + \strlen($ph);
46
            // find param
47 2022
            $result .= match (true) {
48 2022
                $ph === '?' && \count($unnamed) > 0 => self::resolveValue(\array_shift($unnamed)),
49 2022
                \array_key_exists($ph, $named) => self::resolveValue($named[$ph]),
50
                default => $ph,
51
            };
52
        }
53
        $result .= \substr($query, $caret);
54 2026
55
        return $result;
56
    }
57
58
    /**
59
     * Get parameter value.
60
     *
61
     * @psalm-return non-empty-string
62 2026
     */
63
    private static function resolveValue(mixed $parameter): string
64 2026
    {
65 432
        if ($parameter instanceof ParameterInterface) {
66
            return self::resolveValue($parameter->getValue());
67
        }
68 2026
69 2026
        /** @since PHP 8.1 */
70
        if ($parameter instanceof BackedEnum) {
71
            $parameter = $parameter->value;
72 2026
        }
73 590
74
        switch (\gettype($parameter)) {
75 2026
            case 'boolean':
76 24
                return $parameter ? 'TRUE' : 'FALSE';
77
78 2026
            case 'integer':
79
                return (string)$parameter;
80
81 2026
            case 'NULL':
82 2026
                return 'NULL';
83
84 20
            case 'double':
85 12
                return \sprintf('%F', $parameter);
86
87
            case 'string':
88
                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

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