Passed
Pull Request — main (#116)
by Andreas
02:16
created

PDOStatement::doSetFetchMode()   B

Complexity

Conditions 11
Paths 16

Size

Total Lines 37
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 24
c 0
b 0
f 0
nc 16
nop 2
dl 0
loc 37
ccs 17
cts 17
cp 1
crap 11
rs 7.3166

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
     * @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 4
    }
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 2
        };
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 2
    }
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 14
        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 2
    }
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
        ];
325 2
    }
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 24
    }
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
            );
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)
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 24
        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
                    );
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
            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
        return [
493 4
            $ansiErrorCode,
494 4
            $this->errorCode,
495 4
            $this->errorMessage,
496
        ];
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 28
        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();
620 2
        if ($results === false) {
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 26
        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