Passed
Push — develop ( abefc0...4cf285 )
by Kenneth
02:21
created

Statement::placeholderFill()   B

Complexity

Conditions 9
Paths 8

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 9

Importance

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