Passed
Pull Request — main (#183)
by Andreas
01:49
created

PDOStatement::errorCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

369
        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...
370
    }
371
372
    /**
373
     * {@inheritDoc}
374
     */
375 5
    #[\ReturnTypeWillChange]
376 5
    public function fetchColumn($column_number = 0)
377
    {
378 10
        if (!is_int($column_number)) {
379 2
            throw new Exception\InvalidArgumentException('column_number must be a valid integer');
380
        }
381
382 8
        if (!$this->hasExecuted()) {
383 8
            $this->execute();
384
        }
385
386 8
        if (!$this->isSuccessful()) {
387 2
            return false;
388
        }
389
390 6
        if (!$this->collection->valid()) {
391 4
            return false;
392
        }
393
394 4
        $row = $this->collection->current();
395 4
        $this->collection->next();
396
397 4
        if ($column_number >= count($row)) {
398 2
            throw new Exception\OutOfBoundsException(
399 2
                sprintf('The column "%d" with the zero-based does not exist', $column_number)
400 2
            );
401
        }
402
403 2
        return $row[$column_number];
404
    }
405
406
    /**
407
     * {@inheritDoc}
408
     */
409 26
    public function doFetchAll($fetch_style = null, $fetch_argument = null, $ctor_args = null)
0 ignored issues
show
Unused Code introduced by
The parameter $ctor_args is not used and could be removed. ( Ignorable by Annotation )

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

