Passed
Push — master ( baa0ce...a7f435 )
by Damien
03:17
created

Query::getLimit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
dl 0
loc 3
c 1
b 0
f 0
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
namespace DH\Auditor\Provider\Doctrine\Persistence\Reader;
4
5
use DH\Auditor\Exception\InvalidArgumentException;
6
use DH\Auditor\Model\Entry;
7
use DH\Auditor\Provider\Doctrine\Persistence\Helper\SchemaHelper;
8
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\DateRangeFilter;
9
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\FilterInterface;
10
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\RangeFilter;
11
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\SimpleFilter;
12
use Doctrine\DBAL\Connection;
13
use Doctrine\DBAL\Query\QueryBuilder;
14
use Doctrine\DBAL\Result;
15
use Exception;
16
17
class Query
18
{
19
    public const TYPE = 'type';
20
    public const CREATED_AT = 'created_at';
21
    public const TRANSACTION_HASH = 'transaction_hash';
22
    public const OBJECT_ID = 'object_id';
23
    public const USER_ID = 'blame_id';
24
    public const ID = 'id';
25
    public const DISCRIMINATOR = 'discriminator';
26
27
    /**
28
     * @var array
29
     */
30
    private $filters = [];
31
32
    /**
33
     * @var array
34
     */
35
    private $orderBy = [];
36
37
    /**
38
     * @var Connection
39
     */
40
    private $connection;
41
42
    /**
43
     * @var string
44
     */
45
    private $table;
46
47
    /**
48
     * @var int
49
     */
50
    private $offset = 0;
51
52
    /**
53
     * @var int
54
     */
55
    private $limit = 0;
56
57
    public function __construct(string $table, Connection $connection)
58
    {
59
        $this->connection = $connection;
60
        $this->table = $table;
61
62
        foreach ($this->getSupportedFilters() as $filterType) {
63
            $this->filters[$filterType] = [];
64
        }
65
    }
66
67
    public function execute(): array
68
    {
69
        $queryBuilder = $this->buildQueryBuilder();
70
        if (method_exists($queryBuilder, 'executeQuery')) {
71
            // doctrine/dbal v3.x
72
            $statement = $queryBuilder->executeQuery();
73
        } else {
74
            // doctrine/dbal v2.13.x
75
            $statement = $queryBuilder->execute();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Query\QueryBuilder::execute() has been deprecated: Use {@link executeQuery()} or {@link executeStatement()} instead. ( Ignorable by Annotation )

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

75
            $statement = /** @scrutinizer ignore-deprecated */ $queryBuilder->execute();

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...
76
        }
77
78
        $result = [];
79
        \assert($statement instanceof Result);
80
        foreach ($statement->fetchAllAssociative() as $row) {
81
            $result[] = Entry::fromArray($row);
82
        }
83
84
        return $result;
85
    }
86
87
    public function count(): int
88
    {
89
        $queryBuilder = $this->buildQueryBuilder();
90
91
        try {
92
            $queryBuilder
93
                ->resetQueryPart('select')
94
                ->resetQueryPart('orderBy')
95
                ->setMaxResults(null)
96
                ->setFirstResult(null)
97
                ->select('COUNT(id)')
98
            ;
99
100
            if (method_exists($queryBuilder, 'executeQuery')) {
101
                // doctrine/dbal v3.x
102
                $result = $queryBuilder
103
                    ->executeQuery()
104
                    ->fetchOne()
105
                ;
106
            } else {
107
                // doctrine/dbal v2.13.x
108
                $result = $queryBuilder
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Query\QueryBuilder::execute() has been deprecated: Use {@link executeQuery()} or {@link executeStatement()} instead. ( Ignorable by Annotation )

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

108
                $result = /** @scrutinizer ignore-deprecated */ $queryBuilder

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...
109
                    ->execute()
110
                    ->fetchColumn(0)
0 ignored issues
show
Bug introduced by
The method fetchColumn() does not exist on Doctrine\DBAL\Result. Did you maybe mean fetchFirstColumn()? ( Ignorable by Annotation )

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

110
                    ->/** @scrutinizer ignore-call */ fetchColumn(0)

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...
111
                ;
112
            }
113
        } catch (Exception $e) {
114
            $result = false;
115
        }
116
117
        return false === $result ? 0 : $result;
118
    }
119
120
    public function addFilter(FilterInterface $filter): self
121
    {
122
        $this->checkFilter($filter->getName());
123
        $this->filters[$filter->getName()][] = $filter;
124
125
        return $this;
126
    }
127
128
    public function addOrderBy(string $field, string $direction = 'DESC'): self
129
    {
130
        $this->checkFilter($field);
131
132
        if (!\in_array($direction, ['ASC', 'DESC'], true)) {
133
            throw new InvalidArgumentException('Invalid sort direction, allowed value: ASC, DESC');
134
        }
135
136
        $this->orderBy[$field] = $direction;
137
138
        return $this;
139
    }
140
141
    public function resetOrderBy(): self
142
    {
143
        $this->orderBy = [];
144
145
        return $this;
146
    }
147
148
    public function limit(int $limit, int $offset = 0): self
149
    {
150
        if (0 > $limit) {
151
            throw new InvalidArgumentException('Limit cannot be negative.');
152
        }
153
        if (0 > $offset) {
154
            throw new InvalidArgumentException('Offset cannot be negative.');
155
        }
156
157
        $this->limit = $limit;
158
        $this->offset = $offset;
159
160
        return $this;
161
    }
162
163
    public function getSupportedFilters(): array
164
    {
165
        return array_keys(SchemaHelper::getAuditTableIndices('fake'));
166
    }
167
168
    public function getFilters(): array
169
    {
170
        return $this->filters;
171
    }
172
173
    public function getOrderBy(): array
174
    {
175
        return $this->orderBy;
176
    }
177
178
    public function getLimit(): array
179
    {
180
        return [$this->limit, $this->offset];
181
    }
182
183
    private function buildQueryBuilder(): QueryBuilder
184
    {
185
        $queryBuilder = $this->connection->createQueryBuilder();
186
        $queryBuilder
187
            ->select('*')
188
            ->from($this->table, 'at')
189
        ;
190
191
        // build WHERE clause(s)
192
        $queryBuilder = $this->buildWhere($queryBuilder);
193
194
        // build ORDER BY part
195
        $queryBuilder = $this->buildOrderBy($queryBuilder);
196
197
        // build LIMIT part
198
        return $this->buildLimit($queryBuilder);
199
    }
200
201
    private function groupFilters(array $filters): array
202
    {
203
        $grouped = [];
204
205
        foreach ($filters as $filter) {
206
            $class = \get_class($filter);
207
            if (!isset($grouped[$class])) {
208
                $grouped[$class] = [];
209
            }
210
            $grouped[$class][] = $filter;
211
        }
212
213
        return $grouped;
214
    }
215
216
    private function mergeSimpleFilters(array $filters): SimpleFilter
217
    {
218
        $merged = [];
219
        $name = null;
220
221
        foreach ($filters as $filter) {
222
            if (null === $name) {
223
                $name = $filter->getName();
224
            }
225
226
            if (\is_array($filter->getValue())) {
227
                $merged = array_merge($merged, $filter->getValue());
228
            } else {
229
                $merged[] = $filter->getValue();
230
            }
231
        }
232
233
        return new SimpleFilter($name, $merged);
234
    }
235
236
    private function buildWhere(QueryBuilder $queryBuilder): QueryBuilder
237
    {
238
        foreach ($this->filters as $name => $rawFilters) {
239
            if (0 === \count($rawFilters)) {
240
                continue;
241
            }
242
243
            // group filters by class
244
            $grouped = $this->groupFilters($rawFilters);
245
246
            foreach ($grouped as $class => $filters) {
247
                switch ($class) {
248
                    case SimpleFilter::class:
249
                        $filters = [$this->mergeSimpleFilters($filters)];
250
251
                        break;
252
                    case RangeFilter::class:
253
                    case DateRangeFilter::class:
254
                        break;
255
                }
256
257
                foreach ($filters as $filter) {
258
                    $data = $filter->getSQL();
259
260
                    $queryBuilder->andWhere($data['sql']);
261
262
                    foreach ($data['params'] as $name => $value) {
0 ignored issues
show
Comprehensibility Bug introduced by
$name is overwriting a variable from outer foreach loop.
Loading history...
263
                        if (\is_array($value)) {
264
                            $queryBuilder->setParameter($name, $value, Connection::PARAM_STR_ARRAY);
265
                        } else {
266
                            $queryBuilder->setParameter($name, $value);
267
                        }
268
                    }
269
                }
270
            }
271
        }
272
273
        return $queryBuilder;
274
    }
275
276
    private function buildOrderBy(QueryBuilder $queryBuilder): QueryBuilder
277
    {
278
        foreach ($this->orderBy as $field => $direction) {
279
            $queryBuilder->addOrderBy($field, $direction);
280
        }
281
282
        return $queryBuilder;
283
    }
284
285
    private function buildLimit(QueryBuilder $queryBuilder): QueryBuilder
286
    {
287
        if (0 < $this->limit) {
288
            $queryBuilder->setMaxResults($this->limit);
289
        }
290
        if (0 < $this->offset) {
291
            $queryBuilder->setFirstResult($this->offset);
292
        }
293
294
        return $queryBuilder;
295
    }
296
297
    private function checkFilter(string $filter): void
298
    {
299
        if (!\in_array($filter, $this->getSupportedFilters(), true)) {
300
            throw new InvalidArgumentException(sprintf('Unsupported "%s" filter, allowed filters: %s.', $filter, implode(', ', $this->getSupportedFilters())));
301
        }
302
    }
303
}
304