Passed
Pull Request — main (#114)
by Andreas
06:49
created

PDOStatement::fetchAll()   C

Complexity

Conditions 15
Paths 24

Size

Total Lines 66
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
eloc 38
c 4
b 0
f 1
dl 0
loc 66
rs 5.9166
cc 15
nc 24
nop 3

How to fix   Long Method    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
    public function __construct(PDOInterface $pdo, Closure $request, $sql, array $options)
98
    {
99
        $this->sql     = $this->replaceNamedParametersWithPositionals($sql);
100
        $this->pdo     = $pdo;
101
        $this->options = array_merge($this->options, $options);
102
        $this->request = $request;
103
    }
104
105
    private function replaceNamedParametersWithPositionals($sql)
106
    {
107
        if (strpos($sql, ':') === false) {
108
            return $sql;
109
        }
110
        $pattern = '/:((?:[\w|\d|_](?=([^\'\\\]*(\\\.|\'([^\'\\\]*\\\.)*[^\'\\\]*\'))*[^\']*$))*)/';
111
112
        $idx      = 1;
113
        $callback = function ($matches) use (&$idx) {
114
            $value = $matches[1];
115
            if (empty($value)) {
116
                return $matches[0];
117
            }
118
            $this->namedToPositionalMap[$idx] = $value;
119
            $idx++;
120
121
            return '?';
122
        };
123
124
        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
    private function hasExecuted()
135
    {
136
        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
    private function isSuccessful()
147
    {
148
        if (!$this->hasExecuted()) {
149
            // @codeCoverageIgnoreStart
150
            throw new Exception\LogicException('The statement has not been executed yet');
151
            // @codeCoverageIgnoreEnd
152
        }
153
154
        return $this->collection !== null;
155
    }
156
157
    /**
158
     * Get the fetch style to be used
159
     *
160
     * @internal
161
     *
162
     * @return int
163
     */
164
    private function getFetchStyle()
165
    {
166
        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
    private function updateBoundColumns(array $row)
179
    {
180
        foreach ($this->columnBinding as $column => &$metadata) {
181
            $index = $this->collection->getColumnIndex($column);
0 ignored issues
show
Bug introduced by
The method getColumnIndex() 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

181
            /** @scrutinizer ignore-call */ 
182
            $index = $this->collection->getColumnIndex($column);

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...
182
            if ($index === null) {
183
                // todo: I would like to throw an exception and tell someone they screwed up
184
                // but i think that would violate the PDO api
185
                continue;
186
            }
187
188
            // Update by reference
189
            $value           = $this->typedValue($row[$index], $metadata['type']);
190
            $metadata['ref'] = $value;
191
        }
192
    }
193
194
    /**
195
     * {@inheritDoc}
196
     */
197
    public function execute($input_parameters = null)
198
    {
199
        $input_parameters_array = ArrayUtils::toArray($input_parameters);
200
        $zero_based             = array_key_exists(0, $input_parameters_array);
201
        foreach ($input_parameters_array as $parameter => $value) {
202
            if (is_int($parameter) && $zero_based) {
203
                $parameter++;
204
            }
205
            $this->bindValue($parameter, $value);
206
        }
207
208
        // parameter binding might be unordered, so sort it before execute
209
        ksort($this->parameters);
210
211
        $result = $this->request->__invoke($this, $this->sql, array_values($this->parameters));
212
213
        if (is_array($result)) {
214
            $this->errorCode    = $result['code'];
215
            $this->errorMessage = $result['message'];
216
217
            return false;
218
        }
219
220
        $this->collection = $result;
221
222
        return true;
223
    }
224
225
    /**
226
     * {@inheritDoc}
227
     */
228
    public function fetch($fetch_style = null, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = 0)
229
    {
230
        if (!$this->hasExecuted()) {
231
            $this->execute();
232
        }
233
234
        if (!$this->isSuccessful()) {
235
            return false;
236
        }
237
238
        if (!$this->collection->valid()) {
239
            return false;
240
        }
241
242
        // Get the current row
243
        $row = $this->collection->current();
244
245
        // Traverse
246
        $this->collection->next();
247
248
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
249
250
        switch ($fetch_style) {
251
            case PDO::FETCH_NAMED:
252
            case PDO::FETCH_ASSOC:
253
                return array_combine($this->collection->getColumns(false), $row);
254
255
            case PDO::FETCH_BOTH:
256
                return array_merge($row, array_combine($this->collection->getColumns(false), $row));
257
258
            case PDO::FETCH_BOUND:
259
                $this->updateBoundColumns($row);
260
261
                return true;
262
263
            case PDO::FETCH_NUM:
264
                return $row;
265
266
            case PDO::FETCH_OBJ:
267
                return $this->getObjectResult($this->collection->getColumns(false), $row);
268
269
            default:
270
                throw new Exception\UnsupportedException('Unsupported fetch style');
271
        }
272
    }
273
274
    /**
275
     * {@inheritDoc}
276
     */
277
    public function bindParam(
278
        $parameter,
279
        &$variable,
280
        $data_type = PDO::PARAM_STR,
281
        $length = null,
282
        $driver_options = null
283
    ) {
284
        if (is_numeric($parameter)) {
285
            if ($parameter == 0) {
286
                throw new Exception\UnsupportedException("0-based parameter binding not supported, use 1-based");
287
            }
288
            $this->parameters[$parameter - 1] = &$variable;
289
        } else {
290
            $namedParameterKey = substr($parameter, 0, 1) === ':' ? substr($parameter, 1) : $parameter;
291
            if (in_array($namedParameterKey, $this->namedToPositionalMap, true)) {
292
                foreach ($this->namedToPositionalMap as $key => $value) {
293
                    if ($value == $namedParameterKey) {
294
                        $this->parameters[$key] = &$variable;
295
                    }
296
                }
297
            } else {
298
                throw new Exception\OutOfBoundsException(
299
                    sprintf('The named parameter "%s" does not exist', $parameter)
300
                );
301
            }
302
        }
303
    }
304
305
    /**
306
     * {@inheritDoc}
307
     */
308
    public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
309
    {
310
        $type = $type ?: PDO::PARAM_STR;
311
312
        $this->columnBinding[$column] = [
313
            'ref'        => &$param,
314
            'type'       => $type,
315
            'maxlen'     => $maxlen,
316
            'driverdata' => $driverdata,
317
        ];
318
    }
319
320
    /**
321
     * {@inheritDoc}
322
     */
323
    public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR)
324
    {
325
        $value = $this->typedValue($value, $data_type);
326
        $this->bindParam($parameter, $value, $data_type);
327
    }
328
329
    /**
330
     * {@inheritDoc}
331
     */
332
    public function rowCount()
333
    {
334
        if (!$this->hasExecuted()) {
335
            $this->execute();
336
        }
337
338
        if (!$this->isSuccessful()) {
339
            return 0;
340
        }
341
342
        return $this->collection->count();
343
    }
344
345
    /**
346
     * {@inheritDoc}
347
     */
348
    public function fetchColumn($column_number = 0)
349
    {
350
        if (!is_int($column_number)) {
351
            throw new Exception\InvalidArgumentException('column_number must be a valid integer');
352
        }
353
354
        if (!$this->hasExecuted()) {
355
            $this->execute();
356
        }
357
358
        if (!$this->isSuccessful()) {
359
            return false;
360
        }
361
362
        if (!$this->collection->valid()) {
363
            return false;
364
        }
365
366
        $row = $this->collection->current();
367
        $this->collection->next();
368
369
        if ($column_number >= count($row)) {
370
            throw new Exception\OutOfBoundsException(
371
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
372
            );
373
        }
374
375
        return $row[$column_number];
376
    }
377
378
    /**
379
     * {@inheritDoc}
380
     */
381
    public function fetchAll($fetch_style = null, $fetch_argument = null, $ctor_args = [])
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

381
    public function fetchAll($fetch_style = null, $fetch_argument = null, /** @scrutinizer ignore-unused */ $ctor_args = [])

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...
382
    {
383
        if (!$this->hasExecuted()) {
384
            $this->execute();
385
        }
386
387
        if (!$this->isSuccessful()) {
388
            return false;
389
        }
390
391
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
392
393
        switch ($fetch_style) {
394
            case PDO::FETCH_NUM:
395
                return $this->collection->getRows();
396
397
            case PDO::FETCH_NAMED:
398
            case PDO::FETCH_ASSOC:
399
                $columns = $this->collection->getColumns(false);
400
401
                return $this->collection->map(function (array $row) use ($columns) {
402
                    return array_combine($columns, $row);
403
                });
404
405
            case PDO::FETCH_BOTH:
406
                $columns = $this->collection->getColumns(false);
407
408
                return $this->collection->map(function (array $row) use ($columns) {
409
                    return array_merge($row, array_combine($columns, $row));
410
                });
411
412
            case PDO::FETCH_FUNC:
413
                if (!is_callable($fetch_argument)) {
414
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
415
                }
416
417
                return $this->collection->map(function (array $row) use ($fetch_argument) {
418
                    return call_user_func_array($fetch_argument, $row);
419
                });
420
421
            case PDO::FETCH_COLUMN:
422
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
423
424
                if (!is_int($columnIndex)) {
425
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
426
                }
427
428
                $columns = $this->collection->getColumns(false);
429
                if (!isset($columns[$columnIndex])) {
430
                    throw new Exception\OutOfBoundsException(
431
                        sprintf('Column with the index %d does not exist.', $columnIndex)
432
                    );
433
                }
434
435
                return $this->collection->map(function (array $row) use ($columnIndex) {
436
                    return $row[$columnIndex];
437
                });
438
            case PDO::FETCH_OBJ:
439
                $columns = $this->collection->getColumns(false);
440
441
                return $this->collection->map(function (array $row) use ($columns) {
442
                    return $this->getObjectResult($columns, $row);
443
                });
444
445
            default:
446
                throw new Exception\UnsupportedException('Unsupported fetch style');
447
        }
448
    }
449
450
    /**
451
     * {@inheritDoc}
452
     */
453
    public function fetchObject($class_name = null, $ctor_args = null)
454
    {
455
        throw new Exception\UnsupportedException;
456
    }
457
458
    /**
459
     * {@inheritDoc}
460
     */
461
    public function errorCode()
462
    {
463
        return $this->errorCode;
464
    }
465
466
    /**
467
     * {@inheritDoc}
468
     */
469
    public function errorInfo()
470
    {
471
        if ($this->errorCode === null) {
472
            return null;
473
        }
474
475
        switch ($this->errorCode) {
476
            case CrateConst::ERR_INVALID_SQL:
477
                $ansiErrorCode = 42000;
478
                break;
479
480
            default:
481
                $ansiErrorCode = 'Not available';
482
                break;
483
        }
484
485
        return [
486
            $ansiErrorCode,
487
            $this->errorCode,
488
            $this->errorMessage,
489
        ];
490
    }
491
492
    /**
493
     * {@inheritDoc}
494
     */
495
    public function setAttribute($attribute, $value)
496
    {
497
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
498
    }
499
500
    /**
501
     * {@inheritDoc}
502
     */
503
    public function getAttribute($attribute)
504
    {
505
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
506
    }
507
508
    /**
509
     * {@inheritDoc}
510
     */
511
    public function columnCount()
512
    {
513
        if (!$this->hasExecuted()) {
514
            $this->execute();
515
        }
516
517
        return count($this->collection->getColumns(false));
518
    }
519
520
    /**
521
     * {@inheritDoc}
522
     */
523
    public function getColumnMeta($column)
524
    {
525
        throw new Exception\UnsupportedException;
526
    }
527
528
    /**
529
     * {@inheritDoc}
530
     */
531
    public function setFetchMode($mode, $params = null)
532
    {
533
        $args     = func_get_args();
534
        $argCount = count($args);
535
536
        switch ($mode) {
537
            case PDO::FETCH_COLUMN:
538
                if ($argCount != 2) {
539
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
540
                }
541
542
                if (!is_int($params)) {
543
                    throw new Exception\InvalidArgumentException('colno must be an integer');
544
                }
545
546
                $this->options['fetchMode']   = $mode;
547
                $this->options['fetchColumn'] = $params;
548
                break;
0 ignored issues
show
Bug Best Practice introduced by
The expression ImplicitReturnNode returns the type null which is incompatible with the return type mandated by PDOStatement::setFetchMode() of boolean.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
549
550
            case PDO::FETCH_ASSOC:
551
            case PDO::FETCH_NUM:
552
            case PDO::FETCH_BOTH:
553
            case PDO::FETCH_BOUND:
554
            case PDO::FETCH_NAMED:
555
            case PDO::FETCH_OBJ:
556
                if ($params !== null) {
557
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
558
                }
559
560
                $this->options['fetchMode'] = $mode;
561
                break;
562
563
            default:
564
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
565
        }
566
    }
567
568
    /**
569
     * {@inheritDoc}
570
     */
571
    public function nextRowset()
572
    {
573
        if (!$this->hasExecuted()) {
574
            $this->execute();
575
        }
576
577
        if (!$this->isSuccessful()) {
578
            return false;
579
        }
580
581
        $this->collection->next();
582
583
        return $this->collection->valid();
584
    }
585
586
    /**
587
     * {@inheritDoc}
588
     */
589
    public function closeCursor()
590
    {
591
        $this->errorCode  = 0;
592
        $this->collection = null;
593
594
        return true;
595
    }
596
597
    /**
598
     * {@inheritDoc}
599
     */
600
    public function debugDumpParams()
601
    {
602
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
603
    }
604
605
    /**
606
     * {@Inheritdoc}
607
     */
608
    public function getIterator()
609
    {
610
        return new ArrayIterator($this->fetchAll());
0 ignored issues
show
Bug introduced by
It seems like $this->fetchAll() can also be of type false; however, parameter $array of ArrayIterator::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

610
        return new ArrayIterator(/** @scrutinizer ignore-type */ $this->fetchAll());
Loading history...
611
    }
612
613
    private function typedValue($value, $data_type)
614
    {
615
        if (null === $value) {
616
            // Do not typecast null values
617
            return null;
618
        }
619
620
        switch ($data_type) {
621
            case PDO::PARAM_FLOAT:
622
            case PDO::PARAM_DOUBLE:
623
                return (float)$value;
624
625
            case PDO::PARAM_INT:
626
            case PDO::PARAM_LONG:
627
                return (int)$value;
628
629
            case PDO::PARAM_NULL:
630
                return null;
631
632
            case PDO::PARAM_BOOL:
633
                return filter_var($value, FILTER_VALIDATE_BOOLEAN);
634
635
            case PDO::PARAM_STR:
636
            case PDO::PARAM_IP:
637
                return (string)$value;
638
639
            case PDO::PARAM_OBJECT:
640
            case PDO::PARAM_ARRAY:
641
                return (array)$value;
642
643
            case PDO::PARAM_TIMESTAMP:
644
                if (is_numeric($value)) {
645
                    return (int)$value;
646
                }
647
648
                return (string)$value;
649
650
            default:
651
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
652
        }
653
    }
654
655
    /**
656
     * Generate object from array
657
     *
658
     * @param array $columns
659
     * @param array $row
660
     */
661
    private function getObjectResult(array $columns, array $row)
662
    {
663
        $obj = new \stdClass();
664
        foreach ($columns as $key => $column) {
665
            $obj->{$column} = $row[$key];
666
        }
667
668
        return $obj;
669
    }
670
}
671