Passed
Push — main ( 2b5ece...48a507 )
by Peter
02:13
created

Expr::toParamArray()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 5
nop 3
dl 0
loc 20
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace QB\Generic\Expr;
6
7
use InvalidArgumentException;
8
use PDO;
9
use QB\Generic\IQueryPart;
10
11
class Expr implements IQueryPart
12
{
13
    public const PARAM_ALL_STRING = 1;
14
    public const PARAM_ALL_AUTO   = 2;
15
    public const PARAM_ALL_MANUAL = 4;
16
17
    protected string $sql;
18
19
    protected bool $useNamedParams;
20
21
    /** @var array<int,array<int,mixed>> */
22
    protected array $params = [];
23
24
    /** @var int Helps tracking the extensions done on the SQL originally received */
25
    protected int $extendedBy = 0;
26
27
    /**
28
     * Expr constructor.
29
     *
30
     * @param string|IQueryPart $sql
31
     * @param array             $params
32
     * @param int               $paramHandle
33
     */
34
    public function __construct(string|IQueryPart $sql, array $params = [], int $paramHandle = self::PARAM_ALL_AUTO)
35
    {
36
        $this->useNamedParams = !array_key_exists(0, $params);
37
38
        $this->validateParamHandle($paramHandle);
39
        $this->validateParamKeys(array_keys($params));
40
41
        $this->sql = (string)$sql;
42
43
        $this->bindParams($params, $paramHandle);
44
    }
45
46
    /**
47
     * @param int $paramHandle
48
     */
49
    protected function validateParamHandle(int $paramHandle): void
50
    {
51
        if (!in_array($paramHandle, [self::PARAM_ALL_STRING, self::PARAM_ALL_AUTO, self::PARAM_ALL_MANUAL])) {
52
            throw new InvalidArgumentException(
53
                sprintf('invalid param handle received: %d.', $paramHandle)
54
            );
55
        }
56
    }
57
58
    /**
59
     * @param array $paramKeys
60
     */
61
    protected function validateParamKeys(array $paramKeys): void
62
    {
63
        if ($this->useNamedParams) {
64
            foreach ($paramKeys as $paramKey) {
65
                if (is_int($paramKey)) {
66
                    throw new InvalidArgumentException(
67
                        sprintf('string param key was expected, int received: %d.', $paramKey)
68
                    );
69
                }
70
            }
71
72
            return;
73
        }
74
75
        $next = 0;
76
        foreach ($paramKeys as $paramKey) {
77
            if ($paramKey !== $next) {
78
                throw new InvalidArgumentException(
79
                    sprintf('key was expected to be %d, received: %s.', $next, $paramKey)
80
                );
81
            }
82
            $next++;
83
        }
84
    }
85
86
    /**
87
     * @param array $params
88
     * @param int   $paramHandle
89
     */
90
    protected function bindParams(array $params, int $paramHandle = self::PARAM_ALL_AUTO)
91
    {
92
        foreach ($params as $origKey => $origParam) {
93
            $fixedParam = $this->toParamArray($origKey, $origParam, $paramHandle);
94
            if (count($fixedParam) > 1) {
95
                if (!$this->useNamedParams) {
96
                    $this->expandSqlUnnamed($origKey, count($fixedParam));
97
                } else {
98
                    $this->expandSqlNamed($origKey, count($fixedParam));
99
                }
100
            }
101
102
            foreach ($fixedParam as $fixedKey => $var) {
103
                if ($this->useNamedParams) {
104
                    $this->params[$fixedKey] = $this->getFinalParam($var, $paramHandle);
105
                } else {
106
                    $this->params[] = $this->getFinalParam($var, $paramHandle);
107
                }
108
            }
109
        }
110
    }
111
112
    /**
113
     * @param string|int $origKey
114
     * @param            $param
115
     * @param int        $paramHandle
116
     *
117
     * @return array
118
     */
119
    protected function toParamArray(string|int $origKey, $param, int $paramHandle): array
120
    {
121
        if (!$this->isParamArray($param, $paramHandle)) {
122
            if ($this->useNamedParams) {
123
                return [$origKey => $param];
124
            }
125
126
            return [$param];
127
        }
128
129
        if (!$this->useNamedParams) {
130
            return $param;
131
        }
132
133
        $fixedParams = [];
134
        foreach ($param as $i => $p) {
135
            $fixedParams[$origKey . '_' . $i] = $p;
136
        }
137
138
        return $fixedParams;
139
    }
140
141
    /**
142
     * @param     $param
143
     * @param int $paramHandle
144
     *
145
     * @return bool
146
     */
147
    protected function isParamArray($param, int $paramHandle): bool
148
    {
149
        if (is_scalar($param) || is_null($param)) {
150
            return false;
151
        }
152
153
        if (!is_array($param)) {
154
            throw new InvalidArgumentException(sprintf('param must be scalar or array, %s received.', gettype($param)));
155
        }
156
157
        if (in_array($paramHandle, [self::PARAM_ALL_AUTO, self::PARAM_ALL_STRING], true)) {
158
            return true;
159
        }
160
161
        if (count($param) == 2 && is_scalar($param[0]) && is_int($param[1]) && $param[1] >= 0) {
162
            return false;
163
        }
164
165
        return true;
166
    }
167
168
    /**
169
     * @param int $origKey
170
     * @param int $fixedParamCount
171
     */
172
    protected function expandSqlUnnamed(int $origKey, int $fixedParamCount): void
173
    {
174
        $count = $fixedParamCount - 1;
175
        $pos   = $origKey + $this->extendedBy + $count;
176
177
        $parts = explode('?', $this->sql);
178
        $qs    = '?' . str_repeat(', ?', $count);
179
        $start = implode('?', array_slice($parts, 0, $pos));
180
        $end   = implode('?', array_slice($parts, $pos));
181
182
        $this->sql        = $start . $qs . $end;
183
        $this->extendedBy += $fixedParamCount - 1;
184
    }
185
186
    /**
187
     * @param string $origKey
188
     * @param int    $fixedParamCount
189
     */
190
    protected function expandSqlNamed(string $origKey, int $fixedParamCount): void
191
    {
192
        $searchKey = ':' . $origKey;
193
        $parts     = [];
194
        for ($i = 0; $i < $fixedParamCount; $i++) {
195
            $parts[] = $searchKey . '_' . $i;
196
        }
197
        $replace = implode(', ', $parts);
198
199
        $this->sql = str_replace($searchKey, $replace, $this->sql);
200
    }
201
202
    /**
203
     * @param     $var
204
     * @param int $paramHandle
205
     *
206
     * @return array
207
     */
208
    protected function getFinalParam($var, int $paramHandle): array
209
    {
210
        switch ($paramHandle) {
211
            case self::PARAM_ALL_MANUAL:
212
                return [$var[0], $var[1]];
213
            case self::PARAM_ALL_AUTO:
214
                if ($var === null) {
215
                    return [$var, PDO::PARAM_NULL];
216
                } elseif (is_bool($var)) {
217
                    return [$var, PDO::PARAM_BOOL];
218
                } elseif (is_int($var)) {
219
                    return [$var, PDO::PARAM_INT];
220
                }
221
        }
222
223
        return [$var, PDO::PARAM_STR];
224
    }
225
226
    /**
227
     * @return string
228
     */
229
    public function __toString(): string
230
    {
231
        return $this->sql;
232
    }
233
234
    /**
235
     * @return array
236
     */
237
    public function getParams(): array
238
    {
239
        return $this->params;
240
    }
241
}
242