Completed
Push — m/0.6.0 ( ff4122 )
by
unknown
08:11 queued 05:23
created

PDOStatement::updateBoundColumns()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 8
cts 9
cp 0.8889
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 1
crap 3.0123
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 68
    private function replaceNamedParametersWithPositionals($sql)
105
    {
106 68
        if (strpos($sql, ':') === false) {
107 68
            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 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) {
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 = 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
        // 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
            default:
266 1
                throw new Exception\UnsupportedException('Unsupported fetch style');
267 1
        }
268
    }
269
270
    /**
271
     * {@inheritDoc}
272
     */
273 2
    public function bindParam(
274
        $parameter,
275
        & $variable,
276
        $data_type = PDO::PARAM_STR,
277
        $length = null,
278
        $driver_options = null
279
    ) {
280 2
        if (is_numeric($parameter)) {
281 2
            if ($parameter == 0) {
282 1
                throw new Exception\UnsupportedException("0-based parameter binding not supported, use 1-based");
283
            }
284 1
            $this->parameters[$parameter-1] = &$variable;
285 1
        } else {
286
            $namedParameterKey = substr($parameter, 0, 1) === ':' ? substr($parameter, 1) : $parameter;
287
            if (in_array($namedParameterKey, $this->namedToPositionalMap, true)) {
288
                foreach ($this->namedToPositionalMap as $key => $value) {
289
                    if ($value == $namedParameterKey) {
290
                        $this->parameters[$key] = &$variable;
291
                    }
292
                }
293
            } else {
294
                throw new Exception\OutOfBoundsException(
295
                    sprintf('The named parameter "%s" does not exist', $parameter)
296
                );
297
            }
298
        }
299 1
    }
300
301
    /**
302
     * {@inheritDoc}
303
     */
304 1
    public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
305
    {
306 1
        $type = $type ?: PDO::PARAM_STR;
307
308 1
        $this->columnBinding[$column] = [
309 1
            'ref'        => &$param,
310 1
            'type'       => $type,
311 1
            'maxlen'     => $maxlen,
312
            'driverdata' => $driverdata
313 1
        ];
314 1
    }
315
316
    /**
317
     * {@inheritDoc}
318
     */
319 4
    public function bindValue($parameter, $value, $data_type = PDO::PARAM_STR)
320
    {
321 4
        $value = $this->typedValue($value, $data_type);
322 4
        $this->bindParam($parameter, $value, $data_type);
323 4
    }
324
325
    /**
326
     * {@inheritDoc}
327
     */
328 2
    public function rowCount()
329
    {
330 2
        if (!$this->hasExecuted()) {
331 2
            $this->execute();
332 2
        }
333
334 2
        if (!$this->isSuccessful()) {
335 1
            return 0;
336
        }
337
338 1
        return $this->collection->count();
339
    }
340
341
    /**
342
     * {@inheritDoc}
343
     */
344 5
    public function fetchColumn($column_number = 0)
345
    {
346 5
        if (!is_int($column_number)) {
347 1
            throw new Exception\InvalidArgumentException('column_number must be a valid integer');
348
        }
349
350 4
        if (!$this->hasExecuted()) {
351 4
            $this->execute();
352 4
        }
353
354 4
        if (!$this->isSuccessful()) {
355 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...
356
        }
357
358 3
        if (!$this->collection->valid()) {
359 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...
360
        }
361
362 2
        $row = $this->collection->current();
363 2
        $this->collection->next();
364
365 2
        if ($column_number >= count($row)) {
366 1
            throw new Exception\OutOfBoundsException(
367 1
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
368 1
            );
369
        }
370
371 1
        return $row[$column_number];
372
    }
373
374
    /**
375
     * {@inheritDoc}
376
     */
377 12
    public function fetchAll($fetch_style = null, $fetch_argument = null, $ctor_args = [])
378
    {
379 12
        if (!$this->hasExecuted()) {
380 12
            $this->execute();
381 12
        }
382
383 12
        if (!$this->isSuccessful()) {
384 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...
385
        }
386
387 11
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
388
389
        switch ($fetch_style)
390
        {
391 11
            case PDO::FETCH_NUM:
392 1
                return $this->collection->getRows();
393
394 10
            case PDO::FETCH_NAMED:
395 10
            case PDO::FETCH_ASSOC:
396 2
                $columns = array_flip($this->collection->getColumns());
397
398
                return $this->collection->map(function (array $row) use ($columns) {
399 2
                    return array_combine($columns, $row);
400 2
                });
401
402 8
            case PDO::FETCH_BOTH:
403 2
                $columns = array_flip($this->collection->getColumns());
404
405
                return $this->collection->map(function (array $row) use ($columns) {
406 2
                    return array_merge($row, array_combine($columns, $row));
407 2
                });
408
409 6
            case PDO::FETCH_FUNC:
410 2
                if (!is_callable($fetch_argument)) {
411 1
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
412
                }
413
414
                return $this->collection->map(function (array $row) use ($fetch_argument) {
415 1
                    return call_user_func_array($fetch_argument, $row);
416 1
                });
417
418 4
            case PDO::FETCH_COLUMN:
419 3
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
420
421 3
                if (!is_int($columnIndex)) {
422 1
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
423
                }
424
425 2
                $columns = $this->collection->getColumns(false);
426 2
                if (!isset($columns[$columnIndex])) {
427 1
                    throw new Exception\OutOfBoundsException(
428 1
                        sprintf('Column with the index %d does not exist.', $columnIndex)
429 1
                    );
430
                }
431
432 1
                return $this->collection->map(function (array $row) use ($columnIndex) {
433 1
                    return $row[$columnIndex];
434 1
                });
435
436 1
            default:
437 1
                throw new Exception\UnsupportedException('Unsupported fetch style');
438 1
        }
439
    }
