Passed
Push — master ( 514ad2...01110a )
by Damien
03:39
created

Query::buildQueryBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
dl 0
loc 16
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 limit(int $limit, int $offset = 0): self
142
    {
143
        if (0 > $limit) {
144
            throw new InvalidArgumentException('Limit cannot be negative.');
145
        }
146
        if (0 > $offset) {
147
            throw new InvalidArgumentException('Offset cannot be negative.');
148
        }
149
150
        $this->limit = $limit;
151
        $this->offset = $offset;
152
153
        return $this;
154
    }
155
156
    public function getSupportedFilters(): array
157
    {
158
        return array_keys(SchemaHelper::getAuditTableIndices('fake'));
159
    }
160
161
    public function getFilters(): array
162
    {
163
        return $this->filters;
164
    }
165
166
    public function getOrderBy(): array
167
    {
168
        return $this->orderBy;
169
    }
170
171
    public function getLimit(): array
172
    {
173
        return [$this->limit, $this->offset];
174
    }
175
176
    private function buildQueryBuilder(): QueryBuilder
177
    {
178
        $queryBuilder = $this->connection->createQueryBuilder();
179
        $queryBuilder
180
            ->select('*')
181
            ->from($this->table, 'at')
182
        ;
183
184
        // build WHERE clause(s)
185
        $queryBuilder = $this->buildWhere($queryBuilder);
186
187
        // build ORDER BY part
188
        $queryBuilder = $this->buildOrderBy($queryBuilder);
189
190
        // build LIMIT part
191
        return $this->buildLimit($queryBuilder);
192
    }
193
194
    private function groupFilters(array $filters): array
195
    {
196
        $grouped = [];
197
198
        foreach ($filters as $filter) {
199
            $class = \get_class($filter);
200
            if (!isset($grouped[$class])) {
201
                $grouped[$class] = [];
202
            }
203
            $grouped[$class][] = $filter;
204
        }
205
206
        return $grouped;
207
    }
208
209
    private function mergeSimpleFilters(array $filters): SimpleFilter
210
    {
211
        $merged = [];
212
        $name = null;
213
214
        foreach ($filters as $filter) {
215
            if (null === $name) {
216
                $name = $filter->getName();
217
            }
218
219
            if (\is_array($filter->getValue())) {
220
                $merged = array_merge($merged, $filter->getValue());
221
            } else {
222
                $merged[] = $filter->getValue();
223
            }
224
        }
225
226
        return new SimpleFilter($name, $merged);
227
    }
228
229
    private function buildWhere(QueryBuilder $queryBuilder): QueryBuilder
230
    {
231
        foreach ($this->filters as $name => $rawFilters) {
232
            if (0 === \count($rawFilters)) {
233
                continue;
234
            }
235
236
            // group filters by class
237
            $grouped = $this->groupFilters($rawFilters);
238
239
            foreach ($grouped as $class => $filters) {
240
                switch ($class) {
241
                    case SimpleFilter::class:
242
                        $filters = [$this->mergeSimpleFilters($filters)];
243
244
                        break;
245
                    case RangeFilter::class:
246
                    case DateRangeFilter::class:
247
                        break;
248
                }
249
250
                foreach ($filters as $filter) {
251
                    $data = $filter->getSQL();
252
253
                    $queryBuilder->andWhere($data['sql']);
254
255
                    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...
256
                        if (\is_array($value)) {
257
                            $queryBuilder->setParameter($name, $value, Connection::PARAM_STR_ARRAY);
258
                        } else {
259
                            $queryBuilder->setParameter($name, $value);
260
                        }
261
                    }
262
                }
263
            }
264
        }
265
266
        return $queryBuilder;
267
    }
268
269
    private function buildOrderBy(QueryBuilder $queryBuilder): QueryBuilder
270
    {
271
        foreach ($this->orderBy as $field => $direction) {
272
            $queryBuilder->addOrderBy($field, $direction);
273
        }
274
275
        return $queryBuilder;
276
    }
277
278
    private function buildLimit(QueryBuilder $queryBuilder): QueryBuilder
279
    {
280
        if (0 < $this->limit) {
281
            $queryBuilder->setMaxResults($this->limit);
282
        }
283
        if (0 < $this->offset) {
284
            $queryBuilder->setFirstResult($this->offset);
285
        }
286
287
        return $queryBuilder;
288
    }
289
290
    private function checkFilter(string $filter): void
291
    {
292
        if (!\in_array($filter, $this->getSupportedFilters(), true)) {
293
            throw new InvalidArgumentException(sprintf('Unsupported "%s" filter, allowed filters: %s.', $filter, implode(', ', $this->getSupportedFilters())));
294
        }
295
    }
296
}
297