Passed
Pull Request — main (#116)
by Andreas
08:45
created

PDOStatement::errorInfo()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

349
        return $this->collection->/** @scrutinizer ignore-call */ count();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
350
    }
351
352
    /**
353 5
     * {@inheritDoc}
354
     */
355 5
    public function fetchColumn($column_number = 0)
356 1
    {
357
        if (!is_int($column_number)) {
358
            throw new Exception\InvalidArgumentException('column_number must be a valid integer');
359 4
        }
360 4
361
        if (!$this->hasExecuted()) {
362
            $this->execute();
363 4
        }
364 1
365
        if (!$this->isSuccessful()) {
366
            return false;
367 3
        }
368 2
369
        if (!$this->collection->valid()) {
370
            return false;
371 2
        }
372 2
373
        $row = $this->collection->current();
374 2
        $this->collection->next();
375 1
376 1
        if ($column_number >= count($row)) {
377
            throw new Exception\OutOfBoundsException(
378
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
379
            );
380 1
        }
381
382
        return $row[$column_number];
383
    }
384
385
    /**
386 13
     * {@inheritDoc}
387
     */
388 13
    public function doFetchAll($fetch_style = null, $fetch_argument = null, $ctor_args = null)
389 13
    {
390
        if (!$this->hasExecuted()) {
391
            $this->execute();
392 13
        }
393 1
394
        if (!$this->isSuccessful()) {
395
            return false;
396 12
        }
397
398 12
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
399
400 1
        switch ($fetch_style) {
401
            case PDO::FETCH_NUM:
402
                return $this->collection->getRows();
403
404 2
            case PDO::FETCH_NAMED:
405
            case PDO::FETCH_ASSOC:
406
                $columns = $this->collection->getColumns(false);
407 2
408 2
                return $this->collection->map(function (array $row) use ($columns) {
409
                    return array_combine($columns, $row);
410
                });
411 3
412
            case PDO::FETCH_BOTH:
413
                $columns = $this->collection->getColumns(false);
414 3
415 3
                return $this->collection->map(function (array $row) use ($columns) {
416
                    return array_merge($row, array_combine($columns, $row));
417
                });
418 2
419 1
            case PDO::FETCH_FUNC:
420
                if (!is_callable($fetch_argument)) {
421
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
422
                }
423 1
424 1
                return $this->collection->map(function (array $row) use ($fetch_argument) {
425
                    return call_user_func_array($fetch_argument, $row);
426
                });
427 3
428
            case PDO::FETCH_COLUMN:
429 3
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
430 1
431
                if (!is_int($columnIndex)) {
432
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
433 2
                }
434 2
435 1
                $columns = $this->collection->getColumns(false);
436 1
                if (!isset($columns[$columnIndex])) {
437
                    throw new Exception\OutOfBoundsException(
438
                        sprintf('Column with the index %d does not exist.', $columnIndex)
439
                    );
440
                }
441 1
442 1
                return $this->collection->map(function (array $row) use ($columnIndex) {
443
                    return $row[$columnIndex];
444
                });
445
            case PDO::FETCH_OBJ:
446
                $columns = $this->collection->getColumns(false);
447
448
                return $this->collection->map(function (array $row) use ($columns) {
449
                    return $this->getObjectResult($columns, $row);
450
                });
451 1
452
            default:
453
                throw new Exception\UnsupportedException('Unsupported fetch style');
454
        }
455
    }
456
457
    /**
458 1
     * {@inheritDoc}
459
     */
460 1
    public function fetchObject($class_name = null, $ctor_args = null)
461
    {
462
        throw new Exception\UnsupportedException;
463
    }
464
465
    /**
466 1
     * {@inheritDoc}
467
     */
468 1
    public function errorCode()
469
    {
470
        return $this->errorCode;
471
    }
472
473
    /**
474 2
     * {@inheritDoc}
475
     */
476 2
    public function errorInfo()
477 2
    {
478
        if ($this->errorCode === null) {
479
            return null;
480 2
        }
481
482 1
        switch ($this->errorCode) {
483 1
            case CrateConst::ERR_INVALID_SQL:
484
                $ansiErrorCode = 42000;
485
                break;
486 1
487 1
            default:
488
                $ansiErrorCode = 'Not available';
489
                break;
490
        }
491 2
492 2
        return [
493 2
            $ansiErrorCode,
494
            $this->errorCode,
495
            $this->errorMessage,
496
        ];
497
    }
498
499
    /**
500 1
     * {@inheritDoc}
501
     */
502 1
    public function setAttribute($attribute, $value)
