Reader::getAuditsQueryBuilder()   B
last analyzed

Complexity

Conditions 9
Paths 6

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 28
nc 6
nop 8
dl 0
loc 45
rs 8.0555
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace DH\DoctrineAuditBundle\Reader;
4
5
use DateTime;
6
use DH\DoctrineAuditBundle\Annotation\Security;
7
use DH\DoctrineAuditBundle\Configuration;
8
use DH\DoctrineAuditBundle\Exception\AccessDeniedException;
9
use DH\DoctrineAuditBundle\Exception\InvalidArgumentException;
10
use DH\DoctrineAuditBundle\Model\Entry;
11
use DH\DoctrineAuditBundle\User\UserInterface;
12
use Doctrine\DBAL\Connection;
13
use Doctrine\DBAL\Query\QueryBuilder;
14
use Doctrine\DBAL\Statement;
15
use Doctrine\ORM\EntityManagerInterface;
16
use Doctrine\ORM\Mapping\ClassMetadata as ORMMetadata;
17
use PDO;
18
use Symfony\Component\Security\Core\Security as CoreSecurity;
19
20
class Reader
21
{
22
    public const UPDATE = 'update';
23
    public const ASSOCIATE = 'associate';
24
    public const DISSOCIATE = 'dissociate';
25
    public const INSERT = 'insert';
26
    public const REMOVE = 'remove';
27
28
    public const PAGE_SIZE = 50;
29
30
    /**
31
     * @var Configuration
32
     */
33
    private $configuration;
34
35
    /**
36
     * @var EntityManagerInterface
37
     */
38
    private $entityManager;
39
40
    /**
41
     * @var array
42
     */
43
    private $filters = [];
44
45
    /**
46
     * AuditReader constructor.
47
     *
48
     * @param Configuration          $configuration
49
     * @param EntityManagerInterface $entityManager
50
     */
51
    public function __construct(
52
        Configuration $configuration,
53
        EntityManagerInterface $entityManager
54
    ) {
55
        $this->configuration = $configuration;
56
        $this->entityManager = $entityManager;
57
    }
58
59
    /**
60
     * @return Configuration
61
     */
62
    public function getConfiguration(): Configuration
63
    {
64
        return $this->configuration;
65
    }
66
67
    /**
68
     * Set the filter(s) for AuditEntry retrieving.
69
     *
70
     * @param array|string $filter
71
     *
72
     * @return Reader
73
     */
74
    public function filterBy($filter): self
75
    {
76
        $filters = \is_array($filter) ? $filter : [$filter];
77
78
        $this->filters = array_filter($filters, static function ($f) {
79
            return \in_array($f, [self::UPDATE, self::ASSOCIATE, self::DISSOCIATE, self::INSERT, self::REMOVE], true);
80
        });
81
82
        return $this;
83
    }
84
85
    /**
86
     * Returns current filter.
87
     *
88
     * @return array
89
     */
90
    public function getFilters(): array
91
    {
92
        return $this->filters;
93
    }
94
95
    /**
96
     * Returns an array of audit table names indexed by entity FQN.
97
     *
98
     * @throws \Doctrine\ORM\ORMException
99
     *
100
     * @return array
101
     */
102
    public function getEntities(): array
103
    {
104
        $metadataDriver = $this->entityManager->getConfiguration()->getMetadataDriverImpl();
105
        $entities = [];
106
        if (null !== $metadataDriver) {
107
            $entities = $metadataDriver->getAllClassNames();
108
        }
109
        $audited = [];
110
        foreach ($entities as $entity) {
111
            if ($this->configuration->isAuditable($entity)) {
112
                $audited[$entity] = $this->getEntityTableName($entity);
113
            }
114
        }
115
        ksort($audited);
116
117
        return $audited;
118
    }
119
120
    /**
121
     * Returns an array of audited entries/operations.
122
     *
123
     * @param string          $entity
124
     * @param null|int|string $id
125
     * @param null|int        $page
126
     * @param null|int        $pageSize
127
     * @param null|string     $transactionHash
128
     * @param bool            $strict
129
     *
130
     * @throws AccessDeniedException
131
     * @throws InvalidArgumentException
132
     *
133
     * @return array
134
     */
135
    public function getAudits(string $entity, $id = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true): array
136
    {
137
        $this->checkAuditable($entity);
138
        $this->checkRoles($entity, Security::VIEW_SCOPE);
139
140
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize, $transactionHash, $strict);
141
142
        /** @var Statement $statement */
143
        $statement = $queryBuilder->execute();
144
        $statement->setFetchMode(PDO::FETCH_CLASS, Entry::class);
145
146
        return $statement->fetchAll();
147
    }
