Passed
Push — develop ( a41828...be09b7 )
by Kenneth
02:35
created

Statement::bFloatNullable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
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
39 52
    public function __construct(Bindings $bindings)
40
    {
41 52
        $this->bindings = $bindings;
42 52
    }
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
     */
53 8
    private function filterValidateBool($value): ?bool
54
    {
55 8
        return $value === null
56 4
            ? null
57 8
            : 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
     */
68 52
    private function rawPlaceholderFill(string $sql): string
69
    {
70 52
        foreach ($this->rawNamed as $name => $rVal) {
71 11
            $sql = (string) preg_replace('/' . $name . '\b/', $rVal, $sql);
72
        }
73
74 52
        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
     */
86 32
    public function bind(string $name, $value, int $type = PDO::PARAM_STR): self
87
    {
88 32
        $this->named[$name] = array(
89 32
            'type' => $type,
90 32
            'value' => $value
91
        );
92
93 32
        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
     */
103 21
    public function rawBind(string $name, $value): self
104
    {
105 21
        $this->rawNamed[$name] = $value;
106
107 21
        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
     */
121 1
    public function bBoolNullable($value = null): self
122
    {
123 1
        $binding = $this->bindings->bBoolNullable($this->filterValidateBool($value));
124 1
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
125 1
        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
     */
136 3
    public function bBool($value): self
137
    {
138 3
        $binding = $this->bindings->bBool($this->filterValidateBool($value));
139 2
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
140 2
        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
     */
152 1
    public function bBoolIntNullable($value = null): self
153
    {
154 1
        $binding = $this->bindings->bBoolIntNullable($this->filterValidateBool($value));
155 1
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
156 1
        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
     */
167 3
    public function bBoolInt($value): self
168
    {
169 3
        $binding = $this->bindings->bBoolInt($this->filterValidateBool($value));
170 2
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
171 2
        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
     */
183 1
    public function bDateNullable(?string $value = null): self
184
    {
185 1
        $binding = $this->bindings->bDateNullable($value);
186 1
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
187 1
        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
     */
199 13
    public function bDate(string $value): self
200
    {
201 13
        $binding = $this->bindings->bDate($value);
202 13
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
203 13
        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
     */
215 1
    public function bDateTimeNullable(?string $value = null): self
216
    {
217 1
        $binding = $this->bindings->bDateTimeNullable($value);
218 1
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
219 1
        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
     */
231 13
    public function bDateTime(string $value): self
232
    {
233 13
        $binding = $this->bindings->bDateTime($value);
234 13
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
235 13
        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
     */
248 1
    public function bFloatNullable($value = null, $decimals = 3): self
249
    {
250 1
        $binding = $this->bindings->bFloatNullable($value, $decimals);
251 1
        $this->rawBind($this->getNextName('raw'), $binding[0]);
252 1
        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
     */
264 15
    public function bFloat($value, $decimals = 3): self
265
    {
266 15
        $binding = $this->bindings->bFloat($value, $decimals);
267 13
        $this->rawBind($this->getNextName('raw'), $binding[0]);
268 13
        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
     */
279 1
    public function bIntNullable($value = null): self
280
    {
281 1
        $binding = $this->bindings->bIntNullable($value);
282 1
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
283 1
        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
     */
294 3
    public function bInt($value): self
295
    {
296 3
        $binding = $this->bindings->bInt($value);
297 1
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
298 1
        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
     */
311 2
    public function bIntArray(array $data, int $default = 0): self
312
    {
313 2
        $binding = $this->bindings->bIntArray($data, $default);
314 1
        $this->rawBind($this->getNextName('raw'), $binding[0]);
315 1
        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
     */
327 1
    public function bJsonNullable($value): self
328
    {
329 1
        $binding = $this->bindings->bJsonNullable($value);
330 1
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
331 1
        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
     */
343 4
    public function bJson($value): self
344
    {
345 4
        $binding = $this->bindings->bJson($value);
346 1
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
347 1
        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
     * @return Statement
358
     */
359 4
    public function bLike(string $value, bool $ends = false, bool $starts = false): self
360
    {
361 4
        $binding = $this->bindings->bLike($value, $ends, $starts);
362 4
        $this->bind($this->getNextName(), $binding[0]);
363 4
        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 17
    public function bRaw($value): self
375
    {
376 17
        $binding = $this->bindings->bRaw($value);
377 17
        $this->rawBind($this->getNextName('raw'), $binding[0]);
378 17
        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
    public function bStrNullable($value, int $type = PDO::PARAM_STR): self
391
    {
392
        $binding = $this->bindings->bStrNullable($value, $type);
393
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
394
        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 24
    public function bStr($value, int $type = PDO::PARAM_STR): self
407
    {
408 24
        $binding = $this->bindings->bStr($value, $type);
409 23
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
410 23
        return $this;
411
    }
412
413
    /**
414
     * Convert an array into a string and bind it.
415
     * Great for IN() statements.
416
     *
417
     * @param array $values
418
     * @param string|int|float|bool $default
419
     *
420
     * @return Statement
421
     */
422 1
    public function bStrArr(array $values, $default = ''): self
423
    {
424 1
        $binding = $this->bindings->bStrArr($values, $default);
425 1
        $this->rawBind($this->getNextName('raw'), $binding[0]);
426 1
        return $this;
427
    }
428
429
    // The rest of the helpers
430
431
    /**
432
     * Name the positions for binding in PDO.
433
     *
434
     * @param string $type
435
     *
436
     * @return string
437
     */
438 46
    private function getNextName(string $type = 'bind'): string
439
    {
440 46
        switch ($type) {
441 46
            case 'sql':
442
                // sql statement syntax
443 39
                $ret = sprintf(':pos%d', $this->sqlPos++);
444
445 39
                return $ret;
446
447 39
            case 'rawSql':
448
                //$ret = sprintf(':raw%d', $this->_rawSql++);
449 24
                $ret = sprintf(':raw%d', $this->rawPos);
450
451 24
                return $ret;
452
453 36
            case 'raw':
454
                // raw statement syntax
455 21
                $ret = sprintf(':raw%d', $this->rawPos++);
456
457 21
                return $ret;
458
459 32
            case 'bind':
460
            default:
461
                // bind/filling values
462 32
                $ret = sprintf(':pos%d', $this->bindPos++);
463
464 32
                return $ret;
465
        }
466
    }
467
468
    /**
469
     * Prepare and Execute the SQL statement.
470
     *
471
     * @param PDO $pdo
472
     *
473
     * @return PDOStatement
474
     * @throws Exception
475
     */
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 52
        $sql = $this->rawPlaceholderFill((string) implode(' ', $this->SQL));
481
482
        /** @var PDOStatement $stmt */
483 52
        $stmt = $pdo->prepare($sql);
484
485
        // Bind named parameters.
486 52
        foreach ($this->named as $name => $sVal) {
487 18
            switch ($sVal['type']) {
488
                case PDO::PARAM_BOOL:
489 2
                    $stmt->bindValue($name, (bool) $sVal['value'], $sVal['type']);
490 2
                    break;
491
492
                case PDO::PARAM_NULL:
493 2
                    $stmt->bindValue($name, null);
494 2
                    break;
495
496
                case PDO::PARAM_INT:
497 3
                    $stmt->bindValue($name, (int) $sVal['value'], $sVal['type']);
498 3
                    break;
499
500
                case PDO::PARAM_STR:
501
                default:
502 15
                    $stmt->bindValue($name, (string) $sVal['value'], $sVal['type']);
503 18
                    break;
504
            }
505
        }
506
507 52
        $stmt->execute();
508 52
        return $stmt;
509
    }
510
511
    /**
512
     * Use for building out what a might look like when it's pass to the DB.
513
     * Used by Statement::getComputed()
514
     *
515
     * @param array $matches
516
     *
517
     * @return mixed
518
     * @throws Exception
519
     */
520 28
    private function placeholderFill(array $matches)
521
    {
522 28
        $key = $matches[0];
523
524
        // Can't fill this param.
525 28
        if (!isset($this->named[$key]) && !isset($this->rawNamed[$key])) {
526 1
            return $key;
527
        }
528
529 27
        if (isset($this->named[$key])) {
530
            // here is the param
531 23
            $sVal = $this->named[$key];
532
533 23
            switch ($sVal['type']) {
534
                case PDO::PARAM_BOOL:
535 2
                    return $sVal['value'] ? 'TRUE' : 'FALSE';
536
537
                case PDO::PARAM_NULL:
538 4
                    return 'NULL';
539
540
                case PDO::PARAM_INT:
541 4
                    return (int) $sVal['value'];
542
543
                case PDO::PARAM_STR:
544
                default:
545 20
                    return "'" . $sVal['value'] . "'";
546
            }
547
        }
548
549
        // Since it's not named, it must be raw.
550 13
        return $this->rawNamed[$key];
551
    }
552
553
    /**
554
     * Get name of the placeholder.
555
     *
556
     * @return string
557
     */
558 39
    private function placeholderGetName(): string
559
    {
560 39
        return $this->getNextName('sql');
561
    }
562
563
    /**
564
     * Get name of the raw placeholder.
565
     *
566
     * @return string
567
     */
568 24
    private function rawPlaceHolderGetName(): string
569
    {
570 24
        return $this->getNextName('rawSql');
571
    }
572
573
    /**
574
     * Builds up the SQL parameterized statement.
575
     *
576
     * @param string $text
577
     *
578
     * @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 39
            return $this->placeholderGetName();
586 52
        }, $text);
587
588
        $text = (string) preg_replace_callback('/%%/m', function () {
589 24
            return $this->rawPlaceholderGetName();
590 52
        }, $text);
591
592 52
        $this->SQL[] = $text;
593
594 52
        return $this;
595
    }
596
597
    /**
598
     * Reset / Clear out properties.
599
     *
600
     * @return Statement
601
     */
602 52
    public function reset(): self
603
    {
604 52
        $this->bindPos = 0;
605 52
        $this->named = [];
606 52
        $this->SQL = [];
607 52
        $this->sqlPos = 0;
608 52
        $this->rawNamed = array();
609 52
        $this->rawPos = 0;
610 52
        $this->rawSql = array();
611
612 52
        return $this;
613
    }
614
615
    /**
616
     * Create what the SQL query string might look like.
617
     * Great for debugging. YMMV though.
618
     *
619
     * @return string
620
     */
621 29
    public function getComputed(): string
622
    {
623
        // Merge SQL together
624 29
        $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 28
            return $this->placeholderFill($matches);
630 29
        }, $sql);
631
632 29
        return $sql;
633
    }
634
635
    /**
636
     * Return the SQL as a string.
637
     *
638
     * @return string
639
     */
640 1
    public function __toString(): string
641
    {
642 1
        return $this->getComputed();
643
    }
644
645
    /**
646
     * Magic Method for debugging.
647
     *
648
     * @return array
649
     */
650 1
    public function __debugInfo(): array
651
    {
652
        return [
653 1
            'Named Positions' => $this->named,
654 1
            'Unbound SQL' => $this->SQL,
655 1
            'Bound SQL' => $this->getComputed()
656
        ];
657
    }
658
}
659