Test Failed
Push — develop ( be09b7...539e03 )
by Kenneth
05:14
created

Statement::bJsonNullable()   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 1
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
    /**
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 8
     */
54
    private function bind(string $name, $value, int $type = PDO::PARAM_STR): self
55 8
    {
56 4
        $this->named[$name] = array(
57 8
            'type' => $type,
58
            'value' => $value
59
        );
60
61
        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 52
     *
69
     * @param bool|int|string|null $value
70 52
     *
71 11
     * @return bool|null
72
     */
73
    private function filterValidateBool($value): ?bool
74 52
    {
75
        return $value === null
76
            ? null
77
            : 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 32
     * @return string
87
     */
88 32
    private function rawPlaceholderFill(string $sql): string
89 32
    {
90 32
        foreach ($this->rawNamed as $name => $rVal) {
91
            $sql = (string) preg_replace('/' . $name . '\b/', $rVal, $sql);
92
        }
93 32
94
        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 21
     */
104
    public function rawBind(string $name, $value): self
105 21
    {
106
        $this->rawNamed[$name] = $value;
107 21
108
        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 1
     */
122
    public function bBool($value, array $options = []): self
123 1
    {
124 1
        $binding = $this->bindings->bBool($this->filterValidateBool($value), $options);
125 1
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
126
        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 3
     * @throws TypeError
137
     */
138 3
    public function bBoolInt($value, array $options = []): self
139 2
    {
140 2
        $binding = $this->bindings->bBoolInt($this->filterValidateBool($value), $options);
141
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
142
        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 1
     * @return Statement
153
     * @throws TypeError
154 1
     */
155 1
    public function bDate($value, array $options = []): self
156 1
    {
157
        $binding = $this->bindings->bDate($value, $options);
158
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
159
        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 3
     * @param array $options ['nullable' => (bool)]
168
     *
169 3
     * @return Statement
170 2
     * @throws TypeError
171 2
     */
172
    public function bDateTime($value, array $options = []): self
173
    {
174
        $binding = $this->bindings->bDateTime($value, $options);
175
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
176
        return $this;
177
    }
178
179
    /**
180
     * Bind a float.
181
     *
182
     * @param string|int|float|null $value
183 1
     * @param array $options ['decimals' => (int), 'nullable' => (bool)]
184
     *
185 1
     * @return Statement
186 1
     * @throws TypeError
187 1
     */
188
    public function bFloat($value, array $options = ['decimals' => 2]): self
189
    {
190
        $binding = $this->bindings->bFloat($value, $options);
191
        $this->rawBind($this->getNextName('raw'), $binding[0]);
192
        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 13
     * @param array $options ['nullable' => (bool)]
200
     *
201 13
     * @return Statement
202 13
     * @throws TypeError
203 13
     */
204
    public function bInt($value, array $options = []): self
205
    {
206
        $binding = $this->bindings->bInt($value, $options);
207
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
208
        return $this;
209
    }
210
211
    /**
212
     * Convert array of integers to comma separated values. Uses %%
213
     * Great for IN() statements.
214
     *
215 1
     * @param array $data
216
     * @param int $default
217 1
     *
218 1
     * @return Statement
219 1
     * @throws TypeError
220
     */
221
    public function bIntArray(array $data, int $default = 0): self
222
    {
223
        $binding = $this->bindings->bIntArray($data, $default);
224
        $this->rawBind($this->getNextName('raw'), $binding[0]);
225
        return $this;
226
    }
227
228
    /**
229
     * Bind JSON to string or optional null.
230
     *
231 13
     * @param string|object|null $value
232
     * @param array $options ['nullable' => (bool)]
233 13
     *
234 13
     * @return Statement
235 13
     * @throws JsonException
236
     * @throws TypeError
237
     */
238
    public function bJson($value, array $options = []): self
239
    {
240
        $binding = $this->bindings->bJson($value, $options);
241
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
242
        return $this;
243
    }
244
245
    /**
246
     * Create and bind string for LIKE() statements.
247
     *
248 1
     * @param string $value
249
     * @param bool $ends Ends with?
250 1
     * @param bool $starts Starts with?
251 1
     *
252 1
     * @return Statement
253
     */
254
    public function bLike(string $value, bool $ends = false, bool $starts = false): self
255
    {
256
        $binding = $this->bindings->bLike($value, $ends, $starts);
257
        $this->bind($this->getNextName(), $binding[0]);
258
        return $this;
259
    }
260
261
    /**
262
     * !!!DANGER!!!
263
     * Bind a raw value.
264 15
     *
265
     * @param string|int|float|bool $value
266 15
     *
267 13
     * @return Statement
268 13
     */
269
    public function bRaw($value): self
270
    {
271
        $binding = $this->bindings->bRaw($value);
272
        $this->rawBind($this->getNextName('raw'), $binding[0]);
273
        return $this;
274
    }
275
276
    /**
277
     * Bind a string value to string or optional null.
278
     *
279 1
     * @param string|int|float|bool|null $value
280
     * @param array $options ['nullable' => (bool)]
281 1
     *
282 1
     * @return Statement
283 1
     * @throws Exception
284
     */
285
    public function bStr($value, array $options = []): self
286
    {
287
        $binding = $this->bindings->bStr($value, $options);
288
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
289
        return $this;
290
    }
291
292
    /**
293
     * Convert an array into a string and bind it.
294 3
     * Great for IN() statements.
295
     *
296 3
     * @param array $values
297 1
     * @param string|int|float|bool $default
298 1
     *
299
     * @return Statement
300
     */
301
    public function bStrArr(array $values, $default = ''): self
302
    {
303
        $binding = $this->bindings->bStrArr($values, $default);
304
        $this->rawBind($this->getNextName('raw'), $binding[0]);
305
        return $this;
306
    }
307
308
    /**
309
     * Bind a string to the PDO data type.
310
     *
311 2
     * @param string|int|float|bool|null $value
312
     * @param int $type
313 2
     *
314 1
     * @return Statement
315 1
     */
316
    public function bValueType($value, int $type = PDO::PARAM_STR): self
317
    {
318
        $binding = $this->bindings->bValueType($value, $type);
319
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
320
        return $this;
321
    }
322
323
    // The rest of the helpers
324
325
    /**
326
     * Name the positions for binding in PDO.
327 1
     *
328
     * @param string $type
329 1
     *
330 1
     * @return string
331 1
     */
332
    private function getNextName(string $type = 'bind'): string
333
    {
334
        switch ($type) {
335
            case 'sql':
336
                // sql statement syntax
337
                $ret = sprintf(':pos%d', $this->sqlPos++);
338
339
                return $ret;
340
341
            case 'rawSql':
342
                //$ret = sprintf(':raw%d', $this->_rawSql++);
343 4
                $ret = sprintf(':raw%d', $this->rawPos);
344
345 4
                return $ret;
346 1
347 1
            case 'raw':
348
                // raw statement syntax
349
                $ret = sprintf(':raw%d', $this->rawPos++);
350
351
                return $ret;
352
353
            case 'bind':
354
            default:
355
                // bind/filling values
356
                $ret = sprintf(':pos%d', $this->bindPos++);
357
358
                return $ret;
359 4
        }
360
    }
361 4
362 4
    /**
363 4
     * Prepare and Execute the SQL statement.
364
     *
365
     * @param PDO $pdo
366
     *
367
     * @return PDOStatement
368
     * @throws Exception
369
     */
370
    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 17
        $sql = $this->rawPlaceholderFill((string) implode(' ', $this->SQL));
375
376 17
        /** @var PDOStatement $stmt */
377 17
        $stmt = $pdo->prepare($sql);
378 17
379
        // Bind named parameters.
380
        foreach ($this->named as $name => $sVal) {
381
            switch ($sVal['type']) {
382
                case PDO::PARAM_BOOL:
383
                    $stmt->bindValue($name, (bool) $sVal['value'], $sVal['type']);
384
                    break;
385
386
                case PDO::PARAM_NULL:
387
                    $stmt->bindValue($name, null);
388
                    break;
389
390
                case PDO::PARAM_INT:
391
                    $stmt->bindValue($name, (int) $sVal['value'], $sVal['type']);
392
                    break;
393
394
                case PDO::PARAM_STR:
395
                default:
396
                    $stmt->bindValue($name, (string) $sVal['value'], $sVal['type']);
397
                    break;
398
            }
399
        }
400
401
        $stmt->execute();
402
        return $stmt;
403
    }
404
405
    /**
406 24
     * Use for building out what a might look like when it's pass to the DB.
407
     * Used by Statement::getComputed()
408 24
     *
409 23
     * @param array $matches
410 23
     *
411
     * @return mixed
412
     * @throws Exception
413
     */
414
    private function placeholderFill(array $matches)
415
    {
416
        $key = $matches[0];
417
418
        // Can't fill this param.
419
        if (!isset($this->named[$key]) && !isset($this->rawNamed[$key])) {
420
            return $key;
421
        }
422 1
423
        if (isset($this->named[$key])) {
424 1
            // here is the param
425 1
            $sVal = $this->named[$key];
426 1
427
            switch ($sVal['type']) {
428
                case PDO::PARAM_BOOL:
429
                    return $sVal['value'] ? 'TRUE' : 'FALSE';
430
431
                case PDO::PARAM_NULL:
432
                    return 'NULL';
433
434
                case PDO::PARAM_INT:
435
                    return (int) $sVal['value'];
436
437
                case PDO::PARAM_STR:
438 46
                default:
439
                    return "'" . $sVal['value'] . "'";
440 46
            }
441 46
        }
442
443 39
        // Since it's not named, it must be raw.
444
        return $this->rawNamed[$key];
445 39
    }
446
447 39
    /**
448
     * Get name of the placeholder.
449 24
     *
450
     * @return string
451 24
     */
452
    private function placeholderGetName(): string
453 36
    {
454
        return $this->getNextName('sql');
455 21
    }
456
457 21
    /**
458
     * Get name of the raw placeholder.
459 32
     *
460
     * @return string
461
     */
462 32
    private function rawPlaceHolderGetName(): string
463
    {
464 32
        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
    public function sql(string $text): self
475
    {
476 52
        // 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
            return $this->placeholderGetName();
480 52
        }, $text);
481
482
        $text = (string) preg_replace_callback('/%%/m', function () {
483 52
            return $this->rawPlaceholderGetName();
484
        }, $text);
485
486 52
        $this->SQL[] = $text;
487 18
488
        return $this;
489 2
    }
490 2
491
    /**
492
     * Reset / Clear out properties.
493 2
     *
494 2
     * @return Statement
495
     */
496
    public function reset(): self
497 3
    {
498 3
        $this->bindPos = 0;
499
        $this->named = [];
500
        $this->SQL = [];
501
        $this->sqlPos = 0;
502 15
        $this->rawNamed = array();
503 18
        $this->rawPos = 0;
504
        $this->rawSql = array();
505
506
        return $this;
507 52
    }
508 52
509
    /**
510
     * Create what the SQL query string might look like.
511
     * Great for debugging. YMMV though.
512
     *
513
     * @return string
514
     */
515
    public function getComputed(): string
516
    {
517
        // Merge SQL together
518
        $sql = implode("\n", $this->SQL);
519
520 28
        // Replace positioned placeholders with named placeholders (first value).
521
        // Force to string, in the case of null.
522 28
        $sql = (string) preg_replace_callback('/:[a-z0-9_]+/m', function ($matches) {
523
            return $this->placeholderFill($matches);
524
        }, $sql);
525 28
526 1
        return $sql;
527
    }
528
529 27
    /**
530
     * Return the SQL as a string.
531 23
     *
532
     * @return string
533 23
     */
534
    public function __toString(): string
535 2
    {
536
        return $this->getComputed();
537
    }
538 4
539
    /**
540
     * Magic Method for debugging.
541 4
     *
542
     * @return array
543
     */
544
    public function __debugInfo(): array
545 20
    {
546
        return [
547
            'Named Positions' => $this->named,
548
            'Unbound SQL' => $this->SQL,
549
            'Bound SQL' => $this->getComputed()
550 13
        ];
551
    }
552
}
553