148
149
    /**
150
     * Returns an array of all audited entries/operations for a given transaction hash
151
     * indexed by entity FQCN.
152
     *
153
     * @param string $transactionHash
154
     *
155
     * @throws InvalidArgumentException
156
     * @throws \Doctrine\ORM\ORMException
157
     *
158
     * @return array
159
     */
160
    public function getAuditsByTransactionHash(string $transactionHash): array
161
    {
162
        $results = [];
163
164
        $entities = $this->getEntities();
165
        foreach ($entities as $entity => $tablename) {
166
            try {
167
                $audits = $this->getAudits($entity, null, null, null, $transactionHash);
168
                if (\count($audits) > 0) {
169
                    $results[$entity] = $audits;
170
                }
171
            } catch (AccessDeniedException $e) {
172
                // acces denied
173
            }
174
        }
175
176
        return $results;
177
    }
178
179
    /**
180
     * Returns an array of audited entries/operations.
181
     *
182
     * @param string          $entity
183
     * @param null|int|string $id
184
     * @param null|DateTime   $startDate       - Expected in configured timezone
185
     * @param null|DateTime   $endDate         - Expected in configured timezone
186
     * @param null|int        $page
187
     * @param null|int        $pageSize
188
     * @param null|string     $transactionHash
189
     * @param bool            $strict
190
     *
191
     * @throws AccessDeniedException
192
     * @throws InvalidArgumentException
193
     *
194
     * @return array
195
     */
196
    public function getAuditsByDate(string $entity, $id = null, ?DateTime $startDate = null, ?DateTime $endDate = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true): array
197
    {
198
        $this->checkAuditable($entity);
199
        $this->checkRoles($entity, Security::VIEW_SCOPE);
200
201
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize, $transactionHash, $strict, $startDate, $endDate);
202
203
        /** @var Statement $statement */
204
        $statement = $queryBuilder->execute();
205
        $statement->setFetchMode(PDO::FETCH_CLASS, Entry::class);
206
207
        return $statement->fetchAll();
208
    }
209
210
    /**
211
     * Returns an array of audited entries/operations.
212
     *
213
     * @param string          $entity
214
     * @param null|int|string $id
215
     * @param int             $page
216
     * @param int             $pageSize
217
     *
218
     * @throws AccessDeniedException
219
     * @throws InvalidArgumentException
220
     *
221
     * @return array
222
     */
223
    public function getAuditsPager(string $entity, $id = null, int $page = 1, int $pageSize = self::PAGE_SIZE): array
224
    {
225
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize);
226
227
        $paginator = new Paginator($queryBuilder);
228
        $numResults = $paginator->count();
229
230
        $currentPage = $page < 1 ? 1 : $page;
231
        $hasPreviousPage = $currentPage > 1;
232
        $hasNextPage = ($currentPage * $pageSize) < $numResults;
233
234
        return [
235
            'results' => $paginator->getIterator(),
236
            'currentPage' => $currentPage,
237
            'hasPreviousPage' => $hasPreviousPage,
238
            'hasNextPage' => $hasNextPage,
239
            'previousPage' => $hasPreviousPage ? $currentPage - 1 : null,
240
            'nextPage' => $hasNextPage ? $currentPage + 1 : null,
241
            'numPages' => (int) ceil($numResults / $pageSize),
242
            'haveToPaginate' => $numResults > $pageSize,
243
        ];
244
    }
245
246
    /**
247
     * Returns the amount of audited entries/operations.
248
     *
249
     * @param string          $entity
250
     * @param null|int|string $id
251
     *
252
     * @throws AccessDeniedException
253
     * @throws InvalidArgumentException
254
     *
255
     * @return int
256
     */
257
    public function getAuditsCount(string $entity, $id = null): int
258
    {
259
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
260
261
        $result = $queryBuilder
262
            ->resetQueryPart('select')
263
            ->resetQueryPart('orderBy')
264
            ->select('COUNT(id)')
265
            ->execute()
266
            ->fetchColumn(0)
267
        ;
268
269
        return false === $result ? 0 : $result;
270
    }
