Completed
Push — m/crate-pdo-0.4.0 ( 9f4a26 )
by
unknown
03:52
created

PDOStatement::typedValue()   C

Complexity

Conditions 13
Paths 13

Size

Total Lines 37
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 13

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 37
ccs 24
cts 24
cp 1
rs 5.1234
cc 13
eloc 24
nc 13
nop 2
crap 13

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 67
    private function replaceNamedParametersWithPositionals($sql)
105
    {
106 67
        if (strpos($sql, ':') === false) {
107 67
            return $sql;
108
        }
109 1
        $pattern = '/:((?:[\w|\d|_](?=([^\'\\\]*(\\\.|\'([^\'\\\]*\\\.)*[^\'\\\]*\'))*[^\']*$))*)/';
110
111 1
        $idx = 0;
112
        $callback = function ($matches) use (&$idx) {
113 1
            $value = $matches[1];
114 1
            if (empty($value)) {
115 1
                return $matches[0];
116
            }
117 1
            $this->namedToPositionalMap[$value] = $idx;
118 1
            $idx++;
119 1
            return '?';
120 1
        };
121
122 1
        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 30
    private function hasExecuted()
133
    {
134 30
        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 29
    private function isSuccessful()
145
    {
146 29
        if (!$this->hasExecuted()) {
147
            // @codeCoverageIgnoreStart
148
            throw new Exception\LogicException('The statement has not been executed yet');
149
            // @codeCoverageIgnoreEnd
150
        }
151
152 29
        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) {
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 = isset($input_parameters_array[0]);
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 2
        $result = $this->request->__invoke($this, $this->sql, $this->parameters);
209
210 2
        if (is_array($result)) {
211 1
            $this->errorCode    = $result['code'];
212 1
            $this->errorMessage = $result['message'];
213
214 1
            return false;
215
        }
216
217 1
        $this->collection = $result;
218 1
        return true;
219
    }
220
221
    /**
222
     * {@inheritDoc}
223
     */
224 8
    public function fetch($fetch_style = null, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = 0)
225
    {
226 8
        if (!$this->hasExecuted()) {
227 8
            $this->execute();
228 8
        }
229
230 8
        if (!$this->isSuccessful()) {
231 1
            return false;
232
        }
233
234 7
        if (!$this->collection->valid()) {
235 1
            return false;
236
        }
237
238
        // Get the current row
239 6
        $row = $this->collection->current();
240
241
        // Traverse
242 6
        $this->collection->next();
243
244 6
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
245
246
        switch ($fetch_style)
247
        {
248 6
            case PDO::FETCH_NAMED:
249 6
            case PDO::FETCH_ASSOC:
250 2
                return array_combine($this->collection->getColumns(false), $row);
251
252 4
            case PDO::FETCH_BOTH:
253 1
                return array_merge($row, array_combine($this->collection->getColumns(false), $row));
254
255 3
            case PDO::FETCH_BOUND:
256 1
                $this->updateBoundColumns($row);
257 1
                return true;
258
259 2
            case PDO::FETCH_NUM:
260 1
                return $row;
261
262 1
            default:
263 1
                throw new Exception\UnsupportedException('Unsupported fetch style');
264 1
        }
265
    }
266
267
    /**
268
     * {@inheritDoc}
269
     */
270 2
    public function bindParam(
271
        $parameter,
272
        & $variable,
273
        $data_type = PDO::PARAM_STR,
274
        $length = null,
275
        $driver_options = null
276
    ) {
277 2
        if (is_numeric($parameter)) {
278 2
            if ($parameter == 0) {
279 1
                throw new Exception\UnsupportedException("0-based parameter binding not supported, use 1-based");
280
            }
281 1
            $parameter--;
282 1
        } else {
283
            $namedParameterKey = substr($parameter, 0, 1) === ':' ? substr($parameter, 1) : $parameter;
284
            if (array_key_exists($namedParameterKey, $this->namedToPositionalMap)) {
285
                $parameter = $this->namedToPositionalMap[$namedParameterKey];
286
            } else {
287
                throw new Exception\OutOfBoundsException(
288
                    sprintf('The named parameter "%s" does not exist', $parameter)
289
                );
290
            }
291
        }
292 1
        $this->parameters[$parameter] = &$variable;
293 1
    }
294
295
    /**
296
     * {@inheritDoc}
297
     */
298 1
    public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
299
    {
300 1
        $type = $type ?: PDO::PARAM_STR;
301
302 1
        $this->columnBinding[$column] = [
303 1
            'ref'        => &$param,
304 1
            'type'       => $type,
305 1
            'maxlen'     => $maxlen,
306
            'driverdata' => $driverdata
307 1
        ];
308 1
    }
309
310
    /**
311
     * {@inheritDoc}
312
     */
313 4
    public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR)
314
    {
315 4
        $value = $this->typedValue($value, $data_type);
316 4
        $this->bindParam($parameter, $value, $data_type);
317 4
    }
318
319
    /**
320
     * {@inheritDoc}
321
     */
322 2
    public function rowCount()
323
    {
324 2
        if (!$this->hasExecuted()) {
325 2
            $this->execute();
326 2
        }
327
328 2
        if (!$this->isSuccessful()) {
329 1
            return 0;
330
        }
331
332 1
        return $this->collection->count();
333
    }
334
335
    /**
336
     * {@inheritDoc}
337
     */
338 5
    public function fetchColumn($column_number = 0)
339
    {
340 5
        if (!is_int($column_number)) {
341 1
            throw new Exception\InvalidArgumentException('column_number must be a valid integer');
342
        }
343
344 4
        if (!$this->hasExecuted()) {
345 4
            $this->execute();
346 4
        }
347
348 4
        if (!$this->isSuccessful()) {
349 1
            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...
350
        }
351
352 3
        if (!$this->collection->valid()) {
353 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...
354
        }
355
356 2
        $row = $this->collection->current();
357 2
        $this->collection->next();
358
359 2
        if ($column_number >= count($row)) {
360 1
            throw new Exception\OutOfBoundsException(
361 1
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
362 1
            );
363
        }
364
365 1
        return $row[$column_number];
366
    }
367
368
    /**
369
     * {@inheritDoc}
370
     */
371 12
    public function fetchAll($fetch_style = null, $fetch_argument = null, $ctor_args = [])
372
    {
373 12
        if (!$this->hasExecuted()) {
374 12
            $this->execute();
375 12
        }
376
377 12
        if (!$this->isSuccessful()) {
378 1
            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...
379
        }
380
381 11
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
382
383
        switch ($fetch_style)
384
        {
385 11
            case PDO::FETCH_NUM:
386 1
                return $this->collection->getRows();
387
388 10
            case PDO::FETCH_NAMED:
389 10
            case PDO::FETCH_ASSOC:
390 2
                $columns = array_flip($this->collection->getColumns());
391
392
                return $this->collection->map(function (array $row) use ($columns) {
393 2
                    return array_combine($columns, $row);
394 2
                });
395
396 8
            case PDO::FETCH_BOTH:
397 2
                $columns = array_flip($this->collection->getColumns());
398
399
                return $this->collection->map(function (array $row) use ($columns) {
400 2
                    return array_merge($row, array_combine($columns, $row));
401 2
                });
402
403 6
            case PDO::FETCH_FUNC:
404 2
                if (!is_callable($fetch_argument)) {
405 1
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
406
                }
407
408
                return $this->collection->map(function (array $row) use ($fetch_argument) {
409 1
                    return call_user_func_array($fetch_argument, $row);
410 1
                });
411
412 4
            case PDO::FETCH_COLUMN:
413 3
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
414
415 3
                if (!is_int($columnIndex)) {
416 1
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
417
                }
418
419 2
                $columns = $this->collection->getColumns(false);
420 2
                if (!isset($columns[$columnIndex])) {
421 1
                    throw new Exception\OutOfBoundsException(
422 1
                        sprintf('Column with the index %d does not exist.', $columnIndex)
423 1
                    );
424
                }
425
426 1
                return $this->collection->map(function (array $row) use ($columnIndex) {
427 1
                    return $row[$columnIndex];
428 1
                });
429
430 1
            default:
431 1
                throw new Exception\UnsupportedException('Unsupported fetch style');
432 1
        }
433
    }
434
435
    /**
436
     * {@inheritDoc}
437
     */
438 1
    public function fetchObject($class_name = null, $ctor_args = null)
439
    {
440 1
        throw new Exception\UnsupportedException;
441
    }
442
443
    /**
444
     * {@inheritDoc}
445
     */
446 1
    public function errorCode()
447
    {
448 1
        return $this->errorCode;
449
    }
450
451
    /**
452
     * {@inheritDoc}
453
     */
454 2
    public function errorInfo()
455
    {
456 2
        if ($this->errorCode === null) {
457 2
            return null;
458
        }
459
460 2
        switch ($this->errorCode)
461
        {
462 2
            case CrateConst::ERR_INVALID_SQL:
463 1
                $ansiErrorCode = 42000;
464 1
                break;
465
466 1
            default:
467 1
                $ansiErrorCode = 'Not available';
468 1
                break;
469 2
        }
470
471
        return [
472 2
            $ansiErrorCode,
473 2
            $this->errorCode,
474 2
            $this->errorMessage
475 2
        ];
476
    }
477
478
    /**
479
     * {@inheritDoc}
480
     */
481 1
    public function setAttribute($attribute, $value)
482
    {
483 1
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
484
    }
485
486
    /**
487
     * {@inheritDoc}
488
     */
489 1
    public function getAttribute($attribute)
490
    {
491 1
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
492
    }
493
494
    /**
495
     * {@inheritDoc}
496
     */
497 1
    public function columnCount()
498
    {
499 1
        if (!$this->hasExecuted()) {
500 1
            $this->execute();
501 1
        }
502
503 1
        return count($this->collection->getColumns());
504
    }
505
506
    /**
507
     * {@inheritDoc}
508
     */
509 1
    public function getColumnMeta($column)
510
    {
511 1
        throw new Exception\UnsupportedException;
512
    }
513
514
    /**
515
     * {@inheritDoc}
516
     */
517 14
    public function setFetchMode($mode, $params = null)
518
    {
519 14
        $args     = func_get_args();
520 14
        $argCount = count($args);
521
522
        switch ($mode)
523
        {
524 14
            case PDO::FETCH_COLUMN:
525 3
                if ($argCount != 2) {
526 1
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
527
                }
528
529 2
                if (!is_int($params)) {
530 1
                    throw new Exception\InvalidArgumentException('colno must be an integer');
531
                }
532
533 1
                $this->options['fetchMode']   = $mode;
534 1
                $this->options['fetchColumn'] = $params;
535 1
                break;
536
537 11
            case PDO::FETCH_ASSOC:
538 11
            case PDO::FETCH_NUM:
539 11
            case PDO::FETCH_BOTH:
540 11
            case PDO::FETCH_BOUND:
541 11
            case PDO::FETCH_NAMED:
542 10
                if ($params !== null) {
543 5
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
544
                }
545
546 5
                $this->options['fetchMode'] = $mode;
547 5
                break;
548
549 1
            default:
550 1
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
551 1
        }
552 6
    }
553
554
    /**
555
     * {@inheritDoc}
556
     */
557 2
    public function nextRowset()
558
    {
559 2
        if (!$this->hasExecuted()) {
560 2
            $this->execute();
561 2
        }
562
563 2
        if (!$this->isSuccessful()) {
564 1
            return false;
565
        }
566
567 1
        $this->collection->next();
568 1
        return $this->collection->valid();
569
    }
570
571
    /**
572
     * {@inheritDoc}
573
     */
574 1
    public function closeCursor()
575
    {
576 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...
577 1
        $this->collection = null;
578 1
        return true;
579
    }
580
581
    /**
582
     * {@inheritDoc}
583
     */
584 1
    public function debugDumpParams()
585
    {
586 1
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
587
    }
588
589
    /**
590
     * {@Inheritdoc}
591
     */
592 1
    public function getIterator()
593
    {
594 1
        return new ArrayIterator($this->fetchAll());
595
    }
596
597 8
    private function typedValue($value, $data_type)
598
    {
599
        switch ($data_type)
600
        {
601 8
            case PDO::PARAM_FLOAT:
602 8
            case PDO::PARAM_DOUBLE:
603 1
                return (float) $value;
604
605 8
            case PDO::PARAM_INT:
606 8
            case PDO::PARAM_LONG:
607 3
                return (int) $value;
608
609 7
            case PDO::PARAM_NULL:
610 2
                return null;
611
612 6
            case PDO::PARAM_BOOL:
613 2
                return (bool) $value;
614
615 5
            case PDO::PARAM_STR:
616 5
            case PDO::PARAM_IP:
617 4
                return (string) $value;
618
619 2
            case PDO::PARAM_OBJECT:
620 2
            case PDO::PARAM_ARRAY:
621 1
                return (array) $value;
622
623 2
            case PDO::PARAM_TIMESTAMP:
624 1
                if (is_numeric($value)) {
625 1
                    return (int) $value;
626
                }
627 1
                return (string) $value;
628
629 1
            default:
630 1
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
631 1
        }
632
633
    }
634
}
635