Passed
Push — master ( 25b702...de7f7b )
by Damien
04:30
created

Query::execute()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
eloc 9
c 2
b 1
f 1
dl 0
loc 16
rs 9.9666
cc 3
nc 3
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\ForwardCompatibility\DriverStatement;
14
use Doctrine\DBAL\Query\QueryBuilder;
15
use Exception;
16
use PDO;
17
18
class Query
19
{
20
    public const TYPE = 'type';
21
    public const CREATED_AT = 'created_at';
22
    public const TRANSACTION_HASH = 'transaction_hash';
23
    public const OBJECT_ID = 'object_id';
24
    public const USER_ID = 'blame_id';
25
    public const ID = 'id';
26
    public const DISCRIMINATOR = 'discriminator';
27
28
    /**
29
     * @var array
30
     */
31
    private $filters = [];
32
33
    /**
34
     * @var array
35
     */
36
    private $orderBy = [];
37
38
    /**
39
     * @var Connection
40
     */
41
    private $connection;
42
43
    /**
44
     * @var string
45
     */
46
    private $table;
47
48
    /**
49
     * @var int
50
     */
51
    private $offset = 0;
52
53
    /**
54
     * @var int
55
     */
56
    private $limit = 0;
57
58
    public function __construct(string $table, Connection $connection)
59
    {
60
        $this->connection = $connection;
61
        $this->table = $table;
62
63
        foreach ($this->getSupportedFilters() as $filterType) {
64
            $this->filters[$filterType] = [];
65
        }
66
    }
67
68
    public function execute(): array
69
    {
70
        $queryBuilder = $this->buildQueryBuilder();
71
        $statement = $queryBuilder->execute();
72
73
        if ($statement instanceof \Doctrine\DBAL\Result) {
0 ignored issues
show
introduced by
$statement is always a sub-type of Doctrine\DBAL\Result.
Loading history...
74
            $result = [];
75
            foreach ($statement->fetchAllAssociative() as $row) {
76
                $result[] = Entry::fromArray($row);
77
            }
78
            return $result;
79
        }
80
81
        $statement->setFetchMode(PDO::FETCH_CLASS, Entry::class);
82
83
        return $statement->fetchAll();
84
    }
85
86
    public function count(): int
87
    {
88
        $queryBuilder = $this->buildQueryBuilder();
89
90
        try {
91
            $result = $queryBuilder
92
                ->resetQueryPart('select')
93
                ->resetQueryPart('orderBy')
94
                ->setMaxResults(null)
95
                ->setFirstResult(null)
96
                ->select('COUNT(id)')
97
                ->execute();
98
99
            if ($result instanceof \Doctrine\DBAL\Result) {
0 ignored issues
show
introduced by
$result is always a sub-type of Doctrine\DBAL\Result.
Loading history...
100
                $result = $result->fetchOne();
101
            }
102
            else {
103
                $result = $result->fetchColumn(0);
104
            }
105
106
        } catch (Exception $e) {
107
            $result = false;
108
        }
109
110
        return false === $result ? 0 : $result;
111
    }
112
113
    public function addFilter(FilterInterface $filter): self
114
    {
115
        $this->checkFilter($filter->getName());
116
        $this->filters[$filter->getName()][] = $filter;
117
118
        return $this;
119
    }
120
121
    public function addOrderBy(string $field, string $direction = 'DESC'): self
122
    {
123
        $this->checkFilter($field);
124
125
        if (!\in_array($direction, ['ASC', 'DESC'], true)) {
126
            throw new InvalidArgumentException('Invalid sort direction, allowed value: ASC, DESC');
127
        }
128
129
        $this->orderBy[$field] = $direction;
130
131
        return $this;
132
    }
133
134
    public function limit(int $limit, int $offset = 0): self
135
    {
136
        if (0 > $limit) {
137
            throw new InvalidArgumentException('Limit cannot be negative.');
138
        }
139
        if (0 > $offset) {
140
            throw new InvalidArgumentException('Offset cannot be negative.');
141
        }
142
143
        $this->limit = $limit;
144
        $this->offset = $offset;
145
146
        return $this;
147
    }
148
149
    public function getSupportedFilters(): array
150
    {
151
        return array_keys(SchemaHelper::getAuditTableIndices('fake'));
152
    }
153
154
    public function getFilters(): array
155
    {
156
        return $this->filters;
157
    }
158
159
    public function getOrderBy(): array
160
    {
161
        return $this->orderBy;
162
    }
163
164
    public function getLimit(): array
165
    {
166
        return [$this->limit, $this->offset];
167
    }
168
169
    private function buildQueryBuilder(): QueryBuilder
170
    {
171
        $queryBuilder = $this->connection->createQueryBuilder();
172
        $queryBuilder
173
            ->select('*')
174
            ->from($this->table, 'at')
175
        ;
176
177
        // build WHERE clause(s)
178
        $queryBuilder = $this->buildWhere($queryBuilder);
179
180
        // build ORDER BY part
181
        $queryBuilder = $this->buildOrderBy($queryBuilder);
182
183
        // build LIMIT part
184
        return $this->buildLimit($queryBuilder);
185
    }
186
187
    private function groupFilters(array $filters): array
188
    {
189
        $grouped = [];
190
191
        foreach ($filters as $filter) {
192
            $class = \get_class($filter);
193
            if (!isset($grouped[$class])) {
194
                $grouped[$class] = [];
195
            }
196
            $grouped[$class][] = $filter;
197
        }
198
199
        return $grouped;
200
    }
201
202
    private function mergeSimpleFilters(array $filters): SimpleFilter
203
    {
204
        $merged = [];
205
        $name = null;
206
207
        foreach ($filters as $filter) {
208
            if (null === $name) {
209
                $name = $filter->getName();
210
            }
211
212
            if (\is_array($filter->getValue())) {
213
                $merged = array_merge($merged, $filter->getValue());
214
            } else {
215
                $merged[] = $filter->getValue();
216
            }
217
        }
218
219
        return new SimpleFilter($name, $merged);
220
    }
221
222
    private function buildWhere(QueryBuilder $queryBuilder): QueryBuilder
223
    {
224
        foreach ($this->filters as $name => $rawFilters) {
225
            if (0 === \count($rawFilters)) {
226
                continue;
227
            }
228
229
            // group filters by class
230
            $grouped = $this->groupFilters($rawFilters);
231
232
            foreach ($grouped as $class => $filters) {
233
                switch ($class) {
234
                    case SimpleFilter::class:
235
                        $filters = [$this->mergeSimpleFilters($filters)];
236
237
                        break;
238
                    case RangeFilter::class:
239
                    case DateRangeFilter::class:
240
                        break;
241
                }
242
243
                foreach ($filters as $filter) {
244
                    $data = $filter->getSQL();
245
246
                    $queryBuilder->andWhere($data['sql']);
247
248
                    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...
249
                        if (\is_array($value)) {
250
                            $queryBuilder->setParameter($name, $value, Connection::PARAM_STR_ARRAY);
251
                        } else {
252
                            $queryBuilder->setParameter($name, $value);
253
                        }
254
                    }
255
                }
256
            }
257
        }
258
259
        return $queryBuilder;
260
    }
261
262
    private function buildOrderBy(QueryBuilder $queryBuilder): QueryBuilder
263
    {
264
        foreach ($this->orderBy as $field => $direction) {
265
            $queryBuilder->addOrderBy($field, $direction);
266
        }
267
268
        return $queryBuilder;
269
    }
270
271
    private function buildLimit(QueryBuilder $queryBuilder): QueryBuilder
272
    {
273
        if (0 < $this->limit) {
274
            $queryBuilder->setMaxResults($this->limit);
275
        }
276
        if (0 < $this->offset) {
277
            $queryBuilder->setFirstResult($this->offset);
278
        }
279
280
        return $queryBuilder;
281
    }
282
283
    private function checkFilter(string $filter): void
284
    {
285
        if (!\in_array($filter, $this->getSupportedFilters(), true)) {
286
            throw new InvalidArgumentException(sprintf('Unsupported "%s" filter, allowed filters: %s.', $filter, implode(', ', $this->getSupportedFilters())));
287
        }
288
    }
289
}
290