Passed
Pull Request — main (#143)
by Andreas
01:39
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
        // Storing `bulkMode` flag because calling `pdo->getAttribute()` at runtime reveals weird errors.
108 4
        $this->options["bulkMode"] = $this->pdo->getAttribute(PDO::CRATE_ATTR_BULK_MODE);
109
    }
110
111 158
    private function replaceNamedParametersWithPositionals($sql)
112
    {
113 158
        if (strpos($sql, ':') === false) {
114 158
            return $sql;
115
        }
116 4
        $pattern = '/:((?:[\w|\d|_](?=([^\'\\\]*(\\\.|\'([^\'\\\]*\\\.)*[^\'\\\]*\'))*[^\']*$))*)/';
117
118 4
        $idx      = 1;
119 4
        $callback = function ($matches) use (&$idx) {
120 4
            $value = $matches[1];
121 4
            if (empty($value)) {
122 2
                return $matches[0];
123
            }
124 4
            $this->namedToPositionalMap[$idx] = $value;
125 4
            $idx++;
126
127 4
            return '?';
128 4
        };
129
130 4
        return preg_replace_callback($pattern, $callback, $sql);
131
    }
132
133
    /**
134
     * Determines if the statement has been executed
135
     *
136
     * @internal
137
     *
138
     * @return bool
139
     */
140 66
    private function hasExecuted()
141
    {
142 66
        return ($this->collection !== null || $this->errorCode !== null);
143
    }
144
145
    /**
146
     * Internal pointer to mark the state of the current query
147
     *
148
     * @internal
149
     *
150
     * @return bool
151
     */
152 62
    private function isSuccessful()
153
    {
154 62
        if (!$this->hasExecuted()) {
155
            // @codeCoverageIgnoreStart
156
            throw new Exception\LogicException('The statement has not been executed yet');
157
            // @codeCoverageIgnoreEnd
158
        }
159
160 62
        return $this->collection !== null;
161
    }
162
163
    /**
164
     * Get the fetch style to be used
165
     *
166
     * @internal
167
     *
168
     * @return int
169
     */
170 6
    private function getFetchStyle()
171
    {
172 6
        return $this->options['fetchMode'] ?: $this->pdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE);
173
    }
174
175
    /**
176
     * Update all the bound column references
177
     *
178
     * @internal
179
     *
180
     * @param array $row
181
     *
182
     * @return void
183
     */
184 2
    private function updateBoundColumns(array $row)
185
    {
186
187 2
        if (!$this->isSuccessful()) {
188
            return;
189
        }
190
191 2
        foreach ($this->columnBinding as $column => &$metadata) {
192 2
            $index = $this->collection->getColumnIndex($column);
193 2
            if ($index === null) {
194
                // todo: I would like to throw an exception and tell someone they screwed up
195
                // but i think that would violate the PDO api
196
                continue;
197
            }
198
199
            // Update by reference
200 2
            $value           = $this->typedValue($row[$index], $metadata['type']);
201 2
            $metadata['ref'] = $value;
202
        }
203
    }
204
205
    /**
206
     * {@inheritDoc}
207
     */
208 6
    public function execute($input_parameters = null): bool
209
    {
210 6
        $params = ArrayUtils::toArray($input_parameters);
211
212
        // In bulk mode, propagate input parameters 1:1.
213
        // In regular mode, translate input parameters to `bindValue` calls.
214 6
        if ($this->options["bulkMode"] !== true) {
215 6
            $params = $this->bindValues($params);
216
        }
217
218 6
        $result = $this->request->__invoke($this, $this->sql, $params);
219
220 6
        if (is_array($result)) {
221 2
            $this->errorCode    = $result['code'];
222 2
            $this->errorMessage = $result['message'];
223
224 2
            return false;
225
        }
226
227 4
        $this->collection = $result;
228
229 4
        return true;
230
    }
231
232
    /**
233
     * Bind `execute`'s $input_parameters values to statement handle.
234
     */
235 72
    private function bindValues(array $params_in): array
236
    {
237 72
        $zero_based = array_key_exists(0, $params_in);
238 72
        foreach ($params_in as $parameter => $value) {
239 2
            if (is_int($parameter) && $zero_based) {
240 2
                $parameter++;
241
            }
242 2
            $this->bindValue($parameter, $value);
243
        }
244
245
        // parameter binding might be unordered, so sort it before execute
246 72
        ksort($this->parameters);
247 72
        return array_values($this->parameters);
248
    }
249
250
    /**
251
     * {@inheritDoc}
252
     */
