Passed
Push — main ( 28cc2a...82d1e2 )
by Andreas
02:06
created

PDOStatement::bindValues()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 3
nop 1
dl 0
loc 13
ccs 8
cts 8
cp 1
crap 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Licensed to CRATE Technology GmbH("Crate") under one or more contributor
4
 * license agreements.  See the NOTICE file distributed with this work for
5
 * additional information regarding copyright ownership.  Crate licenses
6
 * this file to you under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.  You may
8
 * obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
15
 * License for the specific language governing permissions and limitations
16
 * under the License.
17
 *
18
 * However, if you have executed another commercial license agreement
19
 * with Crate these terms will supersede the license and you may use the
20
 * software solely pursuant to the terms of the relevant commercial agreement.
21
 */
22
23
declare(strict_types=1);
24
25
namespace Crate\PDO;
26
27
use ArrayIterator;
28
use Closure;
29
use Crate\Stdlib\ArrayUtils;
30
use Crate\Stdlib\CollectionInterface;
31
use Crate\Stdlib\CrateConst;
32
use IteratorAggregate;
33
use PDOStatement as BasePDOStatement;
34
35
class PDOStatement extends BasePDOStatement implements IteratorAggregate
36
{
37
    use PDOStatementImplementation;
38
39
    /**
40
     * @var array
41
     */
42
    private $parameters = [];
43
44
    /**
45
     * @var string|null
46
     */
47
    private $errorCode;
48
49
    /**
50
     * @var string|null
51
     */
52
    private $errorMessage;
53
54
    /**
55
     * @var string
56
     */
57
    private $sql;
58
59
    /**
60
     * @var array
61
     */
62
    private $options = [
63
        'bulkMode'           => false,
64
        'fetchMode'          => null,
65
        'fetchColumn'        => 0,
66
        'fetchClass'         => 'array',
67
        'fetchClassCtorArgs' => null,
68
    ];
69
70
    /**
71
     * Used for the {@see PDO::FETCH_BOUND}
72
     *
73
     * @var array
74
     */
75
    private $columnBinding = [];
76
77
    /**
78
     * @var CollectionInterface|null
79
     */
80
    private $collection;
81
82
    /**
83
     * @var PDOInterface
84
     */
85
    private $pdo;
86
87
    /**
88
     * @var Closure
89
     */
90
    private $request;
91
92
    private $namedToPositionalMap = [];
93
94
    /**
95
     * @param PDOInterface $pdo
96
     * @param Closure      $request
97
     * @param string       $sql
98
     * @param array        $options
99
     */
100 4
    public function __construct(PDOInterface $pdo, Closure $request, $sql, array $options)
101
    {
102 4
        $this->sql     = $this->replaceNamedParametersWithPositionals($sql);
103 4
        $this->pdo     = $pdo;
104 4
        $this->options = array_merge($this->options, $options);
105 4
        $this->request = $request;
106
    }
107
108 158
    private function replaceNamedParametersWithPositionals($sql)
109
    {
110 158
        if (strpos($sql, ':') === false) {
111 158
            return $sql;
112
        }
113 4
        $pattern = '/:((?:[\w|\d|_](?=([^\'\\\]*(\\\.|\'([^\'\\\]*\\\.)*[^\'\\\]*\'))*[^\']*$))*)/';
114
115 4
        $idx      = 1;
116 4
        $callback = function ($matches) use (&$idx) {
117 4
            $value = $matches[1];
118 4
            if (empty($value)) {
119 2
                return $matches[0];
120
            }
121 4
            $this->namedToPositionalMap[$idx] = $value;
122 4
            $idx++;
123
124 4
            return '?';
125 4
        };
126
127 4
        return preg_replace_callback($pattern, $callback, $sql);
128
    }
129
130
    /**
131
     * Determines if the statement has been executed
132
     *
133
     * @internal
134
     *
135
     * @return bool
136
     */
137 66
    private function hasExecuted()
138
    {
139 66
        return ($this->collection !== null || $this->errorCode !== null);
140
    }
141
142
    /**
143
     * Internal pointer to mark the state of the current query
144
     *
145
     * @internal
146
     *
147
     * @return bool
148
     */
149 62
    private function isSuccessful()
150
    {
151 62
        if (!$this->hasExecuted()) {
152
            // @codeCoverageIgnoreStart
153
            throw new Exception\LogicException('The statement has not been executed yet');
154
            // @codeCoverageIgnoreEnd
155
        }
156
157 62
        return $this->collection !== null;
158
    }
159
160
    /**
161
     * Get the fetch style to be used
162
     *
163
     * @internal
164
     *
165
     * @return int
166
     */
167 6
    private function getFetchStyle()
168
    {
169 6
        return $this->options['fetchMode'] ?: $this->pdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE);
170
    }
