Passed
Push — main ( 3d19d1...5de7c3 )
by Andreas
02:04
created

PDOStatement   F

Complexity

Total Complexity 109

Size/Duplication

Total Lines 647
Duplicated Lines 0 %

Test Coverage

Coverage 90.83%

Importance

Changes 6
Bugs 0 Features 1
Metric Value
eloc 249
c 6
b 0
f 1
dl 0
loc 647
ccs 208
cts 229
cp 0.9083
rs 2
wmc 109

28 Methods

Rating   Name   Duplication   Size   Complexity  
A updateBoundColumns() 0 18 4
A bindValue() 0 4 1
A __construct() 0 6 1
C fetch() 0 43 12
C typedValue() 0 39 14
A bindColumn() 0 9 2
B doSetFetchMode() 0 37 11
A closeCursor() 0 6 1
A debugDumpParams() 0 3 1
A getColumnMeta() 0 3 1
A getFetchStyle() 0 3 2
A getAttribute() 0 3 1
A nextRowset() 0 13 3
A setAttribute() 0 3 1
A errorCode() 0 3 1
A rowCount() 0 11 3
A hasExecuted() 0 3 2
A fetchObject() 0 3 1
B bindParam() 0 23 7
A getObjectResult() 0 8 2
A getIterator() 0 7 2
C doFetchAll() 0 66 15
A isSuccessful() 0 9 2
A execute() 0 26 5
A columnCount() 0 7 2
A replaceNamedParametersWithPositionals() 0 20 3
A fetchColumn() 0 28 6
A errorInfo() 0 20 3

How to fix   Complexity   

Complex Class

Complex classes like PDOStatement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PDOStatement, and based on these observations, apply Extract Interface, too.

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
     * @param array        $options
98
     */
99 4
    public function __construct(PDOInterface $pdo, Closure $request, $sql, array $options)
100
    {
101 4
        $this->sql     = $this->replaceNamedParametersWithPositionals($sql);
102 4
        $this->pdo     = $pdo;
103 4
        $this->options = array_merge($this->options, $options);
104 4
        $this->request = $request;
105
    }
106
107 158
    private function replaceNamedParametersWithPositionals($sql)
108
    {
109 158
        if (strpos($sql, ':') === false) {
110 158
            return $sql;
111
        }
112 4
        $pattern = '/:((?:[\w|\d|_](?=([^\'\\\]*(\\\.|\'([^\'\\\]*\\\.)*[^\'\\\]*\'))*[^\']*$))*)/';
113
114 4
        $idx      = 1;
115 4
        $callback = function ($matches) use (&$idx) {
116 4
            $value = $matches[1];
117 4
            if (empty($value)) {
118 2
                return $matches[0];
119
            }
120 4
            $this->namedToPositionalMap[$idx] = $value;
121 4
            $idx++;
122
123 4
            return '?';
124 4
        };
125
126 4
        return preg_replace_callback($pattern, $callback, $sql);
127
    }
128
129
    /**
130
     * Determines if the statement has been executed
131
     *
132
     * @internal
133
     *
134
     * @return bool
135
     */
136 66
    private function hasExecuted()