253 18
    public function fetch($fetch_style = null, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = 0)
254
    {
255 18
        if (!$this->hasExecuted()) {
256 18
            $this->execute();
257
        }
258
259 18
        if (!$this->isSuccessful()) {
260 2
            return false;
261
        }
262
263 16
        if ($this->collection === null || !$this->collection->valid()) {
264 2
            return false;
265
        }
266
267
        // Get the current row
268 14
        $row = $this->collection->current();
269
270
        // Traverse
271 14
        $this->collection->next();
272
273 14
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
274
275
        switch ($fetch_style) {
276
            case PDO::FETCH_NAMED:
277
            case PDO::FETCH_ASSOC:
278 4
                return array_combine($this->collection->getColumns(false), $row);
279
280
            case PDO::FETCH_BOTH:
281 4
                return array_merge($row, array_combine($this->collection->getColumns(false), $row));
282
283
            case PDO::FETCH_BOUND:
284 2
                $this->updateBoundColumns($row);
285
286 2
                return true;
287
288
            case PDO::FETCH_NUM:
289 2
                return $row;
290
291
            case PDO::FETCH_OBJ:
292
                return $this->getObjectResult($this->collection->getColumns(false), $row);
293
294
            default:
295 2
                throw new Exception\UnsupportedException('Unsupported fetch style');
296
        }
297
    }
298
299
    /**
300
     * {@inheritDoc}
301
     */
302 4
    public function bindParam(
303
        $parameter,
304
        &$variable,
305
        $data_type = PDO::PARAM_STR,
306
        $length = null,
307
        $driver_options = null
308
    ) {
309 4
        if (is_numeric($parameter)) {
310 4
            if ($parameter == 0) {
311 2
                throw new Exception\UnsupportedException("0-based parameter binding not supported, use 1-based");
312
            }
313 2
            $this->parameters[$parameter - 1] = &$variable;
314
        } else {
315
            $namedParameterKey = substr($parameter, 0, 1) === ':' ? substr($parameter, 1) : $parameter;
316
            if (in_array($namedParameterKey, $this->namedToPositionalMap, true)) {
317
                foreach ($this->namedToPositionalMap as $key => $value) {
318
                    if ($value == $namedParameterKey) {
319
                        $this->parameters[$key] = &$variable;
320
                    }
321
                }
322
            } else {
323
                throw new Exception\OutOfBoundsException(
324
                    sprintf('The named parameter "%s" does not exist', $parameter)
325
                );
326
            }
327
        }
328
    }
329
330
    /**
331
     * {@inheritDoc}
332
     */
333 2
    public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
334
    {
335 2
        $type = $type ?: PDO::PARAM_STR;
336
337 2
        $this->columnBinding[$column] = [
338 2
            'ref'        => &$param,
339 2
            'type'       => $type,
340 2
            'maxlen'     => $maxlen,
341 2
            'driverdata' => $driverdata,
342 2
        ];
343
    }
344
345
    /**
346
     * {@inheritDoc}
347
     */
348 24
    public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR)
349
    {
350 24
        $value = $this->typedValue($value, $data_type);
351 24
        $this->bindParam($parameter, $value, $data_type);
352
    }
353
354
    /**
355
     * {@inheritDoc}
356
     */
357 4
    public function rowCount()
358
    {
359 4
        if (!$this->hasExecuted()) {
360 4
            $this->execute();
361
        }
362
363 4
        if (!$this->isSuccessful()) {
364 2
            return 0;
365
        }
366
367 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

367
        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...
368
    }
369
370
    /**
371
     * {@inheritDoc}
372
     */
373 10
    public function fetchColumn($column_number = 0)
374
    {
375 10
        if (!is_int($column_number)) {
376 2
            throw new Exception\InvalidArgumentException('column_number must be a valid integer');
377
        }
378
379 8
        if (!$this->hasExecuted()) {
380 8
            $this->execute();
381
        }
382
383 8
        if (!$this->isSuccessful()) {
384 2
            return false;
385
        }
386
387 6
        if (!$this->collection->valid()) {
388 4
            return false;
389
        }
390
391 4
        $row = $this->collection->current();
392 4
        $this->collection->next();
393
394 4
        if ($column_number >= count($row)) {
395 2
            throw new Exception\OutOfBoundsException(
396 2
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
397 2
            );
398
        }
399
400 2
        return $row[$column_number];
401
    }
402
403
    /**
404
     * {@inheritDoc}
405
     */
406 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

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

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