271
272
    /**
273
     * @param string $entity
274
     * @param string $id
275
     *
276
     * @throws AccessDeniedException
277
     * @throws InvalidArgumentException
278
     *
279
     * @return mixed[]
280
     */
281
    public function getAudit(string $entity, $id): array
282
    {
283
        $this->checkAuditable($entity);
284
        $this->checkRoles($entity, Security::VIEW_SCOPE);
285
286
        $connection = $this->entityManager->getConnection();
287
288
        /**
289
         * @var \Doctrine\DBAL\Query\QueryBuilder
290
         */
291
        $queryBuilder = $connection->createQueryBuilder();
292
        $queryBuilder
293
            ->select('*')
294
            ->from($this->getEntityAuditTableName($entity))
295
            ->where('id = :id')
296
            ->setParameter('id', $id)
297
        ;
298
299
        $this->filterByType($queryBuilder, $this->filters);
300
301
        /** @var Statement $statement */
302
        $statement = $queryBuilder->execute();
303
        $statement->setFetchMode(PDO::FETCH_CLASS, Entry::class);
304
305
        return $statement->fetchAll();
306
    }
307
308
    /**
309
     * Returns the table name of $entity.
310
     *
311
     * @param string $entity
312
     *
313
     * @return string
314
     */
315
    public function getEntityTableName(string $entity): string
316
    {
317
        return $this->entityManager->getClassMetadata($entity)->getTableName();
318
    }
319
320
    /**
321
     * Returns the audit table name for $entity.
322
     *
323
     * @param string $entity
324
     *
325
     * @return string
326
     */
327
    public function getEntityAuditTableName(string $entity): string
328
    {
329
        $schema = '';
330
        if ($this->entityManager->getClassMetadata($entity)->getSchemaName()) {
331
            $schema = $this->entityManager->getClassMetadata($entity)->getSchemaName().'.';
332
        }
333
334
        return sprintf('%s%s%s%s', $schema, $this->configuration->getTablePrefix(), $this->getEntityTableName($entity), $this->configuration->getTableSuffix());
335
    }
336
337
    /**
338
     * @return EntityManagerInterface
339
     */
340
    public function getEntityManager(): EntityManagerInterface
341
    {
342
        return $this->entityManager;
343
    }
344
345
    private function filterByType(QueryBuilder $queryBuilder, array $filters): QueryBuilder
346
    {
347
        if (!empty($filters)) {
348
            $queryBuilder
349
                ->andWhere('type IN (:filters)')
350
                ->setParameter('filters', $filters, Connection::PARAM_STR_ARRAY)
351
            ;
352
        }
353
354
        return $queryBuilder;
355
    }
356
357
    private function filterByTransaction(QueryBuilder $queryBuilder, ?string $transactionHash): QueryBuilder
358
    {
359
        if (null !== $transactionHash) {
360
            $queryBuilder
361
                ->andWhere('transaction_hash = :transaction_hash')
362
                ->setParameter('transaction_hash', $transactionHash)
363
            ;
364
        }
365
366
        return $queryBuilder;
367
    }
368
369
    private function filterByDate(QueryBuilder $queryBuilder, ?DateTime $startDate, ?DateTime $endDate): QueryBuilder
370
    {
371
        if (null !== $startDate && null !== $endDate && $endDate < $startDate) {
372
            throw new \InvalidArgumentException('$endDate must be greater than $startDate.');
373
        }
374
375
        if (null !== $startDate) {
376
            $queryBuilder
377
                ->andWhere('created_at >= :start_date')
378
                ->setParameter('start_date', $startDate->format('Y-m-d H:i:s'))
379
            ;
380
        }
381
382
        if (null !== $endDate) {
383
            $queryBuilder
384
                ->andWhere('created_at <= :end_date')
385
                ->setParameter('end_date', $endDate->format('Y-m-d H:i:s'))
386
            ;
387
        }
388
389
        return $queryBuilder;
390
    }
391
392
    /**
393
     * @param QueryBuilder    $queryBuilder
394
     * @param null|int|string $id
395
     *
396
     * @return QueryBuilder
397
     */
398
    private function filterByObjectId(QueryBuilder $queryBuilder, $id): QueryBuilder
399
    {
400
        if (null !== $id) {
401
            $queryBuilder
402
                ->andWhere('object_id = :object_id')
403
                ->setParameter('object_id', $id)
404
            ;
405
        }
406
407
        return $queryBuilder;
408
    }