440
441
    /**
442
     * {@inheritDoc}
443
     */
444 1
    public function fetchObject($class_name = null, $ctor_args = null)
445
    {
446 1
        throw new Exception\UnsupportedException;
447
    }
448
449
    /**
450
     * {@inheritDoc}
451
     */
452 1
    public function errorCode()
453
    {
454 1
        return $this->errorCode;
455
    }
456
457
    /**
458
     * {@inheritDoc}
459
     */
460 2
    public function errorInfo()
461
    {
462 2
        if ($this->errorCode === null) {
463 2
            return null;
464
        }
465
466 2
        switch ($this->errorCode)
467
        {
468 2
            case CrateConst::ERR_INVALID_SQL:
469 1
                $ansiErrorCode = 42000;
470 1
                break;
471
472 1
            default:
473 1
                $ansiErrorCode = 'Not available';
474 1
                break;
475 2
        }
476
477
        return [
478 2
            $ansiErrorCode,
479 2
            $this->errorCode,
480 2
            $this->errorMessage
481 2
        ];
482
    }
483
484
    /**
485
     * {@inheritDoc}
486
     */
487 1
    public function setAttribute($attribute, $value)
488
    {
489 1
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
490
    }
491
492
    /**
493
     * {@inheritDoc}
494
     */
495 1
    public function getAttribute($attribute)
496
    {
497 1
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
498
    }
499
500
    /**
501
     * {@inheritDoc}
502
     */
503 1
    public function columnCount()
504
    {
505 1
        if (!$this->hasExecuted()) {
506 1
            $this->execute();
507 1
        }
508
509 1
        return count($this->collection->getColumns());
510
    }
511
512
    /**
513
     * {@inheritDoc}
514
     */
515 1
    public function getColumnMeta($column)
516
    {
517 1
        throw new Exception\UnsupportedException;
518
    }
519
520
    /**
521
     * {@inheritDoc}
522
     */
523 14
    public function setFetchMode($mode, $params = null)
524
    {
525 14
        $args     = func_get_args();
526 14
        $argCount = count($args);
527
528
        switch ($mode)
529
        {
530 14
            case PDO::FETCH_COLUMN:
531 3
                if ($argCount != 2) {
532 1
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
533
                }
534
535 2
                if (!is_int($params)) {
536 1
                    throw new Exception\InvalidArgumentException('colno must be an integer');
537
                }
538
539 1
                $this->options['fetchMode']   = $mode;
540 1
                $this->options['fetchColumn'] = $params;
541 1
                break;
542
543 11
            case PDO::FETCH_ASSOC:
544 11
            case PDO::FETCH_NUM:
545 11
            case PDO::FETCH_BOTH:
546 11
            case PDO::FETCH_BOUND:
547 11
            case PDO::FETCH_NAMED:
548 10
                if ($params !== null) {
549 5
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
550
                }
551
552 5
                $this->options['fetchMode'] = $mode;
553 5
                break;
554
555 1
            default:
556 1
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
557 1
        }
558 6
    }
559
560
    /**
561
     * {@inheritDoc}
562
     */
563 2
    public function nextRowset()
564
    {
565 2
        if (!$this->hasExecuted()) {
566 2
            $this->execute();
567 2
        }
568
569 2
        if (!$this->isSuccessful()) {
570 1
            return false;
571
        }
572
573 1
        $this->collection->next();
574 1
        return $this->collection->valid();
575
    }
576
577
    /**
578
     * {@inheritDoc}
579
     */
580 1
    public function closeCursor()
581
    {
582 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...
583 1
        $this->collection = null;
584 1
        return true;
585
    }
586
587
    /**
588
     * {@inheritDoc}
589
     */
590 1
    public function debugDumpParams()
591
    {
592 1
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
593
    }
594
595
    /**
596
     * {@Inheritdoc}
597
     */
598 1
    public function getIterator()
599
    {
600 1
        return new ArrayIterator($this->fetchAll());
601
    }
602
603 8
    private function typedValue($value, $data_type)
604
    {
605
        switch ($data_type)
606
        {
607 8
            case PDO::PARAM_FLOAT:
608 8
            case PDO::PARAM_DOUBLE:
609 1
                return (float) $value;
610
611 8
            case PDO::PARAM_INT:
612 8
            case PDO::PARAM_LONG:
613 3
                return (int) $value;
614
615 7
            case PDO::PARAM_NULL:
616 2
                return null;
617
618 6
            case PDO::PARAM_BOOL:
619 2
                return (bool) $value;
620
621 5
            case PDO::PARAM_STR:
622 5
            case PDO::PARAM_IP:
623 4
                return (string) $value;
624
625 2
            case PDO::PARAM_OBJECT:
626 2
            case PDO::PARAM_ARRAY:
627 1
                return (array) $value;
628
629 2
            case PDO::PARAM_TIMESTAMP:
630 1
                if (is_numeric($value)) {
631 1
                    return (int) $value;
632
                }
633 1
                return (string) $value;
634
635 1
            default:
636 1
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
637 1
        }
638
639
    }
640
}
641