Completed
Push — 0.5 ( c2e965...82cbca )
by
unknown
08:07 queued 06:00
created

PDOStatement::getAttribute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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