409
410
    /**
411
     * Returns an array of audited entries/operations.
412
     *
413
     * @param string          $entity
414
     * @param null|int|string $id
415
     * @param null|int        $page
416
     * @param null|int        $pageSize
417
     * @param null|string     $transactionHash
418
     * @param bool            $strict
419
     * @param null|DateTime   $startDate
420
     * @param null|DateTime   $endDate
421
     *
422
     * @throws AccessDeniedException
423
     * @throws InvalidArgumentException
424
     *
425
     * @return QueryBuilder
426
     */
427
    private function getAuditsQueryBuilder(string $entity, $id = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true, ?DateTime $startDate = null, ?DateTime $endDate = null): QueryBuilder
428
    {
429
        $this->checkAuditable($entity);
430
        $this->checkRoles($entity, Security::VIEW_SCOPE);
431
432
        if (null !== $page && $page < 1) {
433
            throw new \InvalidArgumentException('$page must be greater or equal than 1.');
434
        }
435
436
        if (null !== $pageSize && $pageSize < 1) {
437
            throw new \InvalidArgumentException('$pageSize must be greater or equal than 1.');
438
        }
439
440
        $storage = $this->configuration->getEntityManager() ?? $this->entityManager;
441
        $connection = $storage->getConnection();
442
443
        $queryBuilder = $connection->createQueryBuilder();
444
        $queryBuilder
445
            ->select('*')
446
            ->from($this->getEntityAuditTableName($entity), 'at')
447
            ->orderBy('created_at', 'DESC')
448
            ->addOrderBy('id', 'DESC')
449
        ;
450
451
        $metadata = $this->entityManager->getClassMetadata($entity);
452
        if ($strict && $metadata instanceof ORMMetadata && ORMMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $metadata->inheritanceType) {
453
            $queryBuilder
454
                ->andWhere('discriminator = :discriminator')
455
                ->setParameter('discriminator', $entity)
456
            ;
457
        }
458
459
        $this->filterByObjectId($queryBuilder, $id);
460
        $this->filterByType($queryBuilder, $this->filters);
461
        $this->filterByTransaction($queryBuilder, $transactionHash);
462
        $this->filterByDate($queryBuilder, $startDate, $endDate);
463
464
        if (null !== $pageSize) {
465
            $queryBuilder
466
                ->setFirstResult(($page - 1) * $pageSize)
467
                ->setMaxResults($pageSize)
468
            ;
469
        }
470
471
        return $queryBuilder;
472
    }
473
474
    /**
475
     * Throws an InvalidArgumentException if given entity is not auditable.
476
     *
477
     * @param string $entity
478
     *
479
     * @throws InvalidArgumentException
480
     */
481
    private function checkAuditable(string $entity): void
482
    {
483
        if (!$this->configuration->isAuditable($entity)) {
484
            throw new InvalidArgumentException('Entity '.$entity.' is not auditable.');
485
        }
486
    }
487
488
    /**
489
     * Throws an AccessDeniedException if user not is granted to access audits for the given entity.
490
     *
491
     * @param string $entity
492
     * @param string $scope
493
     *
494
     * @throws AccessDeniedException
495
     */
496
    private function checkRoles(string $entity, string $scope): void
497
    {
498
        $userProvider = $this->configuration->getUserProvider();
499
        $user = null === $userProvider ? null : $userProvider->getUser();
500
        $security = null === $userProvider ? null : $userProvider->getSecurity();
501
502
        if (!($user instanceof UserInterface) || !($security instanceof CoreSecurity)) {
503
            // If no security defined or no user identified, consider access granted
504
            return;
505
        }
506
507
        $entities = $this->configuration->getEntities();
508
509
        $roles = $entities[$entity]['roles'] ?? null;
510
511
        if (null === $roles) {
512
            // If no roles are configured, consider access granted
513
            return;
514
        }
515
516
        $scope = $roles[$scope] ?? null;
517
518
        if (null === $scope) {
519
            // If no roles for the given scope are configured, consider access granted
520
            return;
521
        }
522
523
        // roles are defined for the give scope
524
        foreach ($scope as $role) {
525
            if ($security->isGranted($role)) {
526
                // role granted => access granted
527
                return;
528
            }
529
        }
530
531
        // access denied
532
        throw new AccessDeniedException('You are not allowed to access audits of '.$entity.' entity.');
533
    }
534
}
535