503
    {
504
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
505
    }
506
507
    /**
508 1
     * {@inheritDoc}
509
     */
510 1
    public function getAttribute($attribute)
511
    {
512
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
513
    }
514
515
    /**
516 2
     * {@inheritDoc}
517
     */
518 2
    public function columnCount()
519 2
    {
520
        if (!$this->hasExecuted()) {
521
            $this->execute();
522 2
        }
523
524
        return count($this->collection->getColumns(false));
525
    }
526
527
    /**
528 1
     * {@inheritDoc}
529
     */
530 1
    public function getColumnMeta($column)
531
    {
532
        throw new Exception\UnsupportedException;
533
    }
534
535
    /**
536 14
     * {@inheritDoc}
537
     */
538 14
    public function doSetFetchMode($mode, $params = null)
539 14
    {
540
        $args     = func_get_args();
541 14
        $argCount = count($args);
542
543 3
        switch ($mode) {
544 1
            case PDO::FETCH_COLUMN:
545
                if ($argCount != 2) {
546
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
547 2
                }
548 1
549
                if (!is_int($params)) {
550
                    throw new Exception\InvalidArgumentException('colno must be an integer');
551 1
                }
552 1
553 1
                $this->options['fetchMode']   = $mode;
554
                $this->options['fetchColumn'] = $params;
555
                break;
556
557
            case PDO::FETCH_ASSOC:
558
            case PDO::FETCH_NUM:
559
            case PDO::FETCH_BOTH:
560
            case PDO::FETCH_BOUND:
561 10
            case PDO::FETCH_NAMED:
562 5
            case PDO::FETCH_OBJ:
563
                if ($params !== null) {
564
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
565 5
                }
566 5
567
                $this->options['fetchMode'] = $mode;
568
                break;
569 1
570
            default:
571
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
572 6
        }
573
574
        return true;
575
    }
576
577
    /**
578 2
     * {@inheritDoc}
579
     */
580 2
    public function nextRowset()
581 2
    {
582
        if (!$this->hasExecuted()) {
583
            $this->execute();
584 2
        }
585 1
586
        if (!$this->isSuccessful()) {
587
            return false;
588 1
        }
589
590 1
        $this->collection->next();
591
592
        return $this->collection->valid();
593
    }
594
595
    /**
596 1
     * {@inheritDoc}
597
     */
598 1
    public function closeCursor()
599 1
    {
600
        $this->errorCode  = 0;
601 1
        $this->collection = null;
602
603
        return true;
604
    }
605
606
    /**
607 1
     * {@inheritDoc}
608
     */
609 1
    public function debugDumpParams()
610
    {
611
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
612
    }
613
614
    /**
615 1
     * {@Inheritdoc}
616
     */
617 1
    public function getIterator(): \Iterator
618 1
    {
619
        $results = $this->fetchAll();
620
        if ($results === false) {
621 1
            throw new Exception\RuntimeException('Failure when fetching data');
622
        }
623
        return new ArrayIterator($results);
624 16
    }
625
626 16
    private function typedValue($value, $data_type)
627
    {
628 3
        if (null === $value) {
629
            // Do not typecast null values
630
            return null;
631 13
        }
632
633
        switch ($data_type) {
634 1
            case PDO::PARAM_FLOAT:
635
            case PDO::PARAM_DOUBLE:
636
                return (float)$value;
637
638 3
            case PDO::PARAM_INT:
639
            case PDO::PARAM_LONG:
640
                return (int)$value;
641 2
642
            case PDO::PARAM_NULL:
643
                return null;
644 7
645
            case PDO::PARAM_BOOL:
646
                return filter_var($value, FILTER_VALIDATE_BOOLEAN);
647
648 4
            case PDO::PARAM_STR:
649
            case PDO::PARAM_IP:
650
                return (string)$value;
651
652 1
            case PDO::PARAM_OBJECT:
653
            case PDO::PARAM_ARRAY:
654
                return (array)$value;
655 1
656 1
            case PDO::PARAM_TIMESTAMP:
657
                if (is_numeric($value)) {
658
                    return (int)$value;
659 1
                }
660
661
                return (string)$value;
662 1
663
            default:
664
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
665
        }
666
    }
667
668
    /**
669
     * Generate object from array
670
     *
671
     * @param array $columns
672
     * @param array $row
673
     */
674
    private function getObjectResult(array $columns, array $row)
675
    {
676
        $obj = new \stdClass();
677
        foreach ($columns as $key => $column) {
678
            $obj->{$column} = $row[$key];
679
        }
680
681
        return $obj;
682
    }
683
}
684