137
    {
138 66
        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
     * @return bool
147
     */
148 62
    private function isSuccessful()
149
    {
150 62
        if (!$this->hasExecuted()) {
151
            // @codeCoverageIgnoreStart
152
            throw new Exception\LogicException('The statement has not been executed yet');
153
            // @codeCoverageIgnoreEnd
154
        }
155
156 62
        return $this->collection !== null;
157
    }
158
159
    /**
160
     * Get the fetch style to be used
161
     *
162
     * @internal
163
     *
164
     * @return int
165
     */
166 6
    private function getFetchStyle()
167
    {
168 6
        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
     * @return void
179
     */
180 2
    private function updateBoundColumns(array $row)
181
    {
182
183 2
        if (!$this->isSuccessful()) {
184
            return;
185
        }
186
187 2
        foreach ($this->columnBinding as $column => &$metadata) {
188 2
            $index = $this->collection->getColumnIndex($column);
189 2
            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
195
            // Update by reference
196 2
            $value           = $this->typedValue($row[$index], $metadata['type']);
197 2
            $metadata['ref'] = $value;
198
        }
199
    }
200
201
    /**
202
     * {@inheritDoc}
203
     */
204 6
    public function execute($input_parameters = null)
205
    {
206 6
        $input_parameters_array = ArrayUtils::toArray($input_parameters);
207 6
        $zero_based             = array_key_exists(0, $input_parameters_array);
208 6
        foreach ($input_parameters_array as $parameter => $value) {
209 2
            if (is_int($parameter) && $zero_based) {
210 2
                $parameter++;
211
            }
212 2
            $this->bindValue($parameter, $value);
213
        }
214
215
        // parameter binding might be unordered, so sort it before execute
216 6
        ksort($this->parameters);
217
218 6
        $result = $this->request->__invoke($this, $this->sql, array_values($this->parameters));
219
220 6
        if (is_array($result)) {
221 2
            $this->errorCode    = $result['code'];
222 2
            $this->errorMessage = $result['message'];
223
224 2
            return false;
225
        }
226
227 4
        $this->collection = $result;
228
229 4
        return true;
230
    }
231
232
    /**
233
     * {@inheritDoc}
234
     */
235 18
    public function fetch($fetch_style = null, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = 0)
236
    {
237 18
        if (!$this->hasExecuted()) {
238 18
            $this->execute();
239
        }
240
241 18
        if (!$this->isSuccessful()) {
242 2
            return false;
243
        }
244
245 16
        if ($this->collection === null || !$this->collection->valid()) {
246 2
            return false;
247
        }
248
249
        // Get the current row
250 14
        $row = $this->collection->current();
251
252
        // Traverse
253 14
        $this->collection->next();
254
255 14
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
256
257
        switch ($fetch_style) {
258
            case PDO::FETCH_NAMED:
259
            case PDO::FETCH_ASSOC:
260 4
                return array_combine($this->collection->getColumns(false), $row);
261
262
            case PDO::FETCH_BOTH:
263 4
                return array_merge($row, array_combine($this->collection->getColumns(false), $row));
264
265
            case PDO::FETCH_BOUND:
266 2
                $this->updateBoundColumns($row);
267
268 2
                return true;
269
270
            case PDO::FETCH_NUM:
271 2
                return $row;
272
273
            case PDO::FETCH_OBJ:
274
                return $this->getObjectResult($this->collection->getColumns(false), $row);
275
276
            default:
277 2
                throw new Exception\UnsupportedException('Unsupported fetch style');
278
        }
279
    }
280
281
    /**
282
     * {@inheritDoc}
283
     */
284 4
    public function bindParam(
285
        $parameter,
286
        &$variable,
287
        $data_type = PDO::PARAM_STR,
288
        $length = null,
289
        $driver_options = null
290
    ) {
291 4
        if (is_numeric($parameter)) {
292 4
            if ($parameter == 0) {
293 2
                throw new Exception\UnsupportedException("0-based parameter binding not supported, use 1-based");
294
            }
295 2
            $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
            }
309
        }
310
    }
311
312
    /**
313
     * {@inheritDoc}
314
     */
315 2
    public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
316
    {
317 2
        $type = $type ?: PDO::PARAM_STR;
318
319 2
        $this->columnBinding[$column] = [
320 2
            'ref'        => &$param,
321 2
            'type'       => $type,
322 2
            'maxlen'     => $maxlen,
323 2
            'driverdata' => $driverdata,
324 2
        ];
325
    }
326
327
    /**
328
     * {@inheritDoc}
329
     */
330 24
    public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR)
331
    {
332 24
        $value = $this->typedValue($value, $data_type);
333 24
        $this->bindParam($parameter, $value, $data_type);
334
    }
335
336
    /**
337
     * {@inheritDoc}
338
     */
