Passed
Push — main ( 681f41...5f9dd5 )
by Andreas
01:54
created

PDOStatement::bindColumn()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 1
b 0
f 0
nc 1
nop 5
dl 0
loc 10
ccs 8
cts 8
cp 1
crap 2
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
use PDO;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Crate\PDO\PDO. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
35
36
class PDOStatement extends BasePDOStatement implements IteratorAggregate
37
{
38
    use PDOStatementImplementation;
39
40
    /**
41
     * @var array
42
     */
43
    private $parameters = [];
44
45
    /**
46
     * @var string|null
47
     */
48
    private $errorCode;
49
50
    /**
51
     * @var string|null
52
     */
53
    private $errorMessage;
54
55
    /**
56
     * @var string
57
     */
58
    private $sql;
59
60
    /**
61
     * @var array
62
     */
63
    private $options = [
64
        'bulkMode'           => false,
65
        'fetchMode'          => null,
66
        'fetchColumn'        => 0,
67
        'fetchClass'         => 'array',
68
        'fetchClassCtorArgs' => null,
69
    ];
70
71
    /**
72
     * Used for the {@see PDO::FETCH_BOUND}
73
     *
74
     * @var array
75
     */
76
    private $columnBinding = [];
77
78
    /**
79
     * @var CollectionInterface|null
80
     */
81
    private $collection;
82
83
    /**
84
     * @var PDOInterface
85
     */
86
    private $pdo;
87
88
    /**
89
     * @var Closure
90
     */
91
    private $request;
92
93
    private $namedToPositionalMap = [];
94
95
    /**
96
     * @param PDOInterface $pdo
97
     * @param Closure      $request
98
     * @param string       $sql
99
     * @param array        $options
100
     */
101 4
    public function __construct(PDOInterface $pdo, Closure $request, $sql, array $options)
102
    {
103 4
        $this->sql     = $this->replaceNamedParametersWithPositionals($sql);
104 4
        $this->pdo     = $pdo;
105 4
        $this->options = array_merge($this->options, $options);
106 4
        $this->request = $request;
107
    }
108
109 162
    private function replaceNamedParametersWithPositionals($sql)
110
    {
111 162
        if (strpos($sql, ':') === false) {
112 162
            return $sql;
113
        }
114 4
        $pattern = '/:((?:[\w|\d|_](?=([^\'\\\]*(\\\.|\'([^\'\\\]*\\\.)*[^\'\\\]*\'))*[^\']*$))*)/';
115
116 4
        $idx      = 1;
117 4
        $callback = function ($matches) use (&$idx) {
118 4
            $value = $matches[1];
119 4
            if (empty($value)) {
120 2
                return $matches[0];
121
            }
122 4
            $this->namedToPositionalMap[$idx] = $value;
123 4
            $idx++;
124
125 4
            return '?';
126 4
        };
127
128 4
        return preg_replace_callback($pattern, $callback, $sql);
129
    }
130
131
    /**
132
     * Determines if the statement has been executed
133
     *
134
     * @internal
135
     *
136
     * @return bool
137
     */
138 66
    private function hasExecuted()
139
    {
140 66
        return ($this->collection !== null || $this->errorCode !== null);
141
    }
142
143
    /**
144
     * Internal pointer to mark the state of the current query
145
     *
146
     * @internal
147
     *
148
     * @return bool
149
     */
150 62
    private function isSuccessful()
151
    {
152 62
        if (!$this->hasExecuted()) {
153
            // @codeCoverageIgnoreStart
154
            throw new Exception\LogicException('The statement has not been executed yet');
155
            // @codeCoverageIgnoreEnd
156
        }
157
158 62
        return $this->collection !== null;
159
    }
160
161
    /**
162
     * Get the fetch style to be used
163
     *
164
     * @internal
165
     *
166
     * @return int
167
     */
168 6
    private function getFetchStyle()
169
    {
170 6
        return $this->options['fetchMode'] ?: $this->pdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE);
171
    }
172
173
    /**
174
     * Update all the bound column references
175
     *
176
     * @internal
177
     *
178
     * @param array $row
179
     *
180
     * @return void
181
     */
182 2
    private function updateBoundColumns(array $row)
