Statement::rawBind()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
c 3
b 0
f 0
1
<?php
2
3
namespace GeekLab\GLPDO2;
4
5
use PDO;
6
use PDOStatement;
7
use Exception;
8
use GeekLab\GLPDO2\Bindings\Bindings;
9
10
class Statement
11
{
12
    private Bindings $bindings;   // Parameter bindings.
13
    private int $bindPos = 0;     // Position for SQL binds.
14
    /**
15
     * @var array{} | array{mixed} $named
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{} | array{mixed} at position 6 could not be parsed: Expected ':' at position 6, but found 'mixed'.
Loading history...
16
     */
17
    private array $named = [];    // Named binding values.
18
    /**
19
     * @var array{} | array{mixed} $SQL
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{} | array{mixed} at position 6 could not be parsed: Expected ':' at position 6, but found 'mixed'.
Loading history...
20
     */
21
    private array $SQL = [];      // SQL Statement.
22
    private int $sqlPos = 0;      // Position holder for statement processing.
23
    /**
24
     * @var array{} | array<int|string, mixed> $rawNamed
25
     */
26
    private array $rawNamed = []; // Raw named placeholders.
27
    private int $rawPos = 0;      // Position holder for raw statement processing.
28
    // /**
29
    //  * @var array{} | array{mixed} $rawSql
30
    //  */
31
    // private array $rawSql = [];   // SQL Statement.
32
33 50
    public function __construct(Bindings $bindings)
34
    {
35 50
        $this->bindings = $bindings;
36
    }
37
38
    /**
39
     * Replace the raw placeholder with raw values.
40
     *
41
     * @param string $sql
42
     *
43
     * @return string
44
     */
45 50
    private function rawPlaceholderFill(string $sql): string
46
    {
47 50
        foreach ($this->rawNamed as $name => $rVal) {
48 12
            $sql = (string)preg_replace('/' . $name . '\b/', $rVal, $sql);
49
        }
50
51 50
        return $sql;
52
    }
53
54
    /**
55
     * Bind a value to a named parameter.
56
     *
57
     * @param string                             $name
58
     * @param float | bool | int | string | null $value
59
     * @param int                                $type
60
     *
61
     * @return Statement
62
     */
63 31
    public function bind(string $name, float | bool | int | string | null $value, int $type = PDO::PARAM_STR): self
64
    {
65 31
        $this->named[$name] = array(
66
            'type' => $type,
67
            'value' => $value
68
        );
69 31
        return $this;
70
    }
71
72
    /**
73
     * Bind a raw value to a named parameter.
74
     *
75
     * @param string                      $name
76
     * @param float | bool | int | string $value
77
     *
78
     * @return Statement
79
     */
80 20
    public function rawBind(string $name, float | bool | int | string $value): self
81
    {
82 20
        $this->rawNamed[$name] = $value;
83 20
        return $this;
84
    }
85
86
    // Bind types
87
88
    /**
89
     * Bind a boolean value as bool, with NULL option.
90
     *
91
     * @param bool | int | null $value
92
     * @param bool              $null
93
     *
94
     * @return Statement
95
     * @throws Exception
96
     */
97 4
    public function bBool(bool | int | null $value = null, bool $null = false): self
98
    {
99 4
        $binding = $this->bindings->bBool($value, $null);
100 3
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
101
102 3
        return $this;
103
    }
104
105
    /**
106
     * Bind a boolean value as int, with NULL option.
107
     *
108
     * @param bool | int | null $value
109
     * @param bool              $null
110
     *
111
     * @return Statement
112
     * @throws Exception
113
     */
114 4
    public function bBoolInt(bool | int | null $value = null, bool $null = false): self
115
    {
116 4
        $binding = $this->bindings->bBoolInt($value, $null);
117 3
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
118
119 3
        return $this;
120
    }
121
122
    /**
123
     * Bind a date value as date or optional NULL.
124
     * YYYY-MM-DD is the proper date format.
125
     *
126
     * @param string | null $value
127
     * @param bool          $null
128
     *
129
     * @return Statement
130
     * @throws Exception
131
     */
132 16
    public function bDate(?string $value = null, bool $null = false): self
133
    {
134 16
        $binding = $this->bindings->bDate($value, $null);
135 15
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
136
137 15
        return $this;
138
    }
139
140
    /**
141
     * Bind a date value as date time or optional NULL.
142
     * YYYY-MM-DD HH:MM:SS is the proper date format.
143
     *
144
     * @param string | null $value
145
     * @param bool          $null
146
     *
147
     * @return Statement
148
     * @throws Exception
149
     */
150 15
    public function bDateTime(?string $value = null, bool $null = false): self
151
    {
152 15
        $binding = $this->bindings->bDateTime($value, $null);
153 14
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
154
155 14
        return $this;
156
    }
157
158
    /**
159
     * Bind a float.
160
     *
161
     * @param float | int | string | null $value
162
     * @param int                         $decimals
163
     * @param bool                        $null
164
     *
165
     * @return Statement
166
     * @throws Exception
167
     */
168 15
    public function bFloat(float | int | string | null $value = null, int $decimals = 3, bool $null = false): self
169
    {
170 15
        $binding = $this->bindings->bFloat($value, $decimals, $null);
171 13
        $this->rawBind($this->getNextName('raw'), $binding[0]);
172
173 13
        return $this;
174
    }
175
176
177
    /**
178
     * Bind an integer with optional NULL.
179
     *
180
     * @param float | bool | int | string | null $value
181
     * @param bool                               $null
182
     *
183
     * @return Statement
184
     * @throws Exception
185
     */
186 3
    public function bInt(float | bool | int | string | null $value = null, bool $null = false): self
187
    {
188 3
        $binding = $this->bindings->bInt($value, $null);
189 1
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
190
191 1
        return $this;
192
    }
193
194
    /**
195
     * Convert array of integers to comma separated values. Uses %%
196
     * Great for IN() statements.
197
     *
198
     * @param array{} | array{mixed} $data
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{} | array{mixed} at position 6 could not be parsed: Expected ':' at position 6, but found 'mixed'.
Loading history...
199
     *
200
     * @return Statement
201
     * @throws Exception
202
     */
203 2
    public function bIntArray(array $data): self
204
    {
205 2
        $binding = $this->bindings->bIntArray($data);
206 1
        $this->rawBind($this->getNextName('raw'), $binding[0]);
207
208 1
        return $this;
209
    }
210
211
    /**
212
     * Bind an object or JSON string to a string
213
     *
214
     * @param mixed $value
215
     * @param bool  $null
216
     *
217
     * @return Statement
218
     * @throws Exception
219
     */
220 5
    public function bJSON(mixed $value, bool $null = false): self
221
    {
222 5
        $binding = $this->bindings->bJSON($value, $null);
223 2
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
224
225 2
        return $this;
226
    }
227
228
    /**
229
     * Create and bind string for LIKE() statements.
230
     *
231
     * @param string $value
232
     * @param bool   $ends   Ends with?
233
     * @param bool   $starts Starts with?
234
     *
235
     * @return Statement
236
     */
237 4
    public function bLike(string $value, bool $ends = false, bool $starts = false): self
238
    {
239 4
        $binding = $this->bindings->bLike($value, $ends, $starts);
240 4
        $this->bind($this->getNextName(), $binding[0]);
241
242 4
        return $this;
243
    }
244
245
    /**
246
     * Convert an array into a string and bind it.
247
     * Great for IN() statements.
248
     *
249
     * @param array{} | array{mixed}      $values
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{} | array{mixed} at position 6 could not be parsed: Expected ':' at position 6, but found 'mixed'.
Loading history...
250
     * @param float | bool | int | string $default
251
     *
252
     * @return Statement
253
     */
254 1
    public function bStrArr(array $values, float | bool | int | string $default = ''): self
255
    {
256 1
        $binding = $this->bindings->bStrArr($values, $default);
257 1
        $this->rawBind($this->getNextName('raw'), $binding[0]);
258
259 1
        return $this;
260
    }
261
262
    /**
263
     * !!!DANGER!!!
264
     * Bind a raw value.
265
     *
266
     * @param float | bool | int | string $value
267
     *
268
     * @return Statement
269
     */
270 17
    public function bRaw(float | bool | int | string $value): self
271
    {
272 17
        $binding = $this->bindings->bRaw($value);
273 17
        $this->rawBind($this->getNextName('raw'), $binding[0]);
274
275 17
        return $this;
276
    }
277
278
    /**
279
     * Bind a string value.
280
     *
281
     * @param float | bool | int | string | null $value
282
     * @param bool                               $null
283
     * @param int                                $type
284
     *
285
     * @return Statement
286
     * @throws Exception
287
     */
288 24
    public function bStr(
289
        float | bool | int | string | null $value,
290
        bool $null = false,
291
        int $type = PDO::PARAM_STR
292
    ): self {
293 24
        $binding = $this->bindings->bStr($value, $null, $type);
294 23
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
295
296 23
        return $this;
297
    }
298
299
    // The rest of the helpers
300
301
    /**
302
     * Name the positions for binding in PDO.
303
     *
304
     * @param string $type
305
     *
306
     * @return string
307
     */
308 44
    private function getNextName(string $type = 'bind'): string
309
    {
310 44
        return match ($type) {
311 38
            'sql' => sprintf(':pos%d', $this->sqlPos++),
312 23
            'rawSql' => sprintf(':raw%d', $this->rawPos),
313 20
            'raw' => sprintf(':raw%d', $this->rawPos++),
314 44
            default => sprintf(':pos%d', $this->bindPos++),
315
        };
316
    }
317
318
    /**
319
     * Prepare and Execute the SQL statement.
320
     *
321
     * @param PDO $PDO
322
     *
323
     * @return PDOStatement
324
     * @throws Exception
325
     */
326 50
    public function execute(PDO $PDO): PDOStatement
327
    {
328
        // Prepare the SQL, force to string in case of null.
329
        // Then replace raw placements with raw values.
330 50
        $sql = $this->rawPlaceholderFill(implode(' ', $this->SQL));
331
332
        /** @var PDOStatement $stmt */
333 50
        $stmt = $PDO->prepare($sql);
334
335
        // Bind named parameters.
336 50
        foreach ($this->named as $name => $sVal) {
337 19
            switch ($sVal['type']) {
338
                case PDO::PARAM_BOOL:
339 2
                    $stmt->bindValue($name, (bool)$sVal['value'], $sVal['type']);
340 2
                    break;
341
342
                case PDO::PARAM_NULL:
343 3
                    $stmt->bindValue($name, null);
344 3
                    break;
345
346
                case PDO::PARAM_INT:
347 3
                    $stmt->bindValue($name, (int)$sVal['value'], $sVal['type']);
348 3
                    break;
349
350
                case PDO::PARAM_STR:
351
                default:
352 16
                    $stmt->bindValue($name, (string)$sVal['value'], $sVal['type']);
353 16
                    break;
354
            }
355
        }
356
357 50
        $stmt->execute();
358
359 50
        return $stmt;
360
    }
361
362
    /**
363
     * Use for building out what a might look like when it's pass to the DB.
364
     * Used by Statement::getComputed()
365
     *
366
     * @param array{} | array{int|string} $matches
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{} | array{int|string} at position 6 could not be parsed: Expected ':' at position 6, but found 'int'.
Loading history...
367
     *
368
     * @return mixed
369
     * @throws Exception
370
     */
371 26
    private function placeholderFill(array $matches): mixed
372
    {
373 26
        if (empty($matches)) {
374
            return null;
375
        }
376
377 26
        $key = $matches[0];
378
379
        // Can't fill this param.
380 26
        if (!isset($this->named[$key]) && !isset($this->rawNamed[$key])) {
381 1
            return $key;
382
        }
383
384 25
        if (isset($this->named[$key])) {
385
            // here is the param
386 22
            $sVal = $this->named[$key];
387
388 22
            return match ($sVal['type']) {
389 2
                PDO::PARAM_BOOL => $sVal['value'] ? 'TRUE' : 'FALSE',
390 4
                PDO::PARAM_NULL => 'NULL',
391 3
                PDO::PARAM_INT => (int)$sVal['value'],
392 22
                default => "'" . $sVal['value'] . "'",
393
            };
394
        }
395
396
        // Since it's not named, it must be raw.
397 12
        return $this->rawNamed[$key];
398
    }
399
400
    /**
401
     * Get name of the placeholder.
402
     *
403
     * @return string
404
     */
405 38
    private function placeholderGetName(): string
406
    {
407 38
        return $this->getNextName('sql');
408
    }
409
410
    /**
411
     * Builds up the SQL parameterized statement.
412
     *
413
     * @param string $text
414
     *
415
     * @return Statement
416
     */
417 50
    public function sql(string $text): self
418
    {
419
        // Replace positioned placeholders with named placeholders (first value).
420
        // Force to string, in the case of null.
421 50
        $text = (string)preg_replace_callback(
422
            '/\?/m',
423 50
            function () {
424 38
                return $this->placeholderGetName();
425
            },
426
            $text
427
        );
428
429 50
        $text = (string)preg_replace_callback(
430
            '/%%/m',
431 50
            function () {
432 23
                return $this->getNextName('rawSql');
433
            },
434
            $text
435
        );
436
437 50
        $this->SQL[] = $text;
438
439 50
        return $this;
440
    }
441
442
    /**
443
     * Reset / Clear out properties.
444
     *
445
     * @return Statement
446
     */
447 50
    public function reset(): self
448
    {
449 50
        $this->bindPos = 0;
450 50
        $this->named = [];
451 50
        $this->SQL = [];
452 50
        $this->sqlPos = 0;
453 50
        $this->rawNamed = [];
454 50
        $this->rawPos = 0;
455
        // $this->rawSql = [];
456
457 50
        return $this;
458
    }
459
460
    /**
461
     * Create what the SQL query string might look like.
462
     * Great for debugging. YMMV though.
463
     *
464
     * @return string
465
     * @throws Exception
466
     */
467 27
    public function getComputed(): string
468
    {
469
        // Merge SQL together
470 27
        $sql = implode("\n", $this->SQL);
471
472
        // Replace positioned placeholders with named placeholders (first value).
473
        // Force to string, in the case of null.
474 27
        return (string)preg_replace_callback(
475
            '/:[a-z0-9_]+/m',
476 27
            function ($matches) {
477 26
                return $this->placeholderFill($matches);
478
            },
479
            $sql
480
        );
481
    }
482
483
    /**
484
     * Return the SQL as a string.
485
     *
486
     * @return string
487
     * @throws Exception
488
     */
489 1
    public function __toString(): string
490
    {
491 1
        return $this->getComputed();
492
    }
493
494
    /**
495
     * Magic Method for debugging.
496
     *
497
     * @return array{'Named Positions': array{} | array{mixed}, 'Unbound SQL': array{} | array{mixed}, 'Bound SQL': string}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{'Named at position 2 could not be parsed: Expected ':' at position 2, but found ''Named'.
Loading history...
498
     * @throws Exception
499
     */
500 1
    public function __debugInfo(): array
501
    {
502
        return [
503 1
            'Named Positions' => $this->named,
504 1
            'Unbound SQL' => $this->SQL,
505 1
            'Bound SQL' => $this->getComputed()
506
        ];
507
    }
508
}
509