409
    public function doFetchAll($fetch_style = null, $fetch_argument = null, /** @scrutinizer ignore-unused */ $ctor_args = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
410
    {
411 26
        if (!$this->hasExecuted()) {
412 26
            $this->execute();
413
        }
414
415 26
        if (!$this->isSuccessful()) {
416 2
            return false;
417
        }
418
419 24
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
420
421
        switch ($fetch_style) {
422
            case PDOCrateDB::FETCH_NUM:
423 2
                return $this->collection->getRows();
424
425
            case PDOCrateDB::FETCH_NAMED:
426
            case PDOCrateDB::FETCH_ASSOC:
427 4
                $columns = $this->collection->getColumns(false);
428
429 4
                return $this->collection->map(function (array $row) use ($columns) {
430 4
                    return array_combine($columns, $row);
431 4
                });
432
433
            case PDOCrateDB::FETCH_BOTH:
434 6
                $columns = $this->collection->getColumns(false);
435
436 6
                return $this->collection->map(function (array $row) use ($columns) {
437 6
                    return array_merge($row, array_combine($columns, $row));
438 6
                });
439
440
            case PDOCrateDB::FETCH_FUNC:
441 4
                if (!is_callable($fetch_argument)) {
442 2
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
443
                }
444
445 2
                return $this->collection->map(function (array $row) use ($fetch_argument) {
446 2
                    return call_user_func_array($fetch_argument, $row);
447 2
                });
448
449
            case PDOCrateDB::FETCH_COLUMN:
450 6
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
451
452 6
                if (!is_int($columnIndex)) {
453 2
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
454
                }
455
456 4
                $columns = $this->collection->getColumns(false);
457 4
                if (!isset($columns[$columnIndex])) {
458 2
                    throw new Exception\OutOfBoundsException(
459 2
                        sprintf('Column with the index %d does not exist.', $columnIndex)
460 2
                    );
461
                }
462
463 2
                return $this->collection->map(function (array $row) use ($columnIndex) {
464 2
                    return $row[$columnIndex];
465 2
                });
466
            case PDOCrateDB::FETCH_OBJ:
467
                $columns = $this->collection->getColumns(false);
468
469
                return $this->collection->map(function (array $row) use ($columns) {
470
                    return $this->getObjectResult($columns, $row);
471
                });
472
473
            default:
474 2
                throw new Exception\UnsupportedException('Unsupported fetch style');
475
        }
476
    }
477
478
    /**
479
     * {@inheritDoc}
480
     */
481 1
    #[\ReturnTypeWillChange]
482 1
    public function fetchObject($class_name = null, $ctor_args = null)
483
    {
484 2
        throw new Exception\UnsupportedException;
485
    }
486
487
    /**
488
     * {@inheritDoc}
489
     */
490 2
    public function errorCode(): ?string
491
    {
492 2
        return $this->errorCode;
493
    }
494
495
    /**
496
     * {@inheritDoc}
497
     *
498
     * @return array
499
     */
500 4
    #[\ReturnTypeWillChange]
501 4
    public function errorInfo()
502
    {
503 8
        if ($this->errorCode === null) {
504 4
            return ["00000", null, null];
505
        }
506
507 4
        switch ($this->errorCode) {
508
            case CrateConst::ERR_INVALID_SQL:
509 2
                $ansiErrorCode = '42000';
510 2
                break;
511
512
            default:
513 2
                $ansiErrorCode = 'Not available';
514 2
                break;
515
        }
516
517 4
        return [
518 4
            strval($ansiErrorCode),
519 4
            intval($this->errorCode),
520 4
            strval($this->errorMessage),
521 4
        ];
522
    }
523
524
    /**
525
     * {@inheritDoc}
526
     *
527
     * @param int $attribute
528
     * @param mixed $value
529
     */
530 1
    #[\ReturnTypeWillChange]
531 1
    public function setAttribute($attribute, $value)
532
    {
533 2
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
534
    }
535
536
    /**
537
     * {@inheritDoc}
538
     */
539 1
    #[\ReturnTypeWillChange]
540 1
    public function getAttribute($attribute)
541
    {
542 2
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
543
    }
544
545
    /**
546
     * {@inheritDoc}
547
     */
548 4
    public function columnCount(): int
549
    {
550 4
        if (!$this->hasExecuted()) {
551 4
            $this->execute();
552
        }
553
554 4
        return count($this->collection->getColumns(false));
555
    }
556
557
    /**
558
     * {@inheritDoc}
559
     */
560 1
    #[\ReturnTypeWillChange]
561 1
    public function getColumnMeta($column)
562
    {
563 2
        throw new Exception\UnsupportedException;
564
    }
565
566
    /**
567
     * {@inheritDoc}
568
     */
569 20
    public function doSetFetchMode($mode, $params = null)
570
    {
571
572
        switch ($mode) {
573
            case PDOCrateDB::FETCH_COLUMN:
574 6
                if ($params === null) {
575 2
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
576
                }
577
578 4
                if (!is_int($params)) {
579 2
                    throw new Exception\InvalidArgumentException('colno must be an integer');
580
                }
581
582 2
                $this->options['fetchMode']   = $mode;
583 2
                $this->options['fetchColumn'] = $params;
584 2
                break;
585
586
            case PDOCrateDB::FETCH_ASSOC:
587
            case PDOCrateDB::FETCH_NUM:
588
            case PDOCrateDB::FETCH_BOTH:
589
            case PDOCrateDB::FETCH_BOUND:
590
            case PDOCrateDB::FETCH_NAMED:
591
            case PDOCrateDB::FETCH_OBJ:
592 20
                if (!empty($params)) {
593 10
                    throw new Exception\InvalidArgumentException('Fetch mode does not allow any extra arguments');
594
                }
595
596 10
                $this->options['fetchMode'] = $mode;
597 10
                break;
598
599
            default:
600 2
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
601
        }
602
603 12
        return true;
604
    }
605
606
    /**
607
     * {@inheritDoc}
608
     */
609 4
    public function nextRowset(): bool
610
    {
611 4
        if (!$this->hasExecuted()) {
612 4
            $this->execute();
613
        }
614
615 4
        if (!$this->isSuccessful()) {
616 2
            return false;
617
        }
618
619 2
        $this->collection->next();
620
621 2
        return $this->collection->valid();
622
    }
623
624
    /**
625
     * {@inheritDoc}
626
     */
627 2
    public function closeCursor(): bool
628
    {
629 2
        $this->errorCode  = null;
630 2
        $this->collection = null;
631
632 2
        return true;
633
    }
634
635
    /**
636
     * {@inheritDoc}
637
     */
638 2
    public function debugDumpParams(): ?bool
639
    {
640 2
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
641
    }
642
643
    /**
644
     * {@Inheritdoc}
645
     */
646 2
    public function getIterator(): \Iterator
647
    {
648 2
        $results = $this->fetchAll();
649 2
        if ($results === false) {
0 ignored issues
show
introduced by
The condition $results === false is always false.
Loading history...
650
            throw new Exception\RuntimeException('Failure when fetching data');
651
        }
652 2
        return new ArrayIterator($results);
653
    }
654
655 32
    private function typedValue($value, $data_type)
656
    {
657 32
        if (null === $value) {
658
            // Do not typecast null values
659 6
            return null;
660
        }
661
662
        switch ($data_type) {
663
            case PDOCrateDB::PARAM_FLOAT:
664
            case PDOCrateDB::PARAM_DOUBLE:
665 2
                return (float)$value;
666
667
            case PDOCrateDB::PARAM_INT:
668
            case PDOCrateDB::PARAM_LONG:
669 6
                return (int)$value;
670
671
            case PDOCrateDB::PARAM_NULL:
672 4
                return null;
673
674
            case PDOCrateDB::PARAM_BOOL:
675 14
                return filter_var($value, FILTER_VALIDATE_BOOLEAN);
676
677
            case PDOCrateDB::PARAM_STR:
678
            case PDOCrateDB::PARAM_IP:
679 8
                return (string)$value;
680
681
            case PDOCrateDB::PARAM_OBJECT:
682
            case PDOCrateDB::PARAM_ARRAY:
683 2
                return (array)$value;
684
685
            case PDOCrateDB::PARAM_TIMESTAMP:
686 2
                if (is_numeric($value)) {
687 2
                    return (int)$value;
688
                }
689
690 2
                return (string)$value;
691
692
            default:
693 2
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
694
        }
695
    }
696
697
    /**
698
     * Generate object from array
699
     *
700
     * @param array $columns
701
     * @param array $row
702
     */
703
    private function getObjectResult(array $columns, array $row)
704
    {
705
        $obj = new \stdClass();
706
        foreach ($columns as $key => $column) {
707
            $obj->{$column} = $row[$key];
708
        }
709
710
        return $obj;
711
    }
712
713 2
    public function isBulkMode()
714
    {
715 2
        return $this->options["bulkMode"];
716
    }
717
}
718