171
172
    /**
173
     * Update all the bound column references
174
     *
175
     * @internal
176
     *
177
     * @param array $row
178
     *
179
     * @return void
180
     */
181 2
    private function updateBoundColumns(array $row)
182
    {
183
184 2
        if (!$this->isSuccessful()) {
185
            return;
186
        }
187
188 2
        foreach ($this->columnBinding as $column => &$metadata) {
189 2
            $index = $this->collection->getColumnIndex($column);
190 2
            if ($index === null) {
191
                // todo: I would like to throw an exception and tell someone they screwed up
192
                // but i think that would violate the PDO api
193
                continue;
194
            }
195
196
            // Update by reference
197 2
            $value           = $this->typedValue($row[$index], $metadata['type']);
198 2
            $metadata['ref'] = $value;
199
        }
200
    }
201
202
    /**
203
     * {@inheritDoc}
204
     */
205 6
    public function execute($input_parameters = null): bool
206
    {
207 6
        $params = ArrayUtils::toArray($input_parameters);
208
209
        // In bulk mode, propagate input parameters 1:1.
210
        // In regular mode, translate input parameters to `bindValue` calls.
211 6
        if ($this->options["bulkMode"] !== true) {
212 6
            $params = $this->bindValues($params);
213
        }
214
215 6
        $result = $this->request->__invoke($this, $this->sql, $params);
216
217 6
        if (is_array($result)) {
218 2
            $this->errorCode    = $result['code'];
219 2
            $this->errorMessage = $result['message'];
220
221 2
            return false;
222
        }
223
224 4
        $this->collection = $result;
225
226 4
        return true;
227
    }
228
229
    /**
230
     * Bind `execute`'s $input_parameters values to statement handle.
231
     */
232 72
    private function bindValues(array $params_in): array
233
    {
234 72
        $zero_based = array_key_exists(0, $params_in);
235 72
        foreach ($params_in as $parameter => $value) {
236 2
            if (is_int($parameter) && $zero_based) {
237 2
                $parameter++;
238
            }
239 2
            $this->bindValue($parameter, $value);
240
        }
241
242
        // parameter binding might be unordered, so sort it before execute
243 72
        ksort($this->parameters);
244 72
        return array_values($this->parameters);
245
    }
246
247
    /**
248
     * {@inheritDoc}
249
     */
250 18
    public function fetch($fetch_style = null, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = 0)
251
    {
252 18
        if (!$this->hasExecuted()) {
253 18
            $this->execute();
254
        }
255
256 18
        if (!$this->isSuccessful()) {
257 2
            return false;
258
        }
259
260 16
        if ($this->collection === null || !$this->collection->valid()) {
261 2
            return false;
262
        }
263
264
        // Get the current row
265 14
        $row = $this->collection->current();
266
267
        // Traverse
268 14
        $this->collection->next();
269
270 14
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
271
272
        switch ($fetch_style) {
273
            case PDO::FETCH_NAMED:
274
            case PDO::FETCH_ASSOC:
275 4
                return array_combine($this->collection->getColumns(false), $row);
276
277
            case PDO::FETCH_BOTH:
278 4
                return array_merge($row, array_combine($this->collection->getColumns(false), $row));
279
280
            case PDO::FETCH_BOUND:
281 2
                $this->updateBoundColumns($row);
282
283 2
                return true;
284
285
            case PDO::FETCH_NUM:
286 2
                return $row;
287
288
            case PDO::FETCH_OBJ:
289
                return $this->getObjectResult($this->collection->getColumns(false), $row);
290
291
            default:
292 2
                throw new Exception\UnsupportedException('Unsupported fetch style');
293
        }
294
    }
