Passed
Pull Request — main (#116)
by Andreas
06:34
created

PDOStatement::fetch()   C

Complexity

Conditions 12
Paths 18

Size

Total Lines 43
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 12.0292

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 12
eloc 24
c 3
b 0
f 1
nc 18
nop 3
dl 0
loc 43
rs 6.9666
ccs 16
cts 17
cp 0.9412
crap 12.0292

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
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
        'fetchMode'          => null,
64
        'fetchColumn'        => 0,
65
        'fetchClass'         => 'array',
66
        'fetchClassCtorArgs' => null,
67
    ];
68
69
    /**
70
     * Used for the {@see PDO::FETCH_BOUND}
71
     *
72
     * @var array
73
     */
74
    private $columnBinding = [];
75
76
    /**
77
     * @var CollectionInterface|null
78
     */
79
    private $collection;
80
81
    /**
82
     * @var PDOInterface
83
     */
84
    private $pdo;
85
86
    /**
87
     * @var Closure
88
     */
89
    private $request;
90
91
    private $namedToPositionalMap = [];
92
93
    /**
94
     * @param PDOInterface $pdo
95
     * @param Closure      $request
96
     * @param string       $sql
97 2
     * @param array        $options
98
     */
99 2
    public function __construct(PDOInterface $pdo, Closure $request, $sql, array $options)
100 2
    {
101 2
        $this->sql     = $this->replaceNamedParametersWithPositionals($sql);
102 2
        $this->pdo     = $pdo;
103 2
        $this->options = array_merge($this->options, $options);
104
        $this->request = $request;
105 79
    }
106
107 79
    private function replaceNamedParametersWithPositionals($sql)
108 79
    {
109
        if (strpos($sql, ':') === false) {
110 2
            return $sql;
111
        }
112 2
        $pattern = '/:((?:[\w|\d|_](?=([^\'\\\]*(\\\.|\'([^\'\\\]*\\\.)*[^\'\\\]*\'))*[^\']*$))*)/';
113
114 2
        $idx      = 1;
115 2
        $callback = function ($matches) use (&$idx) {
116 1
            $value = $matches[1];
117
            if (empty($value)) {
118 2
                return $matches[0];
119 2
            }
120
            $this->namedToPositionalMap[$idx] = $value;
121 2
            $idx++;
122 2
123
            return '?';
124 2
        };
125
126
        return preg_replace_callback($pattern, $callback, $sql);
127
    }
128
129
    /**
130
     * Determines if the statement has been executed
131
     *
132
     * @internal
133
     *
134 33
     * @return bool
135
     */
136 33
    private function hasExecuted()
137
    {
138
        return ($this->collection !== null || $this->errorCode !== null);
139
    }
140
141
    /**
142
     * Internal pointer to mark the state of the current query
143
     *
144
     * @internal
145
     *
146 31
     * @return bool
147
     */
148 31
    private function isSuccessful()
149
    {
150
        if (!$this->hasExecuted()) {
151
            // @codeCoverageIgnoreStart
152
            throw new Exception\LogicException('The statement has not been executed yet');
153
            // @codeCoverageIgnoreEnd
154 31
        }
155
156
        return $this->collection !== null;
157
    }
158
159
    /**
160
     * Get the fetch style to be used
161
     *
162
     * @internal
163
     *
164 3
     * @return int
165
     */
166 3
    private function getFetchStyle()
167
    {
168
        return $this->options['fetchMode'] ?: $this->pdo->getAttribute(PDO::ATTR_DEFAULT_FETCH_MODE);
169
    }
170
171
    /**
172
     * Update all the bound column references
173
     *
174
     * @internal
175
     *
176
     * @param array $row
177
     *
178 1
     * @return void
179
     */
180
    private function updateBoundColumns(array $row)
181 1
    {
182
183
        if (!$this->isSuccessful()) {
184
            return;
185 1
        }
186 1
187 1
        foreach ($this->columnBinding as $column => &$metadata) {
188
            $index = $this->collection->getColumnIndex($column);
189
            if ($index === null) {
190
                // todo: I would like to throw an exception and tell someone they screwed up
191
                // but i think that would violate the PDO api
192
                continue;
193
            }
194 1
195 1
            // Update by reference
196
            $value           = $this->typedValue($row[$index], $metadata['type']);
197 1
            $metadata['ref'] = $value;
198
        }
199
    }
200
201
    /**
202 3
     * {@inheritDoc}
203
     */
204 3
    public function execute($input_parameters = null)
