Passed
Branch v0.1 (e1d14a)
by Nestor
01:33
created

Statement::fetchAsArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cozy\Database\Relational;
6
7
class Statement
8
{
9
    /** @var \PDOStatement */
10
    protected $pdoStatement;
11
    protected $wasExecuted = false;
12
    protected $wasExecutedSuccessfully = false;
13
    protected $autoExecuteEnabled = true;
14
15
    /**
16
     * Statement constructor that wraps a PDOStatement object.
17
     *
18
     * @param \PDOStatement $pdoStatement
19
     */
20
    public function __construct(\PDOStatement $pdoStatement)
21
    {
22
        $this->pdoStatement = $pdoStatement;
23
    }
24
25
    /**
26
     * Returns the wrapped PDO statement object.
27
     *
28
     * @return \PDOStatement
29
     */
30
    public function getPdoStatement(): \PDOStatement
31
    {
32
        return $this->pdoStatement;
33
    }
34
35
    /**
36
     * Set a statement attribute.
37
     *
38
     * @param int $attribute
39
     * @param mixed $value
40
     * @return bool TRUE on success or FALSE on failure.
41
     */
42
    public function setAttribute(int $attribute, $value)
43
    {
44
        return $this->pdoStatement->setAttribute($attribute, $value);
45
    }
46
47
    /**
48
     * Retrieve a statement attribute.
49
     *
50
     * @param int $attribute
51
     * @return mixed The attribute value.
52
     */
53
    public function getAttribute(int $attribute)
54
    {
55
        return $this->pdoStatement->getAttribute($attribute);
56
    }
57
58
    /**
59
     * Returns error information about the last operation performed by this statement.
60
     *
61
     * @return array
62
     */
63
    public function getErrorInfo(): array
64
    {
65
        return $this->pdoStatement->errorInfo();
66
    }
67
68
    /**
69
     * Binds a value to a parameter.
70
     *
71
     * @param mixed $parameter Parameter identifier.
72
     * @param mixed $value The value to bind to the parameter.
73
     * @param mixed $type [optional] Explicit data type for the parameter.
74
     * @return $this
75
     * @throws Exception
76
     */
77
    public function bindValue($parameter, $value, $type = 'str')
78
    {
79
        $pdo_type = \PDO::PARAM_STR;
80
81
        if ($type === 'int' || $type === 'integer' || $type === \PDO::PARAM_INT) {
82
            $pdo_type = \PDO::PARAM_INT;
83
        } elseif ($type === 'bool' || $type === 'boolean' || $type === \PDO::PARAM_BOOL) {
84
            $pdo_type = \PDO::PARAM_BOOL;
85
        } elseif ($type === 'lob' || $type === 'blob' || $type === \PDO::PARAM_LOB) {
86
            $pdo_type = \PDO::PARAM_LOB;
87
        } elseif ($type === 'null' || $type === \PDO::PARAM_NULL || $value === null) {
88
            $pdo_type = \PDO::PARAM_NULL;
89
        }
90
91
        if (!$this->pdoStatement->bindValue($parameter, $value, $pdo_type)) {
92
            throw new Exception(
93
                "Error binding invalid parameter [{$parameter}], it was not defined.",
94
                $this->pdoStatement->errorCode(),
95
                $this->pdoStatement->errorInfo(),
96
                $this->pdoStatement->queryString
97
            );
98
        }
99
100
        return $this;
101
    }
102
103
    /**
104
     * Bind a column to a PHP variable.
105
     *
106
     * @param mixed $column Number of the column (1-indexed) or name of the column in the result set. If using the
107
     *                      column name, be aware that the name should match the case of the column, as returned by
108
     *                      the driver.
109
     * @param mixed $param Name of the PHP variable to which the column will be bound.
110
     * @param mixed $type [optional] Data type of the parameter, specified by the PDO::PARAM_* constants.
111
     * @return $this
112
     * @throws Exception
113
     */
114
    public function bindColumn($column, &$param, $type = null)
115
    {
116
        $pdo_type = \PDO::PARAM_STR;
117
118
        if ($type === 'int' || $type === 'integer' || $type === \PDO::PARAM_INT) {
119
            $pdo_type = \PDO::PARAM_INT;
120
        } elseif ($type === 'bool' || $type === 'boolean' || $type === \PDO::PARAM_BOOL) {
121
            $pdo_type = \PDO::PARAM_BOOL;
122
        } elseif ($type === 'lob' || $type === 'blob' || $type === \PDO::PARAM_LOB) {
123
            $pdo_type = \PDO::PARAM_LOB;
124
        }
125
126
        if (!$this->pdoStatement->bindColumn($column, $param, $pdo_type)) {
127
            throw new Exception(
128
                "Error binding invalid column [{$column}], it was not defined.",
129
                $this->pdoStatement->errorCode(),
130
                $this->pdoStatement->errorInfo(),
131
                $this->pdoStatement->queryString
132
            );
133
        }
134
135
        return $this;
136
    }
137
    /**
138
     * Define if the statement will execute automatically when trying to fetch data.
139
     *
140
     * @param bool $flag
141
     * @return $this
142
     */
143
    public function setAutoExecute(bool $flag)
144
    {
145
        $this->autoExecuteEnabled = $flag;
146
147
        return $this;
148
    }
149
150
    /**
151
     * Execute the statement.
152
     *
153
     * @return bool TRUE on success or FALSE on failure.
154
     * @throws Exception
155
     */
156
    public function execute(): bool
157
    {
158
        try {
159
            $this->wasExecuted = true;
160
161
            if ($this->pdoStatement->execute()) {
162
                $this->wasExecutedSuccessfully = true;
163
                return true;
164
            }
165
166
            return false;
167
        } catch (\PDOException $e) {
168
            throw new Exception(
169
                $e->getMessage(),
170
                $e->getCode(),
171
                $this->pdoStatement->errorInfo(),
172
                $this->pdoStatement->queryString
173
            );
174
        }
175
    }
176
177
    /**
178
     * Returns the number of rows affected by the SQL statement.
179
     * If there is no result set, returns 0.
180
     *
181
     * @return int
182
     */
183
    public function getRowCount(): int
184
    {
185
        return $this->pdoStatement->rowCount();
186
    }
187
188
    /**
189
     * Returns the number of columns in the result set.
190
     * If there is no result set, returns 0.
191
     *
192
     * @return int
193
     */
194
    public function getColumnCount(): int
195
    {
196
        return $this->pdoStatement->columnCount();
197
    }
198
199
    /**
200
     * Returns metadata for a column in a result set.
201
     * Returns FALSE if the requested column does not exist in the result set, or if no result set exists.
202
     *
203
     * @param int $columnNumber
204
     * @return array|false
205
     */
206
    public function getColumnMeta(int $columnNumber)
207
    {
208
        return $this->pdoStatement->getColumnMeta($columnNumber);
209
    }
210
211
    /**
212
     * Closes the cursor, enabling the statement to be executed again.
213
     *
214
     * @return bool
215
     * @throws Exception
216
     */
217
    public function closeCursor(): bool
218
    {
219
        try {
220
            if ($this->pdoStatement->closeCursor()) {
221
                $this->wasExecuted = false;
222
223
                return true;
224
            }
225
226
            return false;
227
        } catch (\PDOException $e) {
228
            throw new Exception(
229
                $e->getMessage(),
230
                $e->getCode(),
231
                $this->pdoStatement->errorInfo(),
232
                $this->pdoStatement->queryString
233
            );
234
        }
235
    }
236
237
    /**
238
     * Advances to the next rowset in a multi-rowset statement handle.
239
     *
240
     * @return bool TRUE on success or FALSE on failure.
241
     */
242
    public function nextRowset()
243
    {
244
        try {
245
            return $this->pdoStatement->nextRowset();
246
        } catch (\PDOException $e) {
247
            throw new Exception(
248
                $e->getMessage(),
249
                $e->getCode(),
250
                $this->pdoStatement->errorInfo(),
251
                $this->pdoStatement->queryString
252
            );
253
        }
254
    }
255
256
    /**
257
     * Fetches the next row from a result set according to cursor.
258
     *
259
     * @return mixed|null
260
     * @throws Exception
261
     */
262
    private function internalFetch()
263
    {
264
        try {
265
            // Auto execute block
266
267
            if ($this->autoExecuteEnabled && !$this->wasExecuted) {
268
                $this->execute();
269
            }
270
271
            // Validate previous execution
272
273
            if (!$this->wasExecutedSuccessfully) {
274
                throw new Exception(
275
                    'Fetching without previous successful execution.',
276
                    'CZ001',
277
                    [],
278
                    $this->pdoStatement->queryString
279
                );
280
            }
281
282
            // Fetch the row
283
284
            $row = $this->pdoStatement->fetch();
285
286
            // Return result
287
288
            if ($row === false && $this->pdoStatement->errorCode() === '00000') {
289
                return null;
290
            }
291
292
            return $row;
293
        } catch (\PDOException $e) {
294
            throw new Exception(
295
                $e->getMessage(),
296
                (string)$e->getCode(),
297
                $this->pdoStatement->errorInfo(),
298
                $this->pdoStatement->queryString
299
            );
300
        }
301
    }
302
303
    // CUSTOM FETCH METHODS
304
305
    /**
306
     * Fetches a row from the result set and assigns the values of the columns to the PHP variables to which
307
     * they were bound with the bindColumn() method.
308
     *
309
     * @return bool
310
     * @throws Exception
311
     */
312
    public function fetchBound()
313
    {
314
        $this->pdoStatement->setFetchMode(\PDO::FETCH_BOUND);
315
316
        return $this->internalFetch();
317
    }
318
319
    /**
320
     * Fetches a row from the result set and returns the following:
321
     * - An associative array, if data was found.
322
     * - Null, if there is no data.
323
     * - False, if there was an error.
324
     *
325
     * @return mixed|null
326
     * @throws Exception
327
     */
328
    public function fetchAsArray()
329
    {
330
        $this->pdoStatement->setFetchMode(\PDO::FETCH_ASSOC);
331
332
        return $this->internalFetch();
333
    }
334
335
    /**
336
     * Fetches a row from the result set and returns the following:
337
     * - An object, if data was found.
338
     * - Null, if there is no data.
339
     * - False, if there was an error.
340
     *
341
     * @param string $className Name of the created class.
342
     * @param array|null $classArguments Elements of this array are passed to the constructor.
343
     * @return mixed|null
344
     * @throws Exception
345
     */
346
    public function fetchAsObject(string $className = 'stdClass', array $classArguments = null)
347
    {
348
        // Validate arguments
349
350
        if (!class_exists($className)) {
351
            throw new \InvalidArgumentException('The argument $className points to a nonexistent class.');
352
        }
353
354
        // Fetch the row as object
355
356
        $this->pdoStatement->setFetchMode(\PDO::FETCH_CLASS, $className, (array)$classArguments);
357
358
        return $this->internalFetch();
359
    }
360
361
    /**
362
     * Fetches a row from the result set and updates an existing object, mapping the columns as named properties.
363
     *
364
     * @param object $object Object to update.
365
     * @return object|bool
366
     * @throws Exception
367
     */
368
    public function fetchIntoObject($object)
369
    {
370
        // Validations
371
372
        if (!is_object($object)) {
373
            throw new \InvalidArgumentException('The argument $object is not a valid object.');
374
        }
375
376
        // Fetch the row as object
377
378
        $this->pdoStatement->setFetchMode(\PDO::FETCH_INTO, $object);
379
380
        return $this->internalFetch();
381
    }
382
383
    /**
384
     * Returns the value of a single column from the next row of the result set.
385
     *
386
     * @param string $column Name of column you wish to retrieve.
387
     * @return mixed
388
     * @throws Exception
389
     */
390
    public function fetchAsColumn(string $column)
391
    {
392
        // Validate arguments
393
394
        if ($column === '') {
395
            throw new \InvalidArgumentException('The argument $column is empty.');
396
        }
397
398
        // Fetch the row
399
400
        $row = $this->fetchAsArray();
401
402
        // More validations
403
404
        if ($row === false || $row === null) {
405
            return $row;
406
        }
407
408
        if (!isset($row[$column])) {
409
            throw new Exception(
410
                "The column '{$column}' is not present in the result set.",
411
                'CZ002',
412
                [],
413
                $this->pdoStatement->queryString
414
            );
415
        }
416
417
        // Return result
418
419
        return $row[$column];
420
    }
421
422
    /**
423
     * Returns an array containing values of a single column retrieved from the result set rows.
424
     *
425
     * @param string $column Name of column you wish to retrieve.
426
     * @param string $index_by Name of the column you want to assign as a row key.
427
     * @return array|bool
428
     * @throws Exception
429
     */
430
    public function fetchAllAsColumn(string $column, string $index_by = null)
431
    {
432
        try {
433
            // Validations
434
435
            if ($column == '') {
436
                throw new \InvalidArgumentException('The argument $column is not a valid string.');
437
            }
438
439
            if (isset($index_by) && $index_by == '') {
440
                throw new \InvalidArgumentException('The argument $index_by is not a valid string.');
441
            }
442
443
            // Auto execute block
444
445
            if ($this->autoExecuteEnabled && !$this->wasExecuted) {
446
                $this->execute();
447
            }
448
449
            // Set initial values
450
451
            $result = [];
452
            $row = $this->pdoStatement->fetch(\PDO::FETCH_ASSOC);
453
454
            // More validations
455
456
            if ($row === false) {
457
                $this->pdoStatement->closeCursor();
458
459
                if ($this->pdoStatement->errorCode() === '00000') {
460
                    return null;
461
                }
462
463
                return false;
464
            }
465
466
            if (!isset($row[$column])) {
467
                throw new Exception(
468
                    "The column '{$column}' is not present in the result set.",
469
                    'CZ002',
470
                    [],
471
                    $this->pdoStatement->queryString
472
                );
473
            }
474
475
            if ($index_by && !isset($row[$index_by])) {
476
                throw new Exception(
477
                    "The column '{$index_by}' is not present in the result set.",
478
                    'CZ002',
479
                    [],
480
                    $this->pdoStatement->queryString
481
                );
482
            }
483
484
            // Traversing the remaining rows
485
486
            while ($row) {
487
                if ($index_by) {
488
                    $result[$row[$index_by]] = $row[$column];
489
                } else {
490
                    $result[] = $row[$column];
491
                }
492
493
                $row = $this->pdoStatement->fetch(\PDO::FETCH_ASSOC);
494
            }
495
496
            // Clear state and return result
497
498
            $this->pdoStatement->closeCursor();
499
500
            return $result;
501
        } catch (\PDOException $e) {
502
            throw new Exception(
503
                $e->getMessage(),
504
                (string)$e->getCode(),
505
                $this->pdoStatement->errorInfo(),
506
                $this->pdoStatement->queryString
507
            );
508
        }
509
    }
510
511
    /**
512
     * Returns an associative array containing all of the result set.
513
     *
514
     * @param string $index_by Name of the column you want to assign as a row key.
515
     * @param string $group_by Name of the columns with which you want to group the result. You can include
516
     *                         maximum 3 columns by separating them with commas.
517
     * @return array|bool
518
     * @throws Exception
519
     */
520
    public function fetchAllAsArray(string $index_by = null, string $group_by = null)
521
    {
522
        try {
523
            // Validations
524
525
            if (isset($index_by) && $index_by == '') {
526
                throw new \InvalidArgumentException('The argument $index_by is not a valid string.');
527
            }
528
529
            if (isset($group_by) && $group_by == '') {
530
                throw new \InvalidArgumentException('The argument $group_by is not a valid string.');
531
            }
532
533
            // Auto execute block
534
535
            if ($this->autoExecuteEnabled && !$this->wasExecuted) {
536
                $this->execute();
537
            }
538
539
            // Set initial values
540
541
            $result = [];
542
            $group_by_count = 0;
543
            $row = $this->pdoStatement->fetch(\PDO::FETCH_ASSOC);
544
545
            // More validations
546
547
            if ($row === false) {
548
                $this->pdoStatement->closeCursor();
549
550
                if ($this->pdoStatement->errorCode() === '00000') {
551
                    return null;
552
                }
553
554
                return false;
555
            }
556
557
            if ($index_by && !array_key_exists($index_by, $row)) {
558
                throw new Exception(
559
                    "The column '{$index_by}' is not present in the result set.",
560
                    'CZ002',
561
                    [],
562
                    $this->pdoStatement->queryString
563
                );
564
            }
565
566
            if ($group_by) {
567
                $group_by = explode(',', str_replace(' ', '', $group_by));
568
                $group_by_count = count($group_by);
569
570
                if ($group_by_count > 3) {
571
                    throw new \InvalidArgumentException('You have exceeded the limit of 3 columns to group-by.');
572
                }
573
574
                foreach ($group_by as $column) {
575
                    $column_err = [];
576
577
                    if (!array_key_exists($column, $row)) {
578
                        $column_err[] = $column;
579
                    }
580
581
                    if ($column_err) {
582
                        throw new Exception(
583
                            'Some columns to group-by (' . implode(', ', $column_err) .
584
                            ') are not present in the result set.',
585
                            'CZ002',
586
                            [],
587
                            $this->pdoStatement->queryString
588
                        );
589
                    }
590
                }
591
            }
592
593
            // Traversing the remaining rows
594
595
            while ($row) {
596
                if (!$index_by && !$group_by) {
597
                    $result[] = $row;
598
                } elseif ($index_by && !$group_by) {
599
                    $result[$row[$index_by]] = $row;
600
                } elseif ($index_by && $group_by) {
601
                    switch ($group_by_count) {
602
                        case 3:
603
                            $result[$row[$group_by[0]]][$row[$group_by[1]]][$row[$group_by[2]]][$row[$index_by]] = $row;
604
                            break;
605
                        case 2:
606
                            $result[$row[$group_by[0]]][$row[$group_by[1]]][$row[$index_by]] = $row;
607
                            break;
608
                        case 1:
609
                            $result[$row[$group_by[0]]][$row[$index_by]] = $row;
610
                            break;
611
                    }
612
                } elseif (!$index_by && $group_by) {
613
                    switch ($group_by_count) {
614
                        case 3:
615
                            $result[$row[$group_by[0]]][$row[$group_by[1]]][$row[$group_by[2]]][] = $row;
616
                            break;
617
                        case 2:
618
                            $result[$row[$group_by[0]]][$row[$group_by[1]]][] = $row;
619
                            break;
620
                        case 1:
621
                            $result[$row[$group_by[0]]][] = $row;
622
                            break;
623
                    }
624
                }
625
626
                $row = $this->pdoStatement->fetch(\PDO::FETCH_ASSOC);
627
            }
628
629
            // Clear state and return result
630
631
            $this->pdoStatement->closeCursor();
632
633
            return $result;
634
        } catch (\PDOException $e) {
635
            throw new Exception(
636
                $e->getMessage(),
637
                (string)$e->getCode(),
638
                $this->pdoStatement->errorInfo(),
639
                $this->pdoStatement->queryString
640
            );
641
        }
642
    }
643
644
    /**
645
     * Returns an array of objects containing all of the result set.
646
     *
647
     * @param string $class_name Name of the class you want to instantiate.
648
     * @param array $class_arguments Elements of this array are passed to the constructor of the class instantiated.
649
     * @param string $index_by Name of the column you want to assign as a row key.
650
     * @param string $group_by Name of the columns with which you want to group the result. You can include
651
     *                         maximum 3 columns by separating them with commas.
652
     * @return array|bool
653
     * @throws Exception
654
     */
655
    public function fetchAllAsObject(
656
        string $class_name = 'stdClass',
657
        array $class_arguments = null,
658
        string $index_by = null,
659
        string $group_by = null
660
    ) {
661
        try {
662
            // Validations
663
664
            if ($class_name === '') {
665
                throw new \InvalidArgumentException('The argument $class_name is not a valid string.');
666
            }
667
668
            if (isset($index_by) && $index_by == '') {
669
                throw new \InvalidArgumentException('The argument $index_by is not a valid string.');
670
            }
671
672
            if (isset($group_by) && $group_by == '') {
673
                throw new \InvalidArgumentException('The argument $group_by is not a valid string.');
674
            }
675
676
            // Auto execute block
677
678
            if ($this->autoExecuteEnabled && !$this->wasExecuted) {
679
                $this->execute();
680
            }
681
682
            // Set initial values
683
684
            $result = [];
685
            $groupByCount = 0;
686
            $row = $this->pdoStatement->fetchObject($class_name, (array)$class_arguments);
687
688
            // More validations
689
690
            if ($row === false) {
691
                $this->pdoStatement->closeCursor();
692
693
                if ($this->pdoStatement->errorCode() === '00000') {
694
                    return null;
695
                }
696
697
                return false;
698
            }
699
700
            if ($index_by && !property_exists($row, $index_by)) {
701
                throw new Exception(
702
                    "The column '{$index_by}' is not present in the result set.",
703
                    'CZ002',
704
                    [],
705
                    $this->pdoStatement->queryString
706
                );
707
            }
708
709
            if ($group_by) {
710
                $group_by = explode(',', str_replace(' ', '', $group_by));
711
                $groupByCount = count($group_by);
712
713
                if ($groupByCount > 3) {
714
                    throw new \InvalidArgumentException(
715
                        'You have exceeded the limit of 3 columns to group-by.'
716
                    );
717
                }
718
719
                foreach ($group_by as $column) {
720
                    $column_err = [];
721
722
                    if (!property_exists($row, $column)) {
723
                        $column_err[] = $column;
724
                    }
725
726
                    if ($column_err) {
727
                        throw new Exception(
728
                            'Some columns to group-by (' . implode(', ', $column_err) .
729
                            ') are not present in the result set.',
730
                            'CZ002',
731
                            [],
732
                            $this->pdoStatement->queryString
733
                        );
734
                    }
735
                }
736
            }
737
738
            // Traversing the remaining rows
739
740
            while ($row) {
741
                if (!$index_by && !$group_by) {
742
                    $result[] = $row;
743
                } elseif ($index_by && !$group_by) {
744
                    $result[$row->{$index_by}] = $row;
745
                } elseif ($index_by && $group_by) {
746
                    switch ($groupByCount) {
747
                        case 3:
748
                            $temp = [
749
                                $row->{$group_by[2]} => [
750
                                    $row->{$index_by} => $row
751
                                ]
752
                            ];
753
                            $result[$row->{$group_by[0]}][$row->{$group_by[1]}] = $temp;
754
// $result[$row->{$group_by[0]}][$row->{$group_by[1]}] >>>>
755
// <<<< [$row->{$group_by[2]}][$row->{$index_by}] = $row;
756
                            break;
757
                        case 2:
758
                            $result[$row->{$group_by[0]}][$row->{$group_by[1]}][$row->{$index_by}] = $row;
759
                            break;
760
                        case 1:
761
                            $result[$row->{$group_by[0]}][$row->{$index_by}] = $row;
762
                            break;
763
                    }
764
                } elseif (!$index_by && $group_by) {
765
                    switch ($groupByCount) {
766
                        case 3:
767
                            $result[$row->{$group_by[0]}][$row->{$group_by[1]}][$row->{$group_by[2]}][] = $row;
768
                            break;
769
                        case 2:
770
                            $result[$row->{$group_by[0]}][$row->{$group_by[1]}][] = $row;
771
                            break;
772
                        case 1:
773
                            $result[$row->{$group_by[0]}][] = $row;
774
                            break;
775
                    }
776
                }
777
778
                $row = $this->pdoStatement->fetchObject($class_name, (array)$class_arguments);
779
            }
780
781
            // Clear state and return result
782
783
            $this->pdoStatement->closeCursor();
784
785
            return $result;
786
        } catch (\PDOException $e) {
787
            throw new Exception(
788
                $e->getMessage(),
789
                (string)$e->getCode(),
790
                $this->pdoStatement->errorInfo(),
791
                $this->pdoStatement->queryString
792
            );
793
        }
794
    }
795
}
796