Test Failed
Push — develop ( aa34ee...bf5ed8 )
by Kenneth
02:49
created

Statement::placeholderFill()   B

Complexity

Conditions 9
Paths 8

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 9

Importance

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