Passed
Pull Request — main (#146)
by Andreas
01:38
created

PDOStatement::getAttribute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 4
ccs 0
cts 2
cp 0
crap 2
rs 10
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 158
    private function replaceNamedParametersWithPositionals($sql)
109
    {
110 158
        if (strpos($sql, ':') === false) {
111 158
            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 18
    #[\ReturnTypeWillChange]
251
    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 4
    #[\ReturnTypeWillChange]
301
    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 2
    #[\ReturnTypeWillChange]
333
    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 24
    public function bindValue($parameter, $value, int $data_type = PDO::PARAM_STR): bool
349
    {
350 24
        $value = $this->typedValue($value, $data_type);
351 24
        $this->bindParam($parameter, $value, $data_type);
352 24
        return true;
353
    }
354
355
    /**
356
     * {@inheritDoc}
357
     */
358 4
    public function rowCount(): int
359
    {
360 4
        if (!$this->hasExecuted()) {
361 4
            $this->execute();
362
        }
363
364 4
        if (!$this->isSuccessful()) {
365 2
            return 0;
366
        }
367
368 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

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

408
    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...
409
    {
410 26
        if (!$this->hasExecuted()) {
411 26
            $this->execute();
412
        }
413
414 26
        if (!$this->isSuccessful()) {
415 2
            return false;
416
        }
417
418 24
        $fetch_style = $fetch_style ?: $this->getFetchStyle();
419
420
        switch ($fetch_style) {
421
            case PDO::FETCH_NUM:
422 2
                return $this->collection->getRows();
423
424
            case PDO::FETCH_NAMED:
425
            case PDO::FETCH_ASSOC:
426 4
                $columns = $this->collection->getColumns(false);
427
428 4
                return $this->collection->map(function (array $row) use ($columns) {
429 4
                    return array_combine($columns, $row);
430 4
                });
431
432
            case PDO::FETCH_BOTH:
433 6
                $columns = $this->collection->getColumns(false);
434
435 6
                return $this->collection->map(function (array $row) use ($columns) {
436 6
                    return array_merge($row, array_combine($columns, $row));
437 6
                });
438
439
            case PDO::FETCH_FUNC:
440 4
                if (!is_callable($fetch_argument)) {
441 2
                    throw new Exception\InvalidArgumentException('Second argument must be callable');
442
                }
443
444 2
                return $this->collection->map(function (array $row) use ($fetch_argument) {
445 2
                    return call_user_func_array($fetch_argument, $row);
446 2
                });
447
448
            case PDO::FETCH_COLUMN:
449 6
                $columnIndex = $fetch_argument ?: $this->options['fetchColumn'];
450
451 6
                if (!is_int($columnIndex)) {
452 2
                    throw new Exception\InvalidArgumentException('Second argument must be a integer');
453
                }
454
455 4
                $columns = $this->collection->getColumns(false);
456 4
                if (!isset($columns[$columnIndex])) {
457 2
                    throw new Exception\OutOfBoundsException(
458 2
                        sprintf('Column with the index %d does not exist.', $columnIndex)
459 2
                    );
460
                }
461
462 2
                return $this->collection->map(function (array $row) use ($columnIndex) {
463 2
                    return $row[$columnIndex];
464 2
                });
465
            case PDO::FETCH_OBJ:
466
                $columns = $this->collection->getColumns(false);
467
468
                return $this->collection->map(function (array $row) use ($columns) {
469
                    return $this->getObjectResult($columns, $row);
470
                });
471
472
            default:
473 2
                throw new Exception\UnsupportedException('Unsupported fetch style');
474
        }
475
    }
476
477
    /**
478
     * {@inheritDoc}
479
     */
480 2
    #[\ReturnTypeWillChange]
481
    public function fetchObject($class_name = null, $ctor_args = null)
482
    {
483 2
        throw new Exception\UnsupportedException;
484
    }
485
486
    /**
487
     * {@inheritDoc}
488
     */
489 2
    public function errorCode(): ?string
490
    {
491 2
        return $this->errorCode;
492
    }
493
494
    /**
495
     * {@inheritDoc}
496
     *
497
     * @return array|null
498
     */
499 4
    #[\ReturnTypeWillChange]
500
    public function errorInfo()
501
    {
502 4
        if ($this->errorCode === null) {
503 4
            return null;
504
        }
505
506 4
        switch ($this->errorCode) {
507 2
            case CrateConst::ERR_INVALID_SQL:
508 2
                $ansiErrorCode = '42000';
509 2
                break;
510
511
            default:
512 2
                $ansiErrorCode = 'Not available';
513 2
                break;
514
        }
515
516 4
        return [
517 4
            $ansiErrorCode,
518 4
            $this->errorCode,
519 4
            $this->errorMessage,
520 4
        ];
521
    }
522
523
    /**
524
     * {@inheritDoc}
525
     */
526
    public function setAttribute(int $attribute, $value): bool
527
    {
528
        throw new Exception\UnsupportedException('This driver doesn\'t support setting attributes');
529
    }
530
531
    /**
532
     * {@inheritDoc}
533
     */
534
    #[\ReturnTypeWillChange]
535
    public function getAttribute(int $attribute)
536
    {
537
        throw new Exception\UnsupportedException('This driver doesn\'t support getting attributes');
538
    }
539
540
    /**
541
     * {@inheritDoc}
542
     */
543 4
    public function columnCount(): int
544
    {
545 4
        if (!$this->hasExecuted()) {
546 4
            $this->execute();
547
        }
548
549 4
        return count($this->collection->getColumns(false));
550
    }
551
552
    /**
553
     * {@inheritDoc}
554
     */
555
    #[\ReturnTypeWillChange]
556
    public function getColumnMeta(int $column)
557
    {
558
        throw new Exception\UnsupportedException;
559
    }
560
561
    /**
562
     * {@inheritDoc}
563
     */
564 28
    public function doSetFetchMode($mode, $params = null)
565
    {
566 28
        $args     = func_get_args();
567 28
        $argCount = count($args);
568
569
        switch ($mode) {
570
            case PDO::FETCH_COLUMN:
571 6
                if ($argCount != 2) {
572 2
                    throw new Exception\InvalidArgumentException('fetch mode requires the colno argument');
573
                }
574
575 4
                if (!is_int($params)) {
576 2
                    throw new Exception\InvalidArgumentException('colno must be an integer');
577
                }
578
579 2
                $this->options['fetchMode']   = $mode;
580 2
                $this->options['fetchColumn'] = $params;
581 2
                break;
582
583
            case PDO::FETCH_ASSOC:
584
            case PDO::FETCH_NUM:
585
            case PDO::FETCH_BOTH:
586
            case PDO::FETCH_BOUND:
587
            case PDO::FETCH_NAMED:
588
            case PDO::FETCH_OBJ:
589 20
                if ($params !== null) {
590 10
                    throw new Exception\InvalidArgumentException('fetch mode doesn\'t allow any extra arguments');
591
                }
592
593 10
                $this->options['fetchMode'] = $mode;
594 10
                break;
595
596
            default:
597 2
                throw new Exception\UnsupportedException('Invalid fetch mode specified');
598
        }
599
600 12
        return true;
601
    }
602
603
    /**
604
     * {@inheritDoc}
605
     */
606 4
    public function nextRowset(): bool
607
    {
608 4
        if (!$this->hasExecuted()) {
609 4
            $this->execute();
610
        }
611
612 4
        if (!$this->isSuccessful()) {
613 2
            return false;
614
        }
615
616 2
        $this->collection->next();
617
618 2
        return $this->collection->valid();
619
    }
620
621
    /**
622
     * {@inheritDoc}
623
     */
624 2
    public function closeCursor(): bool
625
    {
626 2
        $this->errorCode  = null;
627 2
        $this->collection = null;
628
629 2
        return true;
630
    }
631
632
    /**
633
     * {@inheritDoc}
634
     */
635 2
    public function debugDumpParams(): ?bool
636
    {
637 2
        throw new Exception\UnsupportedException('Not supported, use var_dump($stmt) instead');
638
    }
639
640
    /**
641
     * {@Inheritdoc}
642
     */
643 2
    public function getIterator(): \Iterator
644
    {
645 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

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