339 4
    public function rowCount()
340
    {
341 4
        if (!$this->hasExecuted()) {
342 4
            $this->execute();
343
        }
344
345 4
        if (!$this->isSuccessful()) {
346 2
            return 0;
347
        }
348
349 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

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

388
    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...
389
    {
390 26
        if (!$this->hasExecuted()) {
391 26
            $this->execute();
392
        }
393
394 26
        if (!$this->isSuccessful()) {
395 2
            return false;
396
        }
397
398 24
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
399
400
        switch ($fetch_style) {
401
            case PDO::FETCH_NUM:
402 2
                return $this->collection->getRows();
403
404
            case PDO::FETCH_NAMED:
405
            case PDO::FETCH_ASSOC:
406 4
                $columns = $this->collection->getColumns(false);
407
408 4
                return $this->collection->map(function (array $row) use ($columns) {
409 4
                    return array_combine($columns, $row);
410 4
                });
411
412
            case PDO::FETCH_BOTH:
413 6
                $columns = $this->collection->getColumns(false);
414
415 6
                return $this->collection->map(function (array $row) use ($columns) {
416 6
                    return array_merge($row, array_combine($columns, $row));
417 6
                });
418
419
            case PDO::FETCH_FUNC:
420 4
                if (!is_callable($fetch_argument)) {
421 2
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
422
                }
423
424 2
                return $this->collection->map(function (array $row) use ($fetch_argument) {
425 2
                    return call_user_func_array($fetch_argument, $row);
426 2
                });
427
428
            case PDO::FETCH_COLUMN:
429 6
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
430
431 6
                if (!is_int($columnIndex)) {
432 2
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
433
                }
434
435 4
                $columns = $this->collection->getColumns(false);
436 4
                if (!isset($columns[$columnIndex])) {
437 2
                    throw new Exception\OutOfBoundsException(
438 2
                        sprintf('Column with the index %d does not exist.', $columnIndex)
439 2
                    );
440
                }
441
442 2
                return $this->collection->map(function (array $row) use ($columnIndex) {
443 2
                    return $row[$columnIndex];
444 2
                });
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
452
            default:
453 2
                throw new Exception\UnsupportedException('Unsupported fetch style');
454
        }
455
    }
456
457
    /**
458
     * {@inheritDoc}
459
     */
460 2
    public function fetchObject($class_name = null, $ctor_args = null)
461
    {
462 2
        throw new Exception\UnsupportedException;
463
    }
464
465
    /**
466
     * {@inheritDoc}
467
     */
468 2
    public function errorCode()
469
    {
470 2
        return $this->errorCode;
471
    }
472
473
    /**
474
     * {@inheritDoc}
475
     */
476 4
    public function errorInfo()
477
    {
478 4
        if ($this->errorCode === null) {
479 4
            return null;
480
        }
481
482 4
        switch ($this->errorCode) {
483 4
            case CrateConst::ERR_INVALID_SQL:
484 2
                $ansiErrorCode = 42000;
485 2
                break;
486
487
            default:
488 2
                $ansiErrorCode = 'Not available';
489 2
                break;
490
        }
491
492 4
        return [
493 4
            $ansiErrorCode,
494 4
            $this->errorCode,
495 4
            $this->errorMessage,
496 4
        ];
497
    }
498
499
    /**
500
     * {@inheritDoc}
501
     */
502 2
    public function setAttribute($attribute, $value)
503
    {
504 2
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
505
    }
506
507
    /**
508
     * {@inheritDoc}
509
     */
510 2
    public function getAttribute($attribute)
511
    {
512 2
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
513
    }
514
515
    /**
516
     * {@inheritDoc}
517
     */
518 4
    public function columnCount()
519
    {
520 4
        if (!$this->hasExecuted()) {
521 4
            $this->execute();
522
        }
523
524 4
        return count($this->collection->getColumns(false));
525
    }
526
527
    /**
528
     * {@inheritDoc}
529
     */
530 2
    public function getColumnMeta($column)
531
    {
532 2
        throw new Exception\UnsupportedException;
533
    }
534
535
    /**
536
     * {@inheritDoc}
537
     */
538 28
    public function doSetFetchMode($mode, $params = null)
539
    {
540 28
        $args     = func_get_args();
541 28
        $argCount = count($args);
542
543
        switch ($mode) {
544
            case PDO::FETCH_COLUMN:
545 6
                if ($argCount != 2) {
546 2
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
547
                }
548
549 4
                if (!is_int($params)) {
550 2
                    throw new Exception\InvalidArgumentException('colno must be an integer');
551
                }
552
553 2
                $this->options['fetchMode']   = $mode;
554 2
                $this->options['fetchColumn'] = $params;
555 2
                break;
556
557
            case PDO::FETCH_ASSOC:
558
            case PDO::FETCH_NUM:
559
            case PDO::FETCH_BOTH:
560
            case PDO::FETCH_BOUND:
561
            case PDO::FETCH_NAMED:
562
            case PDO::FETCH_OBJ:
563 20
                if ($params !== null) {
564 10
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
565
                }
566
567 10
                $this->options['fetchMode'] = $mode;
568 10
                break;
569
570
            default:
571 2
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
572
        }
573
574 12
        return true;
575
    }
576
577
    /**
578
     * {@inheritDoc}
579
     */
580 4
    public function nextRowset()
581
    {
582 4
        if (!$this->hasExecuted()) {
583 4
            $this->execute();
584
        }
585
586 4
        if (!$this->isSuccessful()) {
587 2
            return false;
588
        }
589
590 2
        $this->collection->next();
591
592 2
        return $this->collection->valid();
593
    }
594
595
    /**
596
     * {@inheritDoc}
597
     */
598 2
    public function closeCursor()
599
    {
600 2
        $this->errorCode  = 0;
601 2
        $this->collection = null;
602
603 2
        return true;
604
    }
605
606
    /**
607
     * {@inheritDoc}
608
     */
609 2
    public function debugDumpParams()
610
    {
611 2
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
612
    }
613
614
    /**
615
     * {@Inheritdoc}
616
     */
617 2
    public function getIterator(): \Iterator
618
    {
619 2
        $results = $this->fetchAll();
0 ignored issues
show
Deprecated Code introduced by
The function Crate\PDO\PDOStatement::fetchAll() has been deprecated: Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead. ( Ignorable by Annotation )

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

619
        $results = /** @scrutinizer ignore-deprecated */ $this->fetchAll();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
620 2
        if ($results === false) {
0 ignored issues
show
introduced by
The condition $results === false is always false.
Loading history...
621
            throw new Exception\RuntimeException('Failure when fetching data');
622
        }
623 2
        return new ArrayIterator($results);
624
    }
625
626 32
    private function typedValue($value, $data_type)
627
    {
628 32
        if (null === $value) {
629
            // Do not typecast null values
630 6
            return null;
631
        }
632
633
        switch ($data_type) {
634
            case PDO::PARAM_FLOAT:
635
            case PDO::PARAM_DOUBLE:
636 2
                return (float)$value;
637
638
            case PDO::PARAM_INT:
639
            case PDO::PARAM_LONG:
640 6
                return (int)$value;
641
642
            case PDO::PARAM_NULL:
643 4
                return null;
644
645
            case PDO::PARAM_BOOL:
646 14
                return filter_var($value, FILTER_VALIDATE_BOOLEAN);
647
648
            case PDO::PARAM_STR:
649
            case PDO::PARAM_IP:
650 8
                return (string)$value;
651
652
            case PDO::PARAM_OBJECT:
653
            case PDO::PARAM_ARRAY:
654 2
                return (array)$value;
655
656
            case PDO::PARAM_TIMESTAMP:
657 2
                if (is_numeric($value)) {
658 2
                    return (int)$value;
659
                }
660
661 2
                return (string)$value;
662
663
            default:
664 2
                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