205 3
    {
206 3
        $input_parameters_array = ArrayUtils::toArray($input_parameters);
207 1
        $zero_based             = array_key_exists(0, $input_parameters_array);
208 1
        foreach ($input_parameters_array as $parameter => $value) {
209
            if (is_int($parameter) && $zero_based) {
210 1
                $parameter++;
211
            }
212
            $this->bindValue($parameter, $value);
213
        }
214 3
215
        // parameter binding might be unordered, so sort it before execute
216 3
        ksort($this->parameters);
217
218 3
        $result = $this->request->__invoke($this, $this->sql, array_values($this->parameters));
219 1
220 1
        if (is_array($result)) {
221
            $this->errorCode    = $result['code'];
222 1
            $this->errorMessage = $result['message'];
223
224
            return false;
225 2
        }
226
227 2
        $this->collection = $result;
228
229
        return true;
230
    }
231
232
    /**
233 9
     * {@inheritDoc}
234
     */
235 9
    public function fetch($fetch_style = null, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = 0)
236 9
    {
237
        if (!$this->hasExecuted()) {
238
            $this->execute();
239 9
        }
240 1
241
        if (!$this->isSuccessful()) {
242
            return false;
243 8
        }
244 1
245
        if ($this->collection === null || !$this->collection->valid()) {
246
            return false;
247
        }
248 7
249
        // Get the current row
250
        $row = $this->collection->current();
251 7
252
        // Traverse
253 7
        $this->collection->next();
254
255 7
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
256
257
        switch ($fetch_style) {
258 2
            case PDO::FETCH_NAMED:
259
            case PDO::FETCH_ASSOC:
260
                return array_combine($this->collection->getColumns(false), $row);
261 2
262
            case PDO::FETCH_BOTH:
263
                return array_merge($row, array_combine($this->collection->getColumns(false), $row));
264 1
265
            case PDO::FETCH_BOUND:
266 1
                $this->updateBoundColumns($row);
267
268
                return true;
269 1
270
            case PDO::FETCH_NUM:
271
                return $row;
272
273
            case PDO::FETCH_OBJ:
274
                return $this->getObjectResult($this->collection->getColumns(false), $row);
275 1
276
            default:
277
                throw new Exception\UnsupportedException('Unsupported fetch style');
278
        }
279
    }
280
281
    /**
282 2
     * {@inheritDoc}
283
     */
284
    public function bindParam(
285
        $parameter,
286
        &$variable,
287
        $data_type = PDO::PARAM_STR,
288
        $length = null,
289 2
        $driver_options = null
290 2
    ) {
291 1
        if (is_numeric($parameter)) {
292
            if ($parameter == 0) {
293 1
                throw new Exception\UnsupportedException("0-based parameter binding not supported, use 1-based");
294
            }
295
            $this->parameters[$parameter - 1] = &$variable;
296
        } else {
297
            $namedParameterKey = substr($parameter, 0, 1) === ':' ? substr($parameter, 1) : $parameter;
298
            if (in_array($namedParameterKey, $this->namedToPositionalMap, true)) {
299
                foreach ($this->namedToPositionalMap as $key => $value) {
300
                    if ($value == $namedParameterKey) {
301
                        $this->parameters[$key] = &$variable;
302
                    }
303
                }
304
            } else {
305
                throw new Exception\OutOfBoundsException(
306
                    sprintf('The named parameter "%s" does not exist', $parameter)
307
                );
308 1
            }
309
        }
310
    }
311
312
    /**
313 1
     * {@inheritDoc}
314
     */
315 1
    public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
316
    {
317 1
        $type = $type ?: PDO::PARAM_STR;
318 1
319 1
        $this->columnBinding[$column] = [
320 1
            'ref'        => &$param,
321 1
            'type'       => $type,
322
            'maxlen'     => $maxlen,
323 1
            'driverdata' => $driverdata,
324
        ];
325
    }
326
327
    /**
328 12
     * {@inheritDoc}
329
     */
330 12
    public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR)
331 12
    {
332 12
        $value = $this->typedValue($value, $data_type);
333
        $this->bindParam($parameter, $value, $data_type);
334
    }
335
336
    /**
337 2
     * {@inheritDoc}
338
     */
339 2
    public function rowCount()
340 2
    {
341
        if (!$this->hasExecuted()) {
342
            $this->execute();
343 2
        }
344 1
345
        if (!$this->isSuccessful()) {
346
            return 0;
347 1
        }
348
349
        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

349
        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...
350
    }
