Passed
Push — main ( 0ca8fa...68af6e )
by Andreas
02:01
created

PDOStatement::execute()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 10
nc 4
nop 1
dl 0
loc 22
ccs 11
cts 11
cp 1
crap 3
rs 9.9332
c 2
b 0
f 0
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(PDO::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 = PDO::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 PDO::FETCH_NAMED:
275
            case PDO::FETCH_ASSOC:
276 4
                return array_combine($this->collection->getColumns(false), $row);
277
278
            case PDO::FETCH_BOTH:
279 4
                return array_merge($row, array_combine($this->collection->getColumns(false), $row));
280
281
            case PDO::FETCH_BOUND:
282 2
                $this->updateBoundColumns($row);
283
284 2
                return true;
285
286
            case PDO::FETCH_NUM:
287 2
                return $row;
288
289
            case PDO::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 = PDO::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 ?: PDO::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 = PDO::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 PDO::FETCH_NUM:
423 2
                return $this->collection->getRows();
424
425
            case PDO::FETCH_NAMED:
426
            case PDO::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 PDO::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 PDO::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 PDO::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 PDO::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 28
    public function doSetFetchMode($mode, $params = null)
570
    {
571 28
        $args     = func_get_args();
572 28
        $argCount = count($args);
573
574
        switch ($mode) {
575
            case PDO::FETCH_COLUMN:
576 6
                if ($argCount != 2) {
577 2
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
578
                }
579
580 4
                if (!is_int($params)) {
581 2
                    throw new Exception\InvalidArgumentException('colno must be an integer');
582
                }
583
584 2
                $this->options['fetchMode']   = $mode;
585 2
                $this->options['fetchColumn'] = $params;
586 2
                break;
587
588
            case PDO::FETCH_ASSOC:
589
            case PDO::FETCH_NUM:
590
            case PDO::FETCH_BOTH:
591
            case PDO::FETCH_BOUND:
592
            case PDO::FETCH_NAMED:
593
            case PDO::FETCH_OBJ:
594 20
                if ($params !== null) {
595 10
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
596
                }
597
598 10
                $this->options['fetchMode'] = $mode;
599 10
                break;
600
601
            default:
602 2
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
603
        }
604
605 12
        return true;
606
    }
607
608
    /**
609
     * {@inheritDoc}
610
     */
611 4
    public function nextRowset(): bool
612
    {
613 4
        if (!$this->hasExecuted()) {
614 4
            $this->execute();
615
        }
616
617 4
        if (!$this->isSuccessful()) {
618 2
            return false;
619
        }
620
621 2
        $this->collection->next();
622
623 2
        return $this->collection->valid();
624
    }
625
626
    /**
627
     * {@inheritDoc}
628
     */
629 2
    public function closeCursor(): bool
630
    {
631 2
        $this->errorCode  = null;
632 2
        $this->collection = null;
633
634 2
        return true;
635
    }
636
637
    /**
638
     * {@inheritDoc}
639
     */
640 2
    public function debugDumpParams(): ?bool
641
    {
642 2
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
643
    }
644
645
    /**
646
     * {@Inheritdoc}
647
     */
648 2
    public function getIterator(): \Iterator
649
    {
650 2
        $results = $this->fetchAll();
0 ignored issues
show
Deprecated Code introduced by
The function Crate\PDO\PDOStatement::fetchAll() has been deprecated: Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead. ( Ignorable by Annotation )

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

650
        $results = /** @scrutinizer ignore-deprecated */ $this->fetchAll();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
651 2
        if ($results === false) {
0 ignored issues
show
introduced by
The condition $results === false is always false.
Loading history...
652
            throw new Exception\RuntimeException('Failure when fetching data');
653
        }
654 2
        return new ArrayIterator($results);
655
    }
656
657 32
    private function typedValue($value, $data_type)
658
    {
659 32
        if (null === $value) {
660
            // Do not typecast null values
661 6
            return null;
662
        }
663
664
        switch ($data_type) {
665
            case PDO::PARAM_FLOAT:
666
            case PDO::PARAM_DOUBLE:
667 2
                return (float)$value;
668
669
            case PDO::PARAM_INT:
670
            case PDO::PARAM_LONG:
671 6
                return (int)$value;
672
673
            case PDO::PARAM_NULL:
674 4
                return null;
675
676
            case PDO::PARAM_BOOL:
677 14
                return filter_var($value, FILTER_VALIDATE_BOOLEAN);
678
679
            case PDO::PARAM_STR:
680
            case PDO::PARAM_IP:
681 8
                return (string)$value;
682
683
            case PDO::PARAM_OBJECT:
684
            case PDO::PARAM_ARRAY:
685 2
                return (array)$value;
686
687
            case PDO::PARAM_TIMESTAMP:
688 2
                if (is_numeric($value)) {
689 2
                    return (int)$value;
690
                }
691
692 2
                return (string)$value;
693
694
            default:
695 2
                throw new Exception\InvalidArgumentException(sprintf('Parameter type %s not supported', $data_type));
696
        }
697
    }
698
699
    /**
700
     * Generate object from array
701
     *
702
     * @param array $columns
703
     * @param array $row
704
     */
705
    private function getObjectResult(array $columns, array $row)
706
    {
707
        $obj = new \stdClass();
708
        foreach ($columns as $key => $column) {
709
            $obj->{$column} = $row[$key];
710
        }
711
712
        return $obj;
713
    }
714
715 2
    public function isBulkMode()
716
    {
717 2
        return $this->options["bulkMode"];
718
    }
719
}
720