295
296
    /**
297
     * {@inheritDoc}
298
     */
299 4
    public function bindParam(
300
        $parameter,
301
        &$variable,
302
        $data_type = PDO::PARAM_STR,
303
        $length = null,
304
        $driver_options = null
305
    ) {
306 4
        if (is_numeric($parameter)) {
307 4
            if ($parameter == 0) {
308 2
                throw new Exception\UnsupportedException("0-based parameter binding not supported, use 1-based");
309
            }
310 2
            $this->parameters[$parameter - 1] = &$variable;
311
        } else {
312
            $namedParameterKey = substr($parameter, 0, 1) === ':' ? substr($parameter, 1) : $parameter;
313
            if (in_array($namedParameterKey, $this->namedToPositionalMap, true)) {
314
                foreach ($this->namedToPositionalMap as $key => $value) {
315
                    if ($value == $namedParameterKey) {
316
                        $this->parameters[$key] = &$variable;
317
                    }
318
                }
319
            } else {
320
                throw new Exception\OutOfBoundsException(
321
                    sprintf('The named parameter "%s" does not exist', $parameter)
322
                );
323
            }
324
        }
325
    }
326
327
    /**
328
     * {@inheritDoc}
329
     */
330 2
    public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
331
    {
332 2
        $type = $type ?: PDO::PARAM_STR;
333
334 2
        $this->columnBinding[$column] = [
335 2
            'ref'        => &$param,
336 2
            'type'       => $type,
337 2
            'maxlen'     => $maxlen,
338 2
            'driverdata' => $driverdata,
339 2
        ];
340
    }
341
342
    /**
343
     * {@inheritDoc}
344
     */
345 24
    public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR)
346
    {
347 24
        $value = $this->typedValue($value, $data_type);
348 24
        $this->bindParam($parameter, $value, $data_type);
349
    }
350
351
    /**
352
     * {@inheritDoc}
353
     */
354 4
    public function rowCount()
355
    {
356 4
        if (!$this->hasExecuted()) {
357 4
            $this->execute();
358
        }
359
360 4
        if (!$this->isSuccessful()) {
361 2
            return 0;
362
        }
363
364 2
        return $this->collection->count();
0 ignored issues
show
Bug introduced by
The method count() does not exist on null. ( Ignorable by Annotation )

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

364
        return $this->collection->/** @scrutinizer ignore-call */ count();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
365
    }
366
367
    /**
368
     * {@inheritDoc}
369
     */
370 10
    public function fetchColumn($column_number = 0)
371
    {
372 10
        if (!is_int($column_number)) {
373 2
            throw new Exception\InvalidArgumentException('column_number must be a valid integer');
374
        }
375
376 8
        if (!$this->hasExecuted()) {
377 8
            $this->execute();
378
        }
379
380 8
        if (!$this->isSuccessful()) {
381 2
            return false;
382
        }
383
384 6
        if (!$this->collection->valid()) {
385 4
            return false;
386
        }
387
388 4
        $row = $this->collection->current();
389 4
        $this->collection->next();
390
391 4
        if ($column_number >= count($row)) {
392 2
            throw new Exception\OutOfBoundsException(
393 2
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
394 2
            );
395
        }
396
397 2
        return $row[$column_number];
398
    }
399
400
    /**
401
     * {@inheritDoc}
402
     */
403 26
    public function doFetchAll($fetch_style = null, $fetch_argument = null, $ctor_args = null)
0 ignored issues
show
Unused Code introduced by
The parameter $ctor_args is not used and could be removed. ( Ignorable by Annotation )

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

