Passed
Pull Request — 3.x (#199)
by
unknown
02:17
created

AuditReader::getAuditsPager()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 39
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 24
c 0
b 0
f 0
nc 8
nop 9
dl 0
loc 39
rs 9.536

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