ExtendedPdoAdapter::getNbResults()   A
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 5
eloc 14
c 3
b 0
f 0
nc 8
nop 0
dl 0
loc 24
rs 9.4888
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ray\AuraSqlModule\Pagerfanta;
6
7
use Aura\Sql\ExtendedPdoInterface;
8
use Override;
9
use Pagerfanta\Adapter\AdapterInterface;
10
11
use function assert;
12
use function count;
13
use function is_array;
14
use function is_int;
15
use function preg_match;
16
use function preg_replace;
17
use function preg_split;
18
use function strpos;
19
use function strtolower;
20
use function trim;
21
22
use const PHP_EOL;
23
24
/**
25
 * @template T
26
 * @implements AdapterInterface<T>
27
 */
28
final class ExtendedPdoAdapter implements AdapterInterface
29
{
30
    private readonly FetcherInterface $fetcher;
31
32
    /**
33
     * @param array<mixed> $params
34
     *
35
     * @psalm-taint-sink sql $sql
36
     */
37
    public function __construct(
38
        private readonly ExtendedPdoInterface $pdo,
39
        private readonly string $sql,
40
        private readonly array $params,
41
        ?FetcherInterface $fetcher = null
42
    ) {
43
        $this->fetcher = $fetcher ?? new FetchAssoc($this->pdo);
0 ignored issues
show
Bug introduced by
The property fetcher is declared read-only in Ray\AuraSqlModule\Pagerfanta\ExtendedPdoAdapter.
Loading history...
44
    }
45
46
    /**
47
     * {@inheritDoc}
48
     *
49
     * @SuppressWarnings(PHPMD.GotoStatement) // @phpstan-ignore-line
50
     */
51
    #[Override]
52
    public function getNbResults(): int
53
    {
54
        // be smart and try to guess the total number of records
55
        $countQuery = $this->rewriteCountQuery($this->sql);
56
        if (! $countQuery) {
57
            // GROUP BY => fetch the whole result set and count the rows returned
58
            $result = $this->pdo->perform($this->sql, $this->params)->fetchAll();
59
            $count = ! $result ? 0 : count($result);
60
            goto ret;
61
        }
62
63
        if ($this->params) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->params of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
64
            $count = $this->pdo->fetchValue($countQuery, $this->params);
65
            goto ret;
66
        }
67
68
        $count = $this->pdo->fetchValue($countQuery);
69
        ret:
70
        /** @var string $count */
71
        $nbResult = ! $count ? 0 : (int) $count;
72
        assert($nbResult >= 0);
73
74
        return $nbResult;
75
    }
76
77
    /**
78
     * {@inheritDoc}
79
     *
80
     * @param int $offset
81
     * @param int $length
82
     *
83
     * @return array<mixed>
84
     */
85
    #[Override]
86
    public function getSlice(int $offset, int $length): iterable
87
    {
88
        $sql = $this->sql . $this->getLimitClause($offset, $length);
89
        $result = ($this->fetcher)($sql, $this->params);
90
91
        return ! $result ? [] : $result;
92
    }
93
94
    /**
95
     * {@inheritDoc}
96
     */
97
    public function getLimitClause(int $offset, int $length): string
98
    {
99
        if ($offset && $length) {
100
            return PHP_EOL . "LIMIT {$length} OFFSET {$offset}";
101
        }
102
103
        if ($length) {
104
            return PHP_EOL . "LIMIT {$length}";
105
        }
106
107
        return '';
108
    }
109
110
    /**
111
     * Return count query
112
     *
113
     * @param string $query
114
     *
115
     * @return string
116
     *
117
     * @see https://github.com/pear/Pager/blob/master/examples/Pager_Wrapper.php
118
     * Taken from pear/pager and modified.
119
     * tested at https://github.com/pear/Pager/blob/80c0e31c8b94f913cfbdeccbe83b63822f42a2f8/tests/pager_wrapper_test.php#L19
120
     * @codeCoverageIgnore
121
     */
122
    public function rewriteCountQuery($query)
123
    {
124
        if (is_int(strpos(strtolower($query), 'union'))) {
0 ignored issues
show
introduced by
The condition is_int(strpos(strtolower($query), 'union')) is always true.
Loading history...
125
            return '';
126
        }
127
128
        if (preg_match('/^\s*SELECT\s+\bDISTINCT\b/is', $query) || preg_match('/\s+GROUP\s+BY\s+/is', $query)) {
129
            return '';
130
        }
131
132
        $openParenthesis = '(?:\()';
133
        $closeParenthesis = '(?:\))';
134
        $subQueryInSelect = $openParenthesis . '.*\bFROM\b.*' . $closeParenthesis;
135
        $pattern = '/(?:.*' . $subQueryInSelect . '.*)\bFROM\b\s+/Uims';
136
        if (preg_match($pattern, $query)) {
137
            return '';
138
        }
139
140
        $subQueryWithLimitOrder = $openParenthesis . '.*\b(LIMIT|ORDER)\b.*' . $closeParenthesis;
141
        $pattern = '/.*\bFROM\b.*(?:.*' . $subQueryWithLimitOrder . '.*).*/Uims';
142
        if (preg_match($pattern, $query)) {
143
            return '';
144
        }
145
146
        $queryCount = preg_replace('/(?:.*)\bFROM\b\s+/Uims', 'SELECT COUNT(*) FROM ', $query, 1);
147
        $split = preg_split('/\s+ORDER\s+BY\s+/is', (string) $queryCount);
148
        assert(is_array($split), 'preg_split() should return an array');
149
        [$queryCount] = $split;
150
        $split2 = preg_split('/\bLIMIT\b/is', (string) $queryCount);
151
        assert(is_array($split2), 'preg_split() should return an array');
152
        [$queryCount2] = $split2;
153
154
        return trim((string) $queryCount2);
155
    }
156
}
157