Completed
Push — master ( fed27b...19b67e )
by
unknown
07:02
created

PDOStatement::fetch()   C

Complexity

Conditions 11
Paths 32

Size

Total Lines 45
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 11

Importance

Changes 0
Metric Value
dl 0
loc 45
ccs 24
cts 24
cp 1
rs 5.2653
c 0
b 0
f 0
cc 11
eloc 25
nc 32
nop 3
crap 11

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

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
359 2
        }
360
361
        if (!$this->collection->valid()) {
362 2
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type of the parent method PDOStatement::fetchColumn of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
363 2
        }
364
365 2
        $row = $this->collection->current();
366 1
        $this->collection->next();
367 1
368 1
        if ($column_number >= count($row)) {
369
            throw new Exception\OutOfBoundsException(
370
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
371 1
            );
372
        }
373
374
        return $row[$column_number];
375
    }
376
377 13
    /**
378
     * {@inheritDoc}
379 13
     */
380 13
    public function fetchAll($fetch_style = null, $fetch_argument = null, $ctor_args = [])
381 13
    {
382
        if (!$this->hasExecuted()) {
383 13
            $this->execute();
384 1
        }
385
386
        if (!$this->isSuccessful()) {
387 12
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type of the parent method PDOStatement::fetchAll of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
388
        }
389
390
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
391 12
392 1
        switch ($fetch_style)
393
        {
394 11
            case PDO::FETCH_NUM:
395 11
                return $this->collection->getRows();
396 2
397
            case PDO::FETCH_NAMED:
398 2
            case PDO::FETCH_ASSOC:
399 2
                $columns = $this->collection->getColumns(false);
400
                return $this->collection->map(function (array $row) use ($columns) {
401 9
                    return array_combine($columns, $row);
402 3
                });
403
404
            case PDO::FETCH_BOTH:
405 3
                $columns = $this->collection->getColumns(false);
406 3
407
                return $this->collection->map(function (array $row) use ($columns) {
408 6
                    return array_merge($row, array_combine($columns, $row));
409 2
                });
410 1
411
            case PDO::FETCH_FUNC:
412
                if (!is_callable($fetch_argument)) {
413
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
414 1
                }
415 1
416
                return $this->collection->map(function (array $row) use ($fetch_argument) {
417 4
                    return call_user_func_array($fetch_argument, $row);
418 3
                });
419
420 3
            case PDO::FETCH_COLUMN:
421 1
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
422
423
                if (!is_int($columnIndex)) {
424 2
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
425 2
                }
426 1
427 1
                $columns = $this->collection->getColumns(false);
428 1
                if (!isset($columns[$columnIndex])) {
429
                    throw new Exception\OutOfBoundsException(
430
                        sprintf('Column with the index %d does not exist.', $columnIndex)
431 1
                    );
432 1
                }
433 1
434
                return $this->collection->map(function (array $row) use ($columnIndex) {
435 1
                    return $row[$columnIndex];
436 1
                });
437 1
            case PDO::FETCH_OBJ:
438
                $columns = $this->collection->getColumns(false);
439
440
                return $this->collection->map(function (array $row) use ($columns) {
441
                    return $this->getObjectResult($columns, $row);
442
                });
443 1
444
            default:
445 1
                throw new Exception\UnsupportedException('Unsupported fetch style');
446
        }
447
    }
448
449
    /**
450
     * {@inheritDoc}
451 1
     */
452
    public function fetchObject($class_name = null, $ctor_args = null)
453 1
    {
454
        throw new Exception\UnsupportedException;
455
    }
456
457
    /**
458
     * {@inheritDoc}
459 2
     */
460
    public function errorCode()
461 2
    {
462 2
        return $this->errorCode;
463
    }
464
465 2
    /**
466
     * {@inheritDoc}
467 2
     */
468 1
    public function errorInfo()
469 1
    {
470
        if ($this->errorCode === null) {
471 1
            return null;
472 1
        }
473 1
474 2
        switch ($this->errorCode)
475
        {
476
            case CrateConst::ERR_INVALID_SQL:
477 2
                $ansiErrorCode = 42000;
478 2
                break;
479 2
480 2
            default:
481
                $ansiErrorCode = 'Not available';
482
                break;
483
        }
484
485
        return [
486 1
            $ansiErrorCode,
487
            $this->errorCode,
488 1
            $this->errorMessage
489
        ];
490
    }
491
492
    /**
493
     * {@inheritDoc}
494 1
     */
