Passed
Push — master ( d1eaf1...9deb7d )
by Kenneth
01:35
created

Statement::bDateTime()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 1 Features 0
Metric Value
cc 1
eloc 3
c 7
b 1
f 0
nc 1
nop 2
dl 0
loc 5
rs 10
1
<?php
2
3
namespace GeekLab\GLPDO2;
4
5
// Make EA inspection stop complaining.
6
use \Exception;
7
use GeekLab\GLPDO2\Bindings\BindingsInterface;
8
use \PDO;
9
use \PDOStatement;
10
11
class Statement
12
{
13
    /** @var BindingsInterface $bindings */
14
    private $bindings;
15
16
    /** @var int $bindPos Position for SQL binds. */
17
    private $bindPos = 0;
18
19
    /** @var array $named Named binding values. */
20
    private $named = [];
21
22
    /** @var array $SQL SQL Statement. */
23
    private $SQL = [];
24
25
    /** @var int Position holder for statement processing. */
26
    private $sqlPos = 0;
27
28
    /** @var array Raw named placeholders. */
29
    private $rawNamed = [];
30
31
    /** @var int $rawPos Position holder for raw statement processing. */
32
    private $rawPos = 0;
33
34
    /** @var array $rawSql SQL Statement. */
35
    private $rawSql = [];
36
37
    public function __construct(BindingsInterface $bindings)
38
    {
39
        $this->bindings = $bindings;
40
    }
41
42
    /**
43
     * Bind a value to a named parameter.
44
     *
45
     * @param string $name
46
     * @param string|int|float|bool|null $value
47
     * @param int $type
48
     *
49
     * @return Statement
50
     */
51
    public function bind(string $name, $value, int $type = PDO::PARAM_STR): self
52
    {
53
        $this->named[$name] = array(
54
            'type' => $type,
55
            'value' => $value
56
        );
57
58
        return $this;
59
    }
60
61
    /**
62
     * Bind a raw value to a named parameter.
63
     *
64
     * @param string $name
65
     * @param string|int|float|bool $value
66
     * @return Statement
67
     */
68
    public function rawBind(string $name, $value): self
69
    {
70
        $this->rawNamed[$name] = $value;
71
72
        return $this;
73
    }
74
75
    // Bind types
76
77
    /**
78
     * Bind a boolean value as bool, with NULL option.
79
     *
80
     * @param string|int|bool|null $value
81
     * @param bool $null
82
     *
83
     * @return Statement
84
     * @throws Exception
85
     */
86
    public function bBool($value = null, bool $null = false): self
87
    {
88
        $binding = $this->bindings->bBool($value, $null);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type string; however, parameter $value of GeekLab\GLPDO2\Bindings\BindingsInterface::bBool() does only seem to accept boolean|integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
        $binding = $this->bindings->bBool(/** @scrutinizer ignore-type */ $value, $null);
Loading history...
89
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
90
        return $this;
91
    }
92
93
    /**
94
     * Bind a boolean value as int, with NULL option.
95
     *
96
     * @param string|int|bool|null $value
97
     * @param bool $null
98
     *
99
     * @return Statement
100
     * @throws Exception
101
     */
102
    public function bBoolInt($value = null, bool $null = false): self
103
    {
104
        $binding = $this->bindings->bBoolInt($value, $null);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type string; however, parameter $value of GeekLab\GLPDO2\Bindings\...gsInterface::bBoolInt() does only seem to accept boolean|integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

104
        $binding = $this->bindings->bBoolInt(/** @scrutinizer ignore-type */ $value, $null);
Loading history...
105
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
106
        return $this;
107
    }
108
109
    /**
110
     * Bind a date value as date or optional NULL.
111
     * YYYY-MM-DD is the proper date format.
112
     *
113
     * @param string|null $value
114
     * @param bool $null
115
     *
116
     * @return Statement
117
     * @throws Exception
118
     */
119
    public function bDate($value = null, bool $null = false): self
120
    {
121
        $binding = $this->bindings->bDate($value, $null);
122
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
123
        return $this;
124
    }
125
126
    /**
127
     * Bind a date value as date time or optional NULL.
128
     * YYYY-MM-DD HH:MM:SS is the proper date format.
129
     *
130
     * @param string|null $value
131
     * @param bool $null
132
     *
133
     * @return Statement
134
     * @throws Exception
135
     */
136
    public function bDateTime($value = null, bool $null = false): self
137
    {
138
        $binding = $this->bindings->bDateTime($value, $null);
139
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
140
        return $this;
141
    }
142
143
    /**
144
     * Bind a float.
145
     *
146
     * @param string|int|float|null $value
147
     * @param int $decimals
148
     * @param bool $null
149
     *
150
     * @return Statement
151
     * @throws Exception
152
     */
153
    public function bFloat($value = null, $decimals = 3, $null = false): self
154
    {
155
        $binding = $this->bindings->bFloat($value, $decimals, $null);
156
        $this->rawBind($this->getNextName('raw'), $binding[0]);
157
        return $this;
158
    }
159
160
161
    /**
162
     * Bind an integer with optional NULL.
163
     *
164
     * @param string|int|float|bool|null $value
165
     * @param bool $null
166
     *
167
     * @return Statement
168
     * @throws Exception
169
     */
170
    public function bInt($value = null, bool $null = false): self
171
    {
172
        $binding = $this->bindings->bInt($value, $null);
173
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
174
        return $this;
175
    }
176
177
    /**
178
     * Convert array of integers to comma separated values. Uses %%
179
     * Great for IN() statements.
180
     *
181
     * @param array $data
182
     * @param int $default
183
     *
184
     * @return Statement
185
     * @throws Exception
186
     */
187
    public function bIntArray(array $data, int $default = 0): self
188
    {
189
        $binding = $this->bindings->bIntArray($data, $default);
190
        $this->rawBind($this->getNextName('raw'), $binding[0]);
191
        return $this;
192
    }
193
194
    /**
195
     * Bind a object or JSON string to a string
196
     *
197
     * @param string|object|null $value
198
     * @param bool $null
199
     *
200
     * @return Statement
201
     * @throws Exception
202
     */
203
    public function bJSON($value, bool $null = false): self
204
    {
205
        $binding = $this->bindings->bJSON($value, $null);
206
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
207
        return $this;
208
    }
209
210
    /**
211
     * Create and bind string for LIKE() statements.
212
     *
213
     * @param string $value
214
     * @param bool $ends Ends with?
215
     * @param bool $starts Starts with?
216
     *
217
     * @return Statement
218
     */
219
    public function bLike(string $value, bool $ends = false, bool $starts = false): self
220
    {
221
        $binding = $this->bindings->bLike($value, $ends, $starts);
222
        $this->bind($this->getNextName(), $binding[0]);
223
        return $this;
224
    }
225
226
    /**
227
     * !!!DANGER!!!
228
     * Bind a raw value.
229
     *
230
     * @param string|int|float|bool $value
231
     *
232
     * @return Statement
233
     */
234
    public function bRaw($value): self
235
    {
236
        $binding = $this->bindings->bRaw($value);
237
        $this->rawBind($this->getNextName('raw'), $binding[0]);
238
        return $this;
239
    }
240
241
    /**
242
     * Bind a string value.
243
     *
244
     * @param string|int|float|bool|null $value
245
     * @param bool $null
246
     * @param int $type
247
     *
248
     * @return Statement
249
     * @throws Exception
250
     */
251
    public function bStr($value, bool $null = false, int $type = PDO::PARAM_STR): self
252
    {
253
        $binding = $this->bindings->bStr($value, $null, $type);
254
        $this->bind($this->getNextName(), $binding[0], $binding[1]);
255
        return $this;
256
    }
257
258
    /**
259
     * Convert an array into a string and bind it.
260
     * Great for IN() statements.
261
     *
262
     * @param array $values
263
     * @param string|int|float|bool $default
264
     *
265
     * @return Statement
266
     */
267
    public function bStrArr(array $values, $default = ''): self
268
    {
269
        $binding = $this->bindings->bStrArr($values, $default);
270
        $this->rawBind($this->getNextName('raw'), $binding[0]);
271
        return $this;
272
    }
273
274
    // The rest of the helpers
275
276
    /**
277
     * Name the positions for binding in PDO.
278
     *
279
     * @param string $type
280
     *
281
     * @return string
282
     */
283
    private function getNextName(string $type = 'bind'): string
284
    {
285
        switch ($type) {
286
            case 'sql':
287
                // sql statement syntax
288
                $ret = sprintf(':pos%d', $this->sqlPos++);
289
290
                return $ret;
291
292
            case 'rawSql':
293
                //$ret = sprintf(':raw%d', $this->_rawSql++);
294
                $ret = sprintf(':raw%d', $this->rawPos);
295
296
                return $ret;
297
298
            case 'raw':
299
                // raw statement syntax
300
                $ret = sprintf(':raw%d', $this->rawPos++);
301
302
                return $ret;
303
304
            case 'bind':
305
            default:
306
                // bind/filling values
307
                $ret = sprintf(':pos%d', $this->bindPos++);
308
309
                return $ret;
310
        }
311
    }
312
313
    /**
314
     * Prepare and Execute the SQL statement.
315
     *
316
     * @param PDO $PDO
317
     *
318
     * @return PDOStatement
319
     * @throws Exception
320
     */
321
    public function execute(PDO $PDO): PDOStatement
322
    {
323
        // Prepare the SQL, force to string in case of null.
324
        $sql = (string) implode(' ', $this->SQL);
325
326
        // Replace raw placements with raw values.
327
        foreach ($this->rawNamed as $name => $rVal) {
328
            $sql = (string) preg_replace('/' . $name . '\b/', $rVal, $sql);
329
        }
330
331
        /** @var PDOStatement $stmt */
332
        $stmt = $PDO->prepare($sql);
333
334
        // Bind named parameters.
335
        foreach ($this->named as $name => $sVal) {
336
            switch ($sVal['type']) {
337
                case PDO::PARAM_BOOL:
338
                    $stmt->bindValue($name, (bool) $sVal['value'], $sVal['type']);
339
                    break;
340
341
                case PDO::PARAM_NULL:
342
                    $stmt->bindValue($name, null);
343
                    break;
344
345
                case PDO::PARAM_INT:
346
                    $stmt->bindValue($name, (int) $sVal['value'], $sVal['type']);
347
                    break;
348
349
                case PDO::PARAM_STR:
350
                default:
351
                    $stmt->bindValue($name, (string) $sVal['value'], $sVal['type']);
352
                    break;
353
            }
354
        }
355
356
        $stmt->execute();
357
        return $stmt;
358
    }
359
360
    /**
361
     * Use for building out what a might look like when it's pass to the DB.
362
     * Used by Statement::getComputed()
363
     *
364
     * @param array $matches
365
     *
366
     * @return mixed
367
     * @throws Exception
368
     */
369
    private function placeholderFill(array $matches)
370
    {
371
        $key = $matches[0];
372
373
        // Can't fill this param.
374
        if (!isset($this->named[$key]) && !isset($this->rawNamed[$key])) {
375
            return $key;
376
        }
377
378
        if (isset($this->named[$key])) {
379
            // here is the param
380
            $sVal = $this->named[$key];
381
382
            switch ($sVal['type']) {
383
                case PDO::PARAM_BOOL:
384
                    return $sVal['value'] ? 'TRUE' : 'FALSE';
385
386
                case PDO::PARAM_NULL:
387
                    return 'NULL';
388
389
                case PDO::PARAM_INT:
390
                    return (int) $sVal['value'];
391
392
                case PDO::PARAM_STR:
393
                default:
394
                    return "'" . $sVal['value'] . "'";
395
            }
396
        }
397
398
        // Since it's not named, it must be raw.
399
        return $this->rawNamed[$key];
400
    }
401
402
    /**
403
     * Get name of the placeholder.
404
     *
405
     * @return string
406
     */
407
    private function placeholderGetName(): string
408
    {
409
        return $this->getNextName('sql');
410
    }
411
412
    /**
413
     * Get name of the raw placeholder.
414
     *
415
     * @return string
416
     */
417
    private function rawPlaceHolderGetName(): string
418
    {
419
        return $this->getNextName('rawSql');
420
    }
421
422
    /**
423
     * Builds up the SQL parameterized statement.
424
     *
425
     * @param string $text
426
     *
427
     * @return Statement
428
     */
429
    public function sql(string $text): self
430
    {
431
        // Replace positioned placeholders with named placeholders (first value).
432
        // Force to string, in the case of null.
433
        $text = (string) preg_replace_callback('/\?/m', function () {
434
            return $this->placeholderGetName();
435
        }, $text);
436
437
        $text = (string) preg_replace_callback('/%%/m', function () {
438
            return $this->rawPlaceholderGetName();
439
        }, $text);
440
441
        $this->SQL[] = $text;
442
443
        return $this;
444
    }
445
446
    /**
447
     * Reset / Clear out properties.
448
     *
449
     * @return Statement
450
     */
451
    public function reset(): self
452
    {
453
        $this->bindPos = 0;
454
        $this->named = [];
455
        $this->SQL = [];
456
        $this->sqlPos = 0;
457
        $this->rawNamed = array();
458
        $this->rawPos = 0;
459
        $this->rawSql = array();
460
461
        return $this;
462
    }
463
464
    /**
465
     * Create what the SQL query string might look like.
466
     * Great for debugging. YMMV though.
467
     *
468
     * @return string
469
     */
470
    public function getComputed(): string
471
    {
472
        // Merge SQL together
473
        $sql = implode("\n", $this->SQL);
474
475
        // Replace positioned placeholders with named placeholders (first value).
476
        // Force to string, in the case of null.
477
        $sql = (string) preg_replace_callback('/:[a-z0-9_]+/m', array($this, 'placeholderFill'), $sql);
478
479
        return $sql;
480
    }
481
482
    /**
483
     * Return the SQL as a string.
484
     *
485
     * @return string
486
     */
487
    public function __toString(): string
488
    {
489
        return $this->getComputed();
490
    }
491
492
    /**
493
     * Magic Method for debugging.
494
     *
495
     * @return array
496
     */
497
    public function __debugInfo(): array
498
    {
499
        return [
500
            'Named Positions' => $this->named,
501
            'Unbound SQL' => $this->SQL,
502
            'Bound SQL' => $this->getComputed()
503
        ];
504
    }
505
}
506