351
352
    /**
353 5
     * {@inheritDoc}
354
     */
355 5
    public function fetchColumn($column_number = 0)
356 1
    {
357
        if (!is_int($column_number)) {
358
            throw new Exception\InvalidArgumentException('column_number must be a valid integer');
359 4
        }
360 4
361
        if (!$this->hasExecuted()) {
362
            $this->execute();
363 4
        }
364 1
365
        if (!$this->isSuccessful()) {
366
            return false;
367 3
        }
368 2
369
        if (!$this->collection->valid()) {
370
            return false;
371 2
        }
372 2
373
        $row = $this->collection->current();
374 2
        $this->collection->next();
375 1
376 1
        if ($column_number >= count($row)) {
377
            throw new Exception\OutOfBoundsException(
378
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
379
            );
380 1
        }
381
382
        return $row[$column_number];
383
    }
384
385
    /**
386 13
     * {@inheritDoc}
387
     */
388 13
    public function doFetchAll($fetch_style = null, $fetch_argument = null, $ctor_args = null)
389 13
    {
390
        if (!$this->hasExecuted()) {
391
            $this->execute();
392 13
        }
393 1
394
        if (!$this->isSuccessful()) {
395
            return false;
396 12
        }
397
398 12
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
399
400 1
        switch ($fetch_style) {
401
            case PDO::FETCH_NUM:
402
                return $this->collection->getRows();
403
404 2
            case PDO::FETCH_NAMED:
405
            case PDO::FETCH_ASSOC:
406
                $columns = $this->collection->getColumns(false);
407 2
408 2
                return $this->collection->map(function (array $row) use ($columns) {
409
                    return array_combine($columns, $row);
410
                });
411 3
412
            case PDO::FETCH_BOTH:
413
                $columns = $this->collection->getColumns(false);
414 3
415 3
                return $this->collection->map(function (array $row) use ($columns) {
416
                    return array_merge($row, array_combine($columns, $row));
417
                });
418 2
419 1
            case PDO::FETCH_FUNC:
420
                if (!is_callable($fetch_argument)) {
421
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
422
                }
423 1
424 1
                return $this->collection->map(function (array $row) use ($fetch_argument) {
425
                    return call_user_func_array($fetch_argument, $row);
426
                });
427 3
428
            case PDO::FETCH_COLUMN:
429 3
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
430 1
431
                if (!is_int($columnIndex)) {
432
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
433 2
                }
434 2
435 1
                $columns = $this->collection->getColumns(false);
436 1
                if (!isset($columns[$columnIndex])) {
437
                    throw new Exception\OutOfBoundsException(
438
                        sprintf('Column with the index %d does not exist.', $columnIndex)
439
                    );
440
                }
441 1
442 1
                return $this->collection->map(function (array $row) use ($columnIndex) {
443
                    return $row[$columnIndex];
444
                });
445
            case PDO::FETCH_OBJ:
446
                $columns = $this->collection->getColumns(false);
447
448
                return $this->collection->map(function (array $row) use ($columns) {
449
                    return $this->getObjectResult($columns, $row);
450
                });
451 1
452
            default:
453
                throw new Exception\UnsupportedException('Unsupported fetch style');
454
        }
455
    }
456
457
    /**
458 1
     * {@inheritDoc}
459
     */
460 1
    public function fetchObject($class_name = null, $ctor_args = null)
461
    {
462
        throw new Exception\UnsupportedException;
463
    }
464
465
    /**
466 1
     * {@inheritDoc}
467
     */
468 1
    public function errorCode()
469
    {
470
        return $this->errorCode;
471
    }
472
473
    /**
474 2
     * {@inheritDoc}
475
     */
476 2
    public function errorInfo()
477 2
    {
478
        if ($this->errorCode === null) {
479
            return null;
480 2
        }
481
482 1
        switch ($this->errorCode) {
483 1
            case CrateConst::ERR_INVALID_SQL:
484
                $ansiErrorCode = 42000;
485
                break;
486 1
487 1
            default:
488
                $ansiErrorCode = 'Not available';
489
                break;
490
        }
491 2
492 2
        return [
493 2
            $ansiErrorCode,
494
            $this->errorCode,
495
            $this->errorMessage,
496
        ];
497
    }
498
499
    /**
500 1
     * {@inheritDoc}
501
     */
502 1
    public function setAttribute($attribute, $value)
503
    {
504
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
505
    }
506
507
    /**
508 1
     * {@inheritDoc}
509
     */