495
    public function setAttribute($attribute, $value)
496 1
    {
497
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
498
    }
499
500
    /**
501
     * {@inheritDoc}
502 2
     */
503
    public function getAttribute($attribute)
504 2
    {
505 2
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
506 2
    }
507
508 2
    /**
509
     * {@inheritDoc}
510
     */
511
    public function columnCount()
512
    {
513
        if (!$this->hasExecuted()) {
514 1
            $this->execute();
515
        }
516 1
517
        return count($this->collection->getColumns(false));
518
    }
519
520
    /**
521
     * {@inheritDoc}
522 14
     */
523
    public function getColumnMeta($column)
524 14
    {
525 14
        throw new Exception\UnsupportedException;
526
    }
527
528
    /**
529 14
     * {@inheritDoc}
530 3
     */
531 1
    public function setFetchMode($mode, $params = null)
532
    {
533
        $args     = func_get_args();
534 2
        $argCount = count($args);
535 1
536
        switch ($mode)
537
        {
538 1
            case PDO::FETCH_COLUMN:
539 1
                if ($argCount != 2) {
540 1
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
541
                }
542 11
543 11
                if (!is_int($params)) {
544 11
                    throw new Exception\InvalidArgumentException('colno must be an integer');
545 11
                }
546 11
547 10
                $this->options['fetchMode']   = $mode;
548 5
                $this->options['fetchColumn'] = $params;
549
                break;
550
551 5
            case PDO::FETCH_ASSOC:
552 5
            case PDO::FETCH_NUM:
553
            case PDO::FETCH_BOTH:
554 1
            case PDO::FETCH_BOUND:
555 1
            case PDO::FETCH_NAMED:
556 1
            case PDO::FETCH_OBJ:
557 6
                if ($params !== null) {
558
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
559
                }
560
561
                $this->options['fetchMode'] = $mode;
562 2
                break;
563
564 2
            default:
565 2
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
566 2
        }
567
    }
568 2
569 1
    /**
570
     * {@inheritDoc}
571
     */
572 1
    public function nextRowset()
573 1
    {
574
        if (!$this->hasExecuted()) {
575
            $this->execute();
576
        }
577
578
        if (!$this->isSuccessful()) {
579 1
            return false;
580
        }
581 1
582 1
        $this->collection->next();
583 1
        return $this->collection->valid();
584
    }
585
586
    /**
587
     * {@inheritDoc}
588
     */
589 1
    public function closeCursor()
590
    {
591 1
        $this->errorCode = 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like 0 of type integer is incompatible with the declared type string|null of property $errorCode.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
592
        $this->collection = null;
593
        return true;
594
    }
595
596
    /**
597 1
     * {@inheritDoc}
598
     */
599 1
    public function debugDumpParams()
600
    {
601
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
602 8
    }
603
604
    /**
605
     * {@Inheritdoc}
606 8
     */
607 8
    public function getIterator()
608 1
    {
609
        return new ArrayIterator($this->fetchAll());
610 8
    }
611 8
612 3
    private function typedValue($value, $data_type)
613
    {
614 7
        switch ($data_type)
615 2
        {
616
            case PDO::PARAM_FLOAT:
617 6
            case PDO::PARAM_DOUBLE:
618 2
                return (float) $value;
619
620 5
            case PDO::PARAM_INT:
621 5
            case PDO::PARAM_LONG:
622 4
                return (int) $value;
623
624 2
            case PDO::PARAM_NULL:
625 2
                return null;
626 1
627
            case PDO::PARAM_BOOL:
628 2
                return (bool) $value;
629 1
630 1
            case PDO::PARAM_STR:
631
            case PDO::PARAM_IP:
632 1
                return (string) $value;
633
634 1
            case PDO::PARAM_OBJECT:
635 1
            case PDO::PARAM_ARRAY:
636 1
                return (array) $value;
637
638
            case PDO::PARAM_TIMESTAMP:
639
                if (is_numeric($value)) {
640
                    return (int) $value;
641
                }
642
                return (string) $value;
643
644
            default:
645
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
646
        }
647
    }
648
649
    /**
650
     * Generate object from array
651
     * @param array        $columns
652
     * @param array        $row
653
     */
654
    private function getObjectResult(array $columns, array $row)
655
    {
656
        $obj = new \stdClass();
657
        foreach ($columns as $key => $column) {
658
            $obj->{$column} = $row[$key];
659
        }
660
661
        return $obj;
662
    }
663
}
664