Completed
Pull Request — master (#70)
by
unknown
08:27 queued 06:30
created

PDOStatement::getObjectResult()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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