Passed
Pull Request — main (#143)
by Andreas
12:11
created

PDOStatement::bind_values()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 7
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 13
ccs 7
cts 7
cp 1
crap 4
rs 10
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 4
     */
100
    public function __construct(PDOInterface $pdo, Closure $request, $sql, array $options)
101 4
    {
102 4
        $this->sql     = $this->replaceNamedParametersWithPositionals($sql);
103 4
        $this->pdo     = $pdo;
104 4
        $this->options = array_merge($this->options, $options);
105
        $this->request = $request;
106
107 158
        // Storing `bulkMode` flag because calling `pdo->getAttribute()` at runtime reveals weird errors.
108
        $this->options["bulkMode"] = $this->pdo->getAttribute(PDO::CRATE_ATTR_BULK_MODE);
109 158
    }
110 158
111
    private function replaceNamedParametersWithPositionals($sql)
112 4
    {
113
        if (strpos($sql, ':') === false) {
114 4
            return $sql;
115 4
        }
116 4
        $pattern = '/:((?:[\w|\d|_](?=([^\'\\\]*(\\\.|\'([^\'\\\]*\\\.)*[^\'\\\]*\'))*[^\']*$))*)/';
117 4
118 2
        $idx      = 1;
119
        $callback = function ($matches) use (&$idx) {
120 4
            $value = $matches[1];
121 4
            if (empty($value)) {
122
                return $matches[0];
123 4
            }
124 4
            $this->namedToPositionalMap[$idx] = $value;
125
            $idx++;
126 4
127
            return '?';
128
        };
129
130
        return preg_replace_callback($pattern, $callback, $sql);
131
    }
132
133
    /**
134
     * Determines if the statement has been executed
135
     *
136 66
     * @internal
137
     *
138 66
     * @return bool
139
     */
140
    private function hasExecuted()
141
    {
142
        return ($this->collection !== null || $this->errorCode !== null);
143
    }
144
145
    /**
146
     * Internal pointer to mark the state of the current query
147
     *
148 62
     * @internal
149
     *
150 62
     * @return bool
151
     */
152
    private function isSuccessful()
153
    {
154
        if (!$this->hasExecuted()) {
155
            // @codeCoverageIgnoreStart
156 62
            throw new Exception\LogicException('The statement has not been executed yet');
157
            // @codeCoverageIgnoreEnd
158
        }
159
160
        return $this->collection !== null;
161
    }
162
163
    /**
164
     * Get the fetch style to be used
165
     *
166 6
     * @internal
167
     *
168 6
     * @return int
169
     */
170
    private function getFetchStyle()
171
    {
172
        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 2
     * @param array $row
181
     *
182
     * @return void
183 2
     */
184
    private function updateBoundColumns(array $row)
185
    {
186
187 2
        if (!$this->isSuccessful()) {
188 2
            return;
189 2
        }
190
191
        foreach ($this->columnBinding as $column => &$metadata) {
192
            $index = $this->collection->getColumnIndex($column);
193
            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 2
                continue;
197 2
            }
198
199
            // Update by reference
200
            $value           = $this->typedValue($row[$index], $metadata['type']);
201
            $metadata['ref'] = $value;
202
        }
203
    }
204 6
205
    /**
206 6
     * {@inheritDoc}
207 6
     */
208 6
    public function execute($input_parameters = null)
209 2
    {
210 2
        $params = ArrayUtils::toArray($input_parameters);
211
212 2
        // In bulk mode, propagate input parameters 1:1.
213
        // In regular mode, translate input parameters to `bindValue` calls.
214
        if ($this->options["bulkMode"] !== true) {
215
            $params = $this->bind_values($params);
216 6
        }
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 18
    private function bind_values($params_in)
0 ignored issues
show
Coding Style introduced by
Method name "PDOStatement::bind_values" is not in camel caps format
Loading history...
236
    {
237 18
        $zero_based = array_key_exists(0, $params_in);
238 18
        foreach ($params_in as $parameter => $value) {
239
            if (is_int($parameter) && $zero_based) {
240
                $parameter++;
241 18
            }
242 2
            $this->bindValue($parameter, $value);
243
        }
244
245 16
        // parameter binding might be unordered, so sort it before execute
246 2
        ksort($this->parameters);
247
        return array_values($this->parameters);
248
    }
249
250 14
    /**
251
     * {@inheritDoc}
252
     */
253 14
    public function fetch($fetch_style = null, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = 0)
254
    {
255 14
        if (!$this->hasExecuted()) {
256
            $this->execute();
257
        }
258
259
        if (!$this->isSuccessful()) {
260 4
            return false;
261
        }
262
263 4
        if ($this->collection === null || !$this->collection->valid()) {
264
            return false;
265
        }
266 2
267
        // Get the current row
268 2
        $row = $this->collection->current();
269
270
        // Traverse
271 2
        $this->collection->next();
272
273
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
274
275
        switch ($fetch_style) {
276
            case PDO::FETCH_NAMED:
277 2
            case PDO::FETCH_ASSOC:
278
                return array_combine($this->collection->getColumns(false), $row);
279
280
            case PDO::FETCH_BOTH:
281
                return array_merge($row, array_combine($this->collection->getColumns(false), $row));
282
283
            case PDO::FETCH_BOUND:
284 4
                $this->updateBoundColumns($row);
285
286
                return true;
287
288
            case PDO::FETCH_NUM:
289
                return $row;
290
291 4
            case PDO::FETCH_OBJ:
292 4
                return $this->getObjectResult($this->collection->getColumns(false), $row);
293 2
294
            default:
295 2
                throw new Exception\UnsupportedException('Unsupported fetch style');
296
        }
297
    }
298
299
    /**
300
     * {@inheritDoc}
301
     */
302
    public function bindParam(
303
        $parameter,
304
        &$variable,
305
        $data_type = PDO::PARAM_STR,
306
        $length = null,
307
        $driver_options = null
308
    ) {
309
        if (is_numeric($parameter)) {
310
            if ($parameter == 0) {
311
                throw new Exception\UnsupportedException("0-based parameter binding not supported, use 1-based");
312
            }
313
            $this->parameters[$parameter - 1] = &$variable;
314
        } else {
315 2
            $namedParameterKey = substr($parameter, 0, 1) === ':' ? substr($parameter, 1) : $parameter;
316
            if (in_array($namedParameterKey, $this->namedToPositionalMap, true)) {
317 2
                foreach ($this->namedToPositionalMap as $key => $value) {
318
                    if ($value == $namedParameterKey) {
319 2
                        $this->parameters[$key] = &$variable;
320 2
                    }
321 2
                }
322 2
            } else {
323 2
                throw new Exception\OutOfBoundsException(
324 2
                    sprintf('The named parameter "%s" does not exist', $parameter)
325
                );
326
            }
327
        }
328
    }
329
330 24
    /**
331
     * {@inheritDoc}
332 24
     */
333 24
    public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
334
    {
335
        $type = $type ?: PDO::PARAM_STR;
336
337
        $this->columnBinding[$column] = [
338
            'ref'        => &$param,
339 4
            'type'       => $type,
340
            'maxlen'     => $maxlen,
341 4
            'driverdata' => $driverdata,
342 4
        ];
343
    }
344
345 4
    /**
346 2
     * {@inheritDoc}
347
     */
348
    public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR)
349 2
    {
350
        $value = $this->typedValue($value, $data_type);
351
        $this->bindParam($parameter, $value, $data_type);
352
    }
353
354
    /**
355 10
     * {@inheritDoc}
356
     */
357 10
    public function rowCount()
358 2
    {
359
        if (!$this->hasExecuted()) {
360
            $this->execute();
361 8
        }
362 8
363
        if (!$this->isSuccessful()) {
364
            return 0;
365 8
        }
366 2
367
        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 6
370 4
    /**
371
     * {@inheritDoc}
372
     */
373 4
    public function fetchColumn($column_number = 0)
374 4
    {
375
        if (!is_int($column_number)) {
376 4
            throw new Exception\InvalidArgumentException('column_number must be a valid integer');
377 2
        }
378 2
379 2
        if (!$this->hasExecuted()) {
380
            $this->execute();
381
        }
382 2
383
        if (!$this->isSuccessful()) {
384
            return false;
385
        }
386
387
        if (!$this->collection->valid()) {
388 26
            return false;
389
        }
390 26
391 26
        $row = $this->collection->current();
392
        $this->collection->next();
393
394 26
        if ($column_number >= count($row)) {
395 2
            throw new Exception\OutOfBoundsException(
396
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
397
            );
398 24
        }
399
400
        return $row[$column_number];
401
    }
402 2
403
    /**
404
     * {@inheritDoc}
405
     */
406 4
    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 4
        if (!$this->hasExecuted()) {
409 4
            $this->execute();
410 4
        }
411
412
        if (!$this->isSuccessful()) {
413 6
            return false;
414
        }
415 6
416 6
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
417 6
418
        switch ($fetch_style) {
419
            case PDO::FETCH_NUM:
420 4
                return $this->collection->getRows();
421 2
422
            case PDO::FETCH_NAMED:
423
            case PDO::FETCH_ASSOC:
424 2
                $columns = $this->collection->getColumns(false);
425 2
426 2
                return $this->collection->map(function (array $row) use ($columns) {
427
                    return array_combine($columns, $row);
428
                });
429 6
430
            case PDO::FETCH_BOTH:
431 6
                $columns = $this->collection->getColumns(false);
432 2
433
                return $this->collection->map(function (array $row) use ($columns) {
434
                    return array_merge($row, array_combine($columns, $row));
435 4
                });
436 4
437 2
            case PDO::FETCH_FUNC:
438 2
                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
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
448
449
                if (!is_int($columnIndex)) {
450
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
451
                }
452
453 2
                $columns = $this->collection->getColumns(false);
454
                if (!isset($columns[$columnIndex])) {
455
                    throw new Exception\OutOfBoundsException(
456
                        sprintf('Column with the index %d does not exist.', $columnIndex)
457
                    );
458
                }
459
460 2
                return $this->collection->map(function (array $row) use ($columnIndex) {
461
                    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 2
                });
469
470 2
            default:
471
                throw new Exception\UnsupportedException('Unsupported fetch style');
472
        }
473
    }
474
475
    /**
476 4
     * {@inheritDoc}
477
     */
478 4
    public function fetchObject($class_name = null, $ctor_args = null)
479 4
    {
480
        throw new Exception\UnsupportedException;
481
    }
482 4
483 2
    /**
484 2
     * {@inheritDoc}
485 2
     */
486
    public function errorCode()
487
    {
488 2
        return $this->errorCode;
489 2
    }
490
491
    /**
492 4
     * {@inheritDoc}
493 4
     */
494 4
    public function errorInfo()
495 4
    {
496 4
        if ($this->errorCode === null) {
497
            return null;
498
        }
499
500
        switch ($this->errorCode) {
501
            case CrateConst::ERR_INVALID_SQL:
502 2
                $ansiErrorCode = 42000;
503
                break;
504 2
505
            default:
506
                $ansiErrorCode = 'Not available';
507
                break;
508
        }
509
510 2
        return [
511
            $ansiErrorCode,
512 2
            $this->errorCode,
513
            $this->errorMessage,
514
        ];
515
    }
516
517
    /**
518 4
     * {@inheritDoc}
519
     */
520 4
    public function setAttribute($attribute, $value)
521 4
    {
522
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
523
    }
524 4
525
    /**
526
     * {@inheritDoc}
527
     */
528
    public function getAttribute($attribute)
529
    {
530 2
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
531
    }
532 2
533
    /**
534
     * {@inheritDoc}
535
     */
536
    public function columnCount()
537
    {
538 28
        if (!$this->hasExecuted()) {
539
            $this->execute();
540 28
        }
541 28
542
        return count($this->collection->getColumns(false));
543
    }
544
545 6
    /**
546 2
     * {@inheritDoc}
547
     */
548
    public function getColumnMeta($column)
549 4
    {
550 2
        throw new Exception\UnsupportedException;
551
    }
552
553 2
    /**
554 2
     * {@inheritDoc}
555 2
     */
556
    public function doSetFetchMode($mode, $params = null)
557
    {
558
        $args     = func_get_args();
559
        $argCount = count($args);
560
561
        switch ($mode) {
562
            case PDO::FETCH_COLUMN:
563 20
                if ($argCount != 2) {
564 10
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
565
                }
566
567 10
                if (!is_int($params)) {
568 10
                    throw new Exception\InvalidArgumentException('colno must be an integer');
569
                }
570
571 2
                $this->options['fetchMode']   = $mode;
572
                $this->options['fetchColumn'] = $params;
573
                break;
574 12
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 4
            case PDO::FETCH_OBJ:
581
                if ($params !== null) {
582 4
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
583 4
                }
584
585
                $this->options['fetchMode'] = $mode;
586 4
                break;
587 2
588
            default:
589
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
590 2
        }
591
592 2
        return true;
593
    }
594
595
    /**
596
     * {@inheritDoc}
597
     */
598 2
    public function nextRowset()
599
    {
600 2
        if (!$this->hasExecuted()) {
601 2
            $this->execute();
602
        }
603 2
604
        if (!$this->isSuccessful()) {
605
            return false;
606
        }
607
608
        $this->collection->next();
609 2
610
        return $this->collection->valid();
611 2
    }
612
613
    /**
614
     * {@inheritDoc}
615
     */
616
    public function closeCursor()
617 2
    {
618
        $this->errorCode  = 0;
619 2
        $this->collection = null;
620 2
621
        return true;
622
    }
623 2
624
    /**
625
     * {@inheritDoc}
626 32
     */
627
    public function debugDumpParams()
628 32
    {
629
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
630 6
    }
631
632
    /**
633
     * {@Inheritdoc}
634
     */
635
    public function getIterator(): \Iterator
636 2
    {
637
        $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
        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 6
        }
641
        return new ArrayIterator($results);
642
    }
643 4
644
    private function typedValue($value, $data_type)
645
    {
646 14
        if (null === $value) {
647
            // Do not typecast null values
648
            return null;
649
        }
650 8
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 2
            case PDO::PARAM_LONG:
658 2
                return (int)$value;
659
660
            case PDO::PARAM_NULL:
661 2
                return null;
662
663
            case PDO::PARAM_BOOL:
664 2
                return filter_var($value, FILTER_VALIDATE_BOOLEAN);
665
666
            case PDO::PARAM_STR:
667
            case PDO::PARAM_IP:
668
                return (string)$value;
669
670
            case PDO::PARAM_OBJECT:
671
            case PDO::PARAM_ARRAY:
672
                return (array)$value;
673
674
            case PDO::PARAM_TIMESTAMP:
675
                if (is_numeric($value)) {
676
                    return (int)$value;
677
                }
678
679
                return (string)$value;
680
681
            default:
682
                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