510 1
    public function getAttribute($attribute)
511
    {
512
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
513
    }
514
515
    /**
516 2
     * {@inheritDoc}
517
     */
518 2
    public function columnCount()
519 2
    {
520
        if (!$this->hasExecuted()) {
521
            $this->execute();
522 2
        }
523
524
        return count($this->collection->getColumns(false));
525
    }
526
527
    /**
528 1
     * {@inheritDoc}
529
     */
530 1
    public function getColumnMeta($column)
531
    {
532
        throw new Exception\UnsupportedException;
533
    }
534
535
    /**
536 14
     * {@inheritDoc}
537
     */
538 14
    public function doSetFetchMode($mode, $params = null)
539 14
    {
540
        $args     = func_get_args();
541 14
        $argCount = count($args);
542
543 3
        switch ($mode) {
544 1
            case PDO::FETCH_COLUMN:
545
                if ($argCount != 2) {
546
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
547 2
                }
548 1
549
                if (!is_int($params)) {
550
                    throw new Exception\InvalidArgumentException('colno must be an integer');
551 1
                }
552 1
553 1
                $this->options['fetchMode']   = $mode;
554
                $this->options['fetchColumn'] = $params;
555
                break;
556
557
            case PDO::FETCH_ASSOC:
558
            case PDO::FETCH_NUM:
559
            case PDO::FETCH_BOTH:
560
            case PDO::FETCH_BOUND:
561 10
            case PDO::FETCH_NAMED:
562 5
            case PDO::FETCH_OBJ:
563
                if ($params !== null) {
564
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
565 5
                }
566 5
567
                $this->options['fetchMode'] = $mode;
568
                break;
569 1
570
            default:
571
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
572 6
        }
573
574
        return true;
575
    }
576
577
    /**
578 2
     * {@inheritDoc}
579
     */
580 2
    public function nextRowset()
581 2
    {
582
        if (!$this->hasExecuted()) {
583
            $this->execute();
584 2
        }
585 1
586
        if (!$this->isSuccessful()) {
587
            return false;
588 1
        }
589
590 1
        $this->collection->next();
591
592
        return $this->collection->valid();
593
    }
594
595
    /**
596 1
     * {@inheritDoc}
597
     */
598 1
    public function closeCursor()
599 1
    {
600
        $this->errorCode  = 0;
601 1
        $this->collection = null;
602
603
        return true;
604
    }
605
606
    /**
607 1
     * {@inheritDoc}
608
     */
609 1
    public function debugDumpParams()
610
    {
611
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
612
    }
613
614
    /**
615 1
     * {@Inheritdoc}
616
     */
617 1
    public function getIterator(): \Iterator
618 1
    {
619
        $results = $this->fetchAll();
620
        if ($results === false) {
621 1
            throw new Exception\RuntimeException('Failure when fetching data');
622
        }
623
        return new ArrayIterator($results);
624 16
    }
625
626 16
    private function typedValue($value, $data_type)
627
    {
628 3
        if (null === $value) {
629
            // Do not typecast null values
630
            return null;
631 13
        }
632
633
        switch ($data_type) {
634 1
            case PDO::PARAM_FLOAT:
635
            case PDO::PARAM_DOUBLE:
636
                return (float)$value;
637
638 3
            case PDO::PARAM_INT:
639
            case PDO::PARAM_LONG:
640
                return (int)$value;
641 2
642
            case PDO::PARAM_NULL:
643
                return null;
644 7
645
            case PDO::PARAM_BOOL:
646
                return filter_var($value, FILTER_VALIDATE_BOOLEAN);
647
648 4
            case PDO::PARAM_STR:
649
            case PDO::PARAM_IP:
650
                return (string)$value;
651
652 1
            case PDO::PARAM_OBJECT:
653
            case PDO::PARAM_ARRAY:
654
                return (array)$value;
655 1
656 1
            case PDO::PARAM_TIMESTAMP:
657
                if (is_numeric($value)) {
658
                    return (int)$value;
659 1
                }
660
661
                return (string)$value;
662 1
663
            default:
664
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
665
        }
666
    }
667
668
    /**
669
     * Generate object from array
670
     *
671
     * @param array $columns
672
     * @param array $row
673
     */
674
    private function getObjectResult(array $columns, array $row)
675
    {
676
        $obj = new \stdClass();
677
        foreach ($columns as $key => $column) {
678
            $obj->{$column} = $row[$key];
679
        }
680
681
        return $obj;
682
    }
683
}
684