183
    {
184
185 2
        if (!$this->isSuccessful()) {
186
            return;
187
        }
188
189 2
        foreach ($this->columnBinding as $column => &$metadata) {
190 2
            $index = $this->collection->getColumnIndex($column);
191 2
            if ($index === null) {
192
                // todo: I would like to throw an exception and tell someone they screwed up
193
                // but i think that would violate the PDO api
194
                continue;
195
            }
196
197
            // Update by reference
198 2
            $value           = $this->typedValue($row[$index], $metadata['type']);
199 2
            $metadata['ref'] = $value;
200
        }
201
    }
202
203
    /**
204
     * {@inheritDoc}
205
     */
206 6
    public function execute($input_parameters = null): bool
207
    {
208 6
        $params = ArrayUtils::toArray($input_parameters);
209
210
        // In bulk mode, propagate input parameters 1:1.
211
        // In regular mode, translate input parameters to `bindValue` calls.
212 6
        if ($this->options["bulkMode"] !== true) {
213 6
            $params = $this->bindValues($params);
214
        }
215
216 6
        $result = $this->request->__invoke($this, $this->sql, $params);
217
218 6
        if (is_array($result)) {
219 2
            $this->errorCode    = strval($result['code']);
220 2
            $this->errorMessage = strval($result['message']);
221
222 2
            return false;
223
        }
224
225 4
        $this->collection = $result;
226
227 4
        return true;
228
    }
229
230
    /**
231
     * Bind `execute`'s $input_parameters values to statement handle.
232
     */
233 72
    private function bindValues(array $params_in): array
234
    {
235 72
        $zero_based = array_key_exists(0, $params_in);
236 72
        foreach ($params_in as $parameter => $value) {
237 2
            if (is_int($parameter) && $zero_based) {
238 2
                $parameter++;
239
            }
240 2
            $this->bindValue($parameter, $value);
241
        }
242
243
        // parameter binding might be unordered, so sort it before execute
244 72
        ksort($this->parameters);
245 72
        return array_values($this->parameters);
246
    }
247
248
    /**
249
     * {@inheritDoc}
250
     */
251 9
    #[\ReturnTypeWillChange]
252 9
    public function fetch($fetch_style = null, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = 0)
253
    {
254 18
        if (!$this->hasExecuted()) {
255 18
            $this->execute();
256
        }
257
258 18
        if (!$this->isSuccessful()) {
259 2
            return false;
260
        }
261
262 16
        if ($this->collection === null || !$this->collection->valid()) {
263 2
            return false;
264
        }
265
266
        // Get the current row
267 14
        $row = $this->collection->current();
268
269
        // Traverse
270 14
        $this->collection->next();
271
272 14
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
273
274
        switch ($fetch_style) {
275
            case PDO::FETCH_NAMED:
276
            case PDO::FETCH_ASSOC:
277 4
                return array_combine($this->collection->getColumns(false), $row);
278
279
            case PDO::FETCH_BOTH:
280 4
                return array_merge($row, array_combine($this->collection->getColumns(false), $row));
281
282
            case PDO::FETCH_BOUND:
283 2
                $this->updateBoundColumns($row);
284
285 2
                return true;
286
287
            case PDO::FETCH_NUM:
288 2
                return $row;
289
290
            case PDO::FETCH_OBJ:
291
                return $this->getObjectResult($this->collection->getColumns(false), $row);
292
293
            default:
294 2
                throw new Exception\UnsupportedException('Unsupported fetch style: ' . $fetch_style);
295
        }
296
    }
297
298
    /**
299
     * {@inheritDoc}
300
     */
301 2
    #[\ReturnTypeWillChange]
302 2
    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 1
    #[\ReturnTypeWillChange]
334 1
    public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
335
    {
336 2
        $type = $type ?: PDO::PARAM_STR;
337
338 2
        $this->columnBinding[$column] = [
339 2
            'ref'        => &$param,
340 2
            'type'       => $type,
341 2
            'maxlen'     => $maxlen,
342 2
            'driverdata' => $driverdata,
343 2
        ];
344
    }
345
346
    /**
347
     * {@inheritDoc}
348
     */
349 12
    #[\ReturnTypeWillChange]
350 12
    public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR)
351
    {
352 24
        $value = $this->typedValue($value, $data_type);
353 24
        $this->bindParam($parameter, $value, $data_type);
354 24
        return true;
355
    }
356
357
    /**
358
     * {@inheritDoc}
359
     */
360 4
    public function rowCount(): int
