Passed
Branch main (45b422)
by Andreas
01:40
created

PDOStatement::fetch()   C

Complexity

Conditions 12
Paths 18

Size

Total Lines 43
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 12.0247

Importance

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

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

347
        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...
348
    }
349
350
    /**
351
     * {@inheritDoc}
352
     */
353 5
    public function fetchColumn($column_number = 0)
354
    {
355 5
        if (!is_int($column_number)) {
356 1
            throw new Exception\InvalidArgumentException('column_number must be a valid integer');
357
        }
358
359 4
        if (!$this->hasExecuted()) {
360 4
            $this->execute();
361
        }
362
363 4
        if (!$this->isSuccessful()) {
364 1
            return false;
365
        }
366
367 3
        if (!$this->collection->valid()) {
368 2
            return false;
369
        }
370
371 2
        $row = $this->collection->current();
372 2
        $this->collection->next();
373
374 2
        if ($column_number >= count($row)) {
375 1
            throw new Exception\OutOfBoundsException(
376 1
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
377
            );
378
        }
379
380 1
        return $row[$column_number];
381
    }
382
383
    /**
384
     * {@inheritDoc}
385
     */
386 13
    public function fetchAll($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

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