403
    public function doFetchAll($fetch_style = null, $fetch_argument = null, /** @scrutinizer ignore-unused */ $ctor_args = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
404
    {
405 26
        if (!$this->hasExecuted()) {
406 26
            $this->execute();
407
        }
408
409 26
        if (!$this->isSuccessful()) {
410 2
            return false;
411
        }
412
413 24
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
414
415
        switch ($fetch_style) {
416
            case PDO::FETCH_NUM:
417 2
                return $this->collection->getRows();
418
419
            case PDO::FETCH_NAMED:
420
            case PDO::FETCH_ASSOC:
421 4
                $columns = $this->collection->getColumns(false);
422
423 4
                return $this->collection->map(function (array $row) use ($columns) {
424 4
                    return array_combine($columns, $row);
425 4
                });
426
427
            case PDO::FETCH_BOTH:
428 6
                $columns = $this->collection->getColumns(false);
429
430 6
                return $this->collection->map(function (array $row) use ($columns) {
431 6
                    return array_merge($row, array_combine($columns, $row));
432 6
                });
433
434
            case PDO::FETCH_FUNC:
435 4
                if (!is_callable($fetch_argument)) {
436 2
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
437
                }
438
439 2
                return $this->collection->map(function (array $row) use ($fetch_argument) {
440 2
                    return call_user_func_array($fetch_argument, $row);
441 2
                });
442
443
            case PDO::FETCH_COLUMN:
444 6
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
445
446 6
                if (!is_int($columnIndex)) {
447 2
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
448
                }
449
450 4
                $columns = $this->collection->getColumns(false);
451 4
                if (!isset($columns[$columnIndex])) {
452 2
                    throw new Exception\OutOfBoundsException(
453 2
                        sprintf('Column with the index %d does not exist.', $columnIndex)
454 2
                    );
455
                }
456
457 2
                return $this->collection->map(function (array $row) use ($columnIndex) {
458 2
                    return $row[$columnIndex];
459 2
                });
460
            case PDO::FETCH_OBJ:
461
                $columns = $this->collection->getColumns(false);
462
463
                return $this->collection->map(function (array $row) use ($columns) {
464
                    return $this->getObjectResult($columns, $row);
465
                });
466
467
            default:
468 2
                throw new Exception\UnsupportedException('Unsupported fetch style');
469
        }
470
    }
471
472
    /**
473
     * {@inheritDoc}
474
     */
475 2
    public function fetchObject($class_name = null, $ctor_args = null)
476
    {
477 2
        throw new Exception\UnsupportedException;
478
    }
479
480
    /**
481
     * {@inheritDoc}
482
     */
483 2
    public function errorCode()
484
    {
485 2
        return $this->errorCode;
486
    }
487
488
    /**
489
     * {@inheritDoc}
490
     */
491 4
    public function errorInfo()
492
    {
493 4
        if ($this->errorCode === null) {
494 4
            return null;
495
        }
496
497 4
        switch ($this->errorCode) {
498 4
            case CrateConst::ERR_INVALID_SQL:
499 2
                $ansiErrorCode = 42000;
500 2
                break;
501
502
            default:
503 2
                $ansiErrorCode = 'Not available';
504 2
                break;
505
        }
506
507 4
        return [
508 4
            $ansiErrorCode,
509 4
            $this->errorCode,
510 4
            $this->errorMessage,
511 4
        ];
512
    }
513
514
    /**
515
     * {@inheritDoc}
516
     */
517 2
    public function setAttribute($attribute, $value)
518
    {
519 2
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
520
    }
521
522
    /**
523
     * {@inheritDoc}
524
     */
525 2
    public function getAttribute($attribute)
526
    {
527 2
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
528
    }
529
530
    /**
531
     * {@inheritDoc}
532
     */
533 4
    public function columnCount()
534
    {
535 4
        if (!$this->hasExecuted()) {
536 4
            $this->execute();
537
        }
538
539 4
        return count($this->collection->getColumns(false));
540
    }
541
542
    /**
543
     * {@inheritDoc}
544
     */
545 2
    public function getColumnMeta($column)
546
    {
547 2
        throw new Exception\UnsupportedException;
548
    }
549
550
    /**
551
     * {@inheritDoc}
552
     */
553 28
    public function doSetFetchMode($mode, $params = null)
554
    {
555 28
        $args     = func_get_args();
556 28
        $argCount = count($args);
557
558
        switch ($mode) {
559
            case PDO::FETCH_COLUMN:
560 6
                if ($argCount != 2) {
561 2
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
562
                }
563
564 4
                if (!is_int($params)) {
565 2
                    throw new Exception\InvalidArgumentException('colno must be an integer');
566
                }
567
568 2
                $this->options['fetchMode']   = $mode;
569 2
                $this->options['fetchColumn'] = $params;
570 2
                break;
571
572
            case PDO::FETCH_ASSOC:
573
            case PDO::FETCH_NUM:
574
            case PDO::FETCH_BOTH:
575
            case PDO::FETCH_BOUND:
576
            case PDO::FETCH_NAMED:
577
            case PDO::FETCH_OBJ:
578 20
                if ($params !== null) {
579 10
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
580
                }
581
582 10
                $this->options['fetchMode'] = $mode;
583 10
                break;
584
585
            default:
586 2
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
587
        }
588
589 12
        return true;
590
    }
591
592
    /**
593
     * {@inheritDoc}
594
     */
595 4
    public function nextRowset()
596
    {
597 4
        if (!$this->hasExecuted()) {
598 4
            $this->execute();
599
        }
600
601 4
        if (!$this->isSuccessful()) {
602 2
            return false;
603
        }
604
605 2
        $this->collection->next();
606
607 2
        return $this->collection->valid();
608
    }
609
610
    /**
611
     * {@inheritDoc}
612
     */
613 2
    public function closeCursor()
614
    {
615 2
        $this->errorCode  = 0;
616 2
        $this->collection = null;
617
618 2
        return true;
619
    }
620
621
    /**
622
     * {@inheritDoc}
623
     */
624 2
    public function debugDumpParams()
625
    {
626 2
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
627
    }
628
629
    /**
630
     * {@Inheritdoc}
631
     */
632 2
    public function getIterator(): \Iterator
633
    {
634 2
        $results = $this->fetchAll();
0 ignored issues
show
Deprecated Code introduced by
The function Crate\PDO\PDOStatement::fetchAll() has been deprecated: Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead. ( Ignorable by Annotation )

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

634
        $results = /** @scrutinizer ignore-deprecated */ $this->fetchAll();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
635 2
        if ($results === false) {
0 ignored issues
show
introduced by
The condition $results === false is always false.
Loading history...
636
            throw new Exception\RuntimeException('Failure when fetching data');
637
        }
638 2
        return new ArrayIterator($results);
639
    }
640
641 32
    private function typedValue($value, $data_type)
642
    {
643 32
        if (null === $value) {
644
            // Do not typecast null values
645 6
            return null;
646
        }
647
648
        switch ($data_type) {
649
            case PDO::PARAM_FLOAT:
650
            case PDO::PARAM_DOUBLE:
651 2
                return (float)$value;
652
653
            case PDO::PARAM_INT:
654
            case PDO::PARAM_LONG:
655 6
                return (int)$value;
656
657
            case PDO::PARAM_NULL:
658 4
                return null;
659
660
            case PDO::PARAM_BOOL:
661 14
                return filter_var($value, FILTER_VALIDATE_BOOLEAN);
662
663
            case PDO::PARAM_STR:
664
            case PDO::PARAM_IP:
665 8
                return (string)$value;
666
667
            case PDO::PARAM_OBJECT:
668
            case PDO::PARAM_ARRAY:
669 2
                return (array)$value;
670
671
            case PDO::PARAM_TIMESTAMP:
672 2
                if (is_numeric($value)) {
673 2
                    return (int)$value;
674
                }
675
676 2
                return (string)$value;
677
678
            default:
679 2
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
680
        }
681
    }
682
683
    /**
684
     * Generate object from array
685
     *
686
     * @param array $columns
687
     * @param array $row
688
     */
689
    private function getObjectResult(array $columns, array $row)
690
    {
691
        $obj = new \stdClass();
692
        foreach ($columns as $key => $column) {
693
            $obj->{$column} = $row[$key];
694
        }
695
696
        return $obj;
697
    }
698
699 2
    public function isBulkMode()
700
    {
701 2
        return $this->options["bulkMode"];
702
    }
703
}
704