Passed
Push — main ( 862a83...587b29 )
by Andreas
02:07
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 20
    public function doSetFetchMode($mode, $params = null)
571
    {
572
573
        switch ($mode) {
574
            case PDO::FETCH_COLUMN:
575 6
                if ($params === null) {
576 2
                    throw new Exception\InvalidArgumentException('Fetch mode requires the colno argument');
577
                }
578
579 4
                if (!is_int($params)) {
580 2
                    throw new Exception\InvalidArgumentException('Column number must be an integer');
581
                }
582
583 2
                $this->options['fetchMode']   = $mode;
584 2
                $this->options['fetchColumn'] = $params;
585 2
                break;
586
587
            case PDO::FETCH_ASSOC:
588
            case PDO::FETCH_NUM:
589
            case PDO::FETCH_BOTH:
590
            case PDO::FETCH_BOUND:
591
            case PDO::FETCH_NAMED:
592
            case PDO::FETCH_OBJ:
593 20
                if ($params !== null) {
594 10
                    throw new Exception\InvalidArgumentException('Fetch mode does not allow any extra arguments');
595
                }
596
597 10
                $this->options['fetchMode'] = $mode;
598 10
                break;
599
600
            default:
601 2
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
602
        }
603
604 12
        return true;
605
    }
606
607
    /**
608
     * {@inheritDoc}
609
     */
610 4
    public function nextRowset(): bool
611
    {
612 4
        if (!$this->hasExecuted()) {
613 4
            $this->execute();
614
        }
615
616 4
        if (!$this->isSuccessful()) {
617 2
            return false;
618
        }
619
620 2
        $this->collection->next();
621
622 2
        return $this->collection->valid();
623
    }
624
625
    /**
626
     * {@inheritDoc}
627
     */
628 2
    public function closeCursor(): bool
629
    {
630 2
        $this->errorCode  = null;
631 2
        $this->collection = null;
632
633 2
        return true;
634
    }
635
636
    /**
637
     * {@inheritDoc}
638
     */
639 2
    public function debugDumpParams(): ?bool
640
    {
641 2
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
642
    }
643
644
    /**
645
     * {@Inheritdoc}
646
     */
647 2
    public function getIterator(): \Iterator
648
    {
649 2
        $results = $this->fetchAll();
650 2
        if ($results === false) {
0 ignored issues
show
introduced by
The condition $results === false is always false.
Loading history...
651
            throw new Exception\RuntimeException('Failure when fetching data');
652
        }
653 2
        return new ArrayIterator($results);
654
    }
655
656 32
    private function typedValue($value, $data_type)
657
    {
658 32
        if (null === $value) {
659
            // Do not typecast null values
660 6
            return null;
661
        }
662
663
        switch ($data_type) {
664
            case PDOCrateDB::PARAM_FLOAT:
665
            case PDOCrateDB::PARAM_DOUBLE:
666 2
                return (float)$value;
667
668
            case PDO::PARAM_INT:
669
            case PDOCrateDB::PARAM_LONG:
670 6
                return (int)$value;
671
672
            case PDO::PARAM_NULL:
673 4
                return null;
674
675
            case PDO::PARAM_BOOL:
676 14
                return filter_var($value, FILTER_VALIDATE_BOOLEAN);
677
678
            case PDO::PARAM_STR:
679
            case PDOCrateDB::PARAM_IP:
680 8
                return (string)$value;
681
682
            case PDOCrateDB::PARAM_OBJECT:
683
            case PDOCrateDB::PARAM_ARRAY:
684 2
                return (array)$value;
685
686
            case PDOCrateDB::PARAM_TIMESTAMP:
687 2
                if (is_numeric($value)) {
688 2
                    return (int)$value;
689
                }
690
691 2
                return (string)$value;
692
693
            default:
694 2
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
695
        }
696
    }
697
698
    /**
699
     * Generate object from array
700
     *
701
     * @param array $columns
702
     * @param array $row
703
     */
704
    private function getObjectResult(array $columns, array $row)
705
    {
706
        $obj = new \stdClass();
707
        foreach ($columns as $key => $column) {
708
            $obj->{$column} = $row[$key];
709
        }
710
711
        return $obj;
712
    }
713
714 2
    public function isBulkMode()
715
    {
716 2
        return $this->options["bulkMode"];
717
    }
718
}
719