361
    {
362 4
        if (!$this->hasExecuted()) {
363 4
            $this->execute();
364
        }
365
366 4
        if (!$this->isSuccessful()) {
367 2
            return 0;
368
        }
369
370 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

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

410
    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...
411
    {
412 26
        if (!$this->hasExecuted()) {
413 26
            $this->execute();
414
        }
415
416 26
        if (!$this->isSuccessful()) {
417 2
            return false;
418
        }
419
420 24
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
421
422
        switch ($fetch_style) {
423
            case PDO::FETCH_NUM:
424 2
                return $this->collection->getRows();
425
426
            case PDO::FETCH_NAMED:
427
            case PDO::FETCH_ASSOC:
428 4
                $columns = $this->collection->getColumns(false);
429
430 4
                return $this->collection->map(function (array $row) use ($columns) {
431 4
                    return array_combine($columns, $row);
432 4
                });
433
434
            case PDO::FETCH_BOTH:
435 6
                $columns = $this->collection->getColumns(false);
436
437 6
                return $this->collection->map(function (array $row) use ($columns) {
438 6
                    return array_merge($row, array_combine($columns, $row));
439 6
                });
440
441
            case PDO::FETCH_FUNC:
442 4
                if (!is_callable($fetch_argument)) {
443 2
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
444
                }
445
446 2
                return $this->collection->map(function (array $row) use ($fetch_argument) {
447 2
                    return call_user_func_array($fetch_argument, $row);
448 2
                });
449
450
            case PDO::FETCH_COLUMN:
451 6
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
452
453 6
                if (!is_int($columnIndex)) {
454 2
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
455
                }
456
457 4
                $columns = $this->collection->getColumns(false);
458 4
                if (!isset($columns[$columnIndex])) {
459 2
                    throw new Exception\OutOfBoundsException(
460 2
                        sprintf('Column with the index %d does not exist.', $columnIndex)
461 2
                    );
462
                }
463
464 2
                return $this->collection->map(function (array $row) use ($columnIndex) {
465 2
                    return $row[$columnIndex];
466 2
                });
467
            case PDO::FETCH_OBJ:
468
                $columns = $this->collection->getColumns(false);
469
470
                return $this->collection->map(function (array $row) use ($columns) {
471
                    return $this->getObjectResult($columns, $row);
472
                });
473
474
            default:
475 2
                throw new Exception\UnsupportedException('Unsupported fetch style: ' . $fetch_style);
476
        }
477
    }
478
479
    /**
480
     * {@inheritDoc}
481
     */
482 1
    #[\ReturnTypeWillChange]
483 1
    public function fetchObject($class_name = null, $ctor_args = null)
484
    {
485 2
        throw new Exception\UnsupportedException;
486
    }
487
488
    /**
489
     * {@inheritDoc}
490
     */
491 2
    public function errorCode(): ?string
492
    {
493 2
        return $this->errorCode;
494
    }
495
496
    /**
497
     * {@inheritDoc}
498
     *
499
     * @return array
500
     */
501 4
    #[\ReturnTypeWillChange]
502 4
    public function errorInfo()
503
    {
504 8
        if ($this->errorCode === null) {
505 4
            return ["00000", null, null];
506
        }
507
508 4
        switch ($this->errorCode) {
509
            case CrateConst::ERR_INVALID_SQL:
510 2
                $ansiErrorCode = '42000';
511 2
                break;
512
513
            default:
514 2
                $ansiErrorCode = 'Not available';
515 2
                break;
516
        }
517
518 4
        return [
519 4
            strval($ansiErrorCode),
520 4
            intval($this->errorCode),
521 4
            strval($this->errorMessage),
522 4
        ];
523
    }
524
525
    /**
526
     * {@inheritDoc}
527
     *
528
     * @param int $attribute
529
     * @param mixed $value
530
     */
531 1
    #[\ReturnTypeWillChange]
532 1
    public function setAttribute($attribute, $value)
533
    {
534 2
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
535
    }
536
537
    /**
538
     * {@inheritDoc}
539
     */
540 1
    #[\ReturnTypeWillChange]
541 1
    public function getAttribute($attribute)
542
    {
543 2
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
544
    }
545
546
    /**
547
     * {@inheritDoc}
548
     */
549 4
    public function columnCount(): int
550
    {
551 4
        if (!$this->hasExecuted()) {
552 4
            $this->execute();
553
        }
554
555 4
        return count($this->collection->getColumns(false));
556
    }
557
558
    /**
559
     * {@inheritDoc}
560
     */
561 1
    #[\ReturnTypeWillChange]
562 1
    public function getColumnMeta($column)
563
    {
564 2
        throw new Exception\UnsupportedException;
565
    }
566
567
    /**
568
     * {@inheritDoc}
569
     */
570 28
    public function doSetFetchMode($mode, $params = null)
571
    {
572 28
        $args     = func_get_args();
573 28
        $argCount = count($args);
574
575
        switch ($mode) {
576
            case PDO::FETCH_COLUMN:
577 6
                if ($argCount != 2) {
578 2
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
579
                }
580
581 4
                if (!is_int($params)) {
582 2
                    throw new Exception\InvalidArgumentException('colno must be an integer');
583
                }
584
585 2
                $this->options['fetchMode']   = $mode;
586 2
                $this->options['fetchColumn'] = $params;
587 2
                break;
588
589
            case PDO::FETCH_ASSOC:
590
            case PDO::FETCH_NUM:
591
            case PDO::FETCH_BOTH:
592
            case PDO::FETCH_BOUND:
593
            case PDO::FETCH_NAMED:
594
            case PDO::FETCH_OBJ:
595 20
                if ($params !== null) {
596 10
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
597
                }
598
599 10
                $this->options['fetchMode'] = $mode;
600 10
                break;
601
602
            default:
603 2
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
604
        }
605
606 12
        return true;
607
    }
608
609
    /**
610
     * {@inheritDoc}
611
     */
612 4
    public function nextRowset(): bool
613
    {
614 4
        if (!$this->hasExecuted()) {
615 4
            $this->execute();
616
        }
617
618 4
        if (!$this->isSuccessful()) {
619 2
            return false;
620
        }
621
622 2
        $this->collection->next();
623
624 2
        return $this->collection->valid();
625
    }
626
627
    /**
628
     * {@inheritDoc}
629
     */
630 2
    public function closeCursor(): bool
631
    {
632 2
        $this->errorCode  = null;
633 2
        $this->collection = null;
634
635 2
        return true;
636
    }
637
638
    /**
639
     * {@inheritDoc}
640
     */
641 2
    public function debugDumpParams(): ?bool
642
    {
643 2
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
644
    }
645
646
    /**
647
     * {@Inheritdoc}
648
     */
649 2
    public function getIterator(): \Iterator
650
    {
651 2
        $results = $this->fetchAll();
652 2
        if ($results === false) {
0 ignored issues
show
introduced by
The condition $results === false is always false.
Loading history...
653
            throw new Exception\RuntimeException('Failure when fetching data');
654
        }
655 2
        return new ArrayIterator($results);
656
    }
657
658 32
    private function typedValue($value, $data_type)
659
    {
660 32
        if (null === $value) {
661
            // Do not typecast null values
662 6
            return null;
663
        }
664
665
        switch ($data_type) {
666
            case PDOCrateDB::PARAM_FLOAT:
667
            case PDOCrateDB::PARAM_DOUBLE:
668 2
                return (float)$value;
669
670
            case PDO::PARAM_INT:
671
            case PDOCrateDB::PARAM_LONG:
672 6
                return (int)$value;
673
674
            case PDO::PARAM_NULL:
675 4
                return null;
676
677
            case PDO::PARAM_BOOL:
678 14
                return filter_var($value, FILTER_VALIDATE_BOOLEAN);
679
680
            case PDO::PARAM_STR:
681
            case PDOCrateDB::PARAM_IP:
682 8
                return (string)$value;
683
684
            case PDOCrateDB::PARAM_OBJECT:
685
            case PDOCrateDB::PARAM_ARRAY:
686 2
                return (array)$value;
687
688
            case PDOCrateDB::PARAM_TIMESTAMP:
689 2
                if (is_numeric($value)) {
690 2
                    return (int)$value;
691
                }
692
693 2
                return (string)$value;
694
695
            default:
696 2
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
697
        }
698
    }
699
700
    /**
701
     * Generate object from array
702
     *
703
     * @param array $columns
704
     * @param array $row
705
     */
706
    private function getObjectResult(array $columns, array $row)
707
    {
708
        $obj = new \stdClass();
709
        foreach ($columns as $key => $column) {
710
            $obj->{$column} = $row[$key];
711
        }
712
713
        return $obj;
714
    }
715
716 2
    public function isBulkMode()
717
    {
718 2
        return $this->options["bulkMode"];
719
    }
720
}
721