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

PDOStatement::fetch()   C

Complexity

Conditions 12
Paths 18

Size

Total Lines 44
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 12.0247

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 12
eloc 25
c 3
b 0
f 1
nc 18
nop 3
dl 0
loc 44
ccs 17
cts 18
cp 0.9444
crap 12.0247
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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