Passed
Pull Request — 3.x (#201)
by Giso
02:52
created

AuditReader::getAuditsByDate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 8
dl 0
loc 12
rs 10
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\AuditConfiguration;
8
use DH\DoctrineAuditBundle\Exception\AccessDeniedException;
9
use DH\DoctrineAuditBundle\Exception\InvalidArgumentException;
10
use DH\DoctrineAuditBundle\User\UserInterface;
11
use Doctrine\Common\Collections\ArrayCollection;
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
     * @var ArrayCollection
46
     */
47
    private $conditions;
48
49
    /**
50
     * AuditReader constructor.
51
     *
52
     * @param AuditConfiguration     $configuration
53
     * @param EntityManagerInterface $entityManager
54
     */
55
    public function __construct(
56
        AuditConfiguration $configuration,
57
        EntityManagerInterface $entityManager
58
    ) {
59
        $this->configuration = $configuration;
60
        $this->entityManager = $entityManager;
61
62
        $this->conditions    = new ArrayCollection();
63
    }
64
65
    /**
66
     * @return AuditConfiguration
67
     */
68
    public function getConfiguration(): AuditConfiguration
69
    {
70
        return $this->configuration;
71
    }
72
73
    /**
74
     * Set the filter(s) for AuditEntry retrieving.
75
     *
76
     * @param array|string $filter
77
     *
78
     * @return AuditReader
79
     */
80
    public function filterBy($filter): self
81
    {
82
        $filters = \is_array($filter) ? $filter : [$filter];
83
84
        $this->filters = array_filter($filters, static function ($f) {
85
            return \in_array($f, [self::UPDATE, self::ASSOCIATE, self::DISSOCIATE, self::INSERT, self::REMOVE], true);
86
        });
87
88
        return $this;
89
    }
90
91
    /**
92
     * Add a condition (if it was not yet added)
93
     *
94
     * @param ConditionInterface $condition
95
     *
96
     * @return Reader
0 ignored issues
show
Bug introduced by
The type DH\DoctrineAuditBundle\Reader\Reader was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
97
     */
98
    public function addCondition(ConditionInterface $condition): self
99
    {
100
        if (!$this->conditions->contains($condition)) {
101
            $this->conditions->add($condition);
102
        }
103
104
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type DH\DoctrineAuditBundle\Reader\AuditReader which is incompatible with the documented return type DH\DoctrineAuditBundle\Reader\Reader.
Loading history...
105
    }
106
107
    /**
108
     * Check if a condition was set
109
     *
110
     * @param ConditionInterface $condition
111
     *
112
     * @return bool
113
     */
114
    public function hasCondition(ConditionInterface $condition): bool
115
    {
116
        return $this->conditions->contains($condition);
117
    }
118
119
    /**
120
     * Set all conditions
121
     *
122
     * @param ArrayCollection $conditions
123
     *
124
     * @return Reader
125
     */
126
    public function setConditions(ArrayCollection $conditions): self
127
    {
128
        $this->conditions = $conditions;
129
130
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type DH\DoctrineAuditBundle\Reader\AuditReader which is incompatible with the documented return type DH\DoctrineAuditBundle\Reader\Reader.
Loading history...
131
    }
132
133
    /**
134
     * Remove a Condition and returns the removed condition
135
     *
136
     * @param ConditionInterface $condition
137
     * @return ConditionInterface
138
     */
139
    public function removeCondition(ConditionInterface $condition): ConditionInterface
140
    {
141
        if (!$this->conditions->contains($condition)) {
142
            throw new \RuntimeException('Can not remove a condition that was not set');
143
        }
144
145
        $index = $this->conditions->indexOf($condition);
146
147
        return $this->conditions->remove($index);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->conditions->remove($index) could return the type null which is incompatible with the type-hinted return DH\DoctrineAuditBundle\Reader\ConditionInterface. Consider adding an additional type-check to rule them out.
Loading history...
148
    }
149
150
    /**
151
     * Returns current filter.
152
     *
153
     * @return array
154
     */
155
    public function getFilters(): array
156
    {
157
        return $this->filters;
158
    }
159
160
    /**
161
     * Returns an array of audit table names indexed by entity FQN.
162
     *
163
     * @throws \Doctrine\ORM\ORMException
164
     *
165
     * @return array
166
     */
167
    public function getEntities(): array
168
    {
169
        $metadataDriver = $this->entityManager->getConfiguration()->getMetadataDriverImpl();
170
        $entities = [];
171
        if (null !== $metadataDriver) {
172
            $entities = $metadataDriver->getAllClassNames();
173
        }
174
        $audited = [];
175
        foreach ($entities as $entity) {
176
            if ($this->configuration->isAuditable($entity)) {
177
                $audited[$entity] = $this->getEntityTableName($entity);
178
            }
179
        }
180
        ksort($audited);
181
182
        return $audited;
183
    }
184
185
    /**
186
     * Returns an array of audited entries/operations.
187
     *
188
     * @param string          $entity
189
     * @param null|int|string $id
190
     * @param null|int        $page
191
     * @param null|int        $pageSize
192
     * @param null|string     $transactionHash
193
     * @param bool            $strict
194
     *
195
     * @throws AccessDeniedException
196
     * @throws InvalidArgumentException
197
     *
198
     * @return array
199
     */
200
    public function getAudits(string $entity, $id = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true): array
201
    {
202
        $this->checkAuditable($entity);
203
        $this->checkRoles($entity, Security::VIEW_SCOPE);
204
205
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize, $transactionHash, $strict);
206
207
        /** @var Statement $statement */
208
        $statement = $queryBuilder->execute();
209
        $statement->setFetchMode(PDO::FETCH_CLASS, AuditEntry::class);
210
211
        return $statement->fetchAll();
212
    }
213
214
    /**
215
     * Returns an array of all audited entries/operations for a given transaction hash
216
     * indexed by entity FQCN.
217
     *
218
     * @param string $transactionHash
219
     *
220
     * @throws InvalidArgumentException
221
     * @throws \Doctrine\ORM\ORMException
222
     *
223
     * @return array
224
     */
225
    public function getAuditsByTransactionHash(string $transactionHash): array
226
    {
227
        $results = [];
228
229
        $entities = $this->getEntities();
230
        foreach ($entities as $entity => $tablename) {
231
            try {
232
                $audits = $this->getAudits($entity, null, null, null, $transactionHash);
233
                if (\count($audits) > 0) {
234
                    $results[$entity] = $audits;
235
                }
236
            } catch (AccessDeniedException $e) {
237
                // acces denied
238
            }
239
        }
240
241
        return $results;
242
    }
243
244
    /**
245
     * Returns an array of audited entries/operations.
246
     *
247
     * @param string          $entity
248
     * @param null|int|string $id
249
     * @param null|DateTime   $startDate       - Expected in configured timezone
250
     * @param null|DateTime   $endDate         - Expected in configured timezone
251
     * @param null|int        $page
252
     * @param null|int        $pageSize
253
     * @param null|string     $transactionHash
254
     * @param bool            $strict
255
     *
256
     * @throws AccessDeniedException
257
     * @throws InvalidArgumentException
258
     *
259
     * @return array
260
     */
261
    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
262
    {
263
        $this->checkAuditable($entity);
264
        $this->checkRoles($entity, Security::VIEW_SCOPE);
265
266
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize, $transactionHash, $strict, $startDate, $endDate);
267
268
        /** @var Statement $statement */
269
        $statement = $queryBuilder->execute();
270
        $statement->setFetchMode(PDO::FETCH_CLASS, AuditEntry::class);
271
272
        return $statement->fetchAll();
273
    }
274
275
    /**
276
     * Returns an array of audited entries/operations.
277
     *
278
     * @param string          $entity
279
     * @param null|int|string $id
280
     * @param int             $page
281
     * @param int             $pageSize
282
     *
283
     * @throws AccessDeniedException
284
     * @throws InvalidArgumentException
285
     *
286
     * @return array
287
     */
288
    public function getAuditsPager(string $entity, $id = null, int $page = 1, int $pageSize = self::PAGE_SIZE): array
289
    {
290
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id, $page, $pageSize);
291
292
        $paginator = new Paginator($queryBuilder);
293
        $numResults = $paginator->count();
294
295
        $currentPage = $page < 1 ? 1 : $page;
296
        $hasPreviousPage = $currentPage > 1;
297
        $hasNextPage = ($currentPage * $pageSize) < $numResults;
298
299
        return [
300
            'results' => $paginator->getIterator(),
301
            'currentPage' => $currentPage,
302
            'hasPreviousPage' => $hasPreviousPage,
303
            'hasNextPage' => $hasNextPage,
304
            'previousPage' => $hasPreviousPage ? $currentPage - 1 : null,
305
            'nextPage' => $hasNextPage ? $currentPage + 1 : null,
306
            'numPages' => (int) ceil($numResults / $pageSize),
307
            'haveToPaginate' => $numResults > $pageSize,
308
        ];
309
    }
310
311
    /**
312
     * Returns the amount of audited entries/operations.
313
     *
314
     * @param string          $entity
315
     * @param null|int|string $id
316
     *
317
     * @throws AccessDeniedException
318
     * @throws InvalidArgumentException
319
     *
320
     * @return int
321
     */
322
    public function getAuditsCount(string $entity, $id = null): int
323
    {
324
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
325
326
        $result = $queryBuilder
327
            ->resetQueryPart('select')
328
            ->resetQueryPart('orderBy')
329
            ->select('COUNT(id)')
330
            ->execute()
331
            ->fetchColumn(0)
332
        ;
333
334
        return false === $result ? 0 : $result;
335
    }
336
337
    /**
338
     * @param string $entity
339
     * @param string $id
340
     *
341
     * @throws AccessDeniedException
342
     * @throws InvalidArgumentException
343
     *
344
     * @return mixed[]
345
     */
346
    public function getAudit(string $entity, $id): array
347
    {
348
        $this->checkAuditable($entity);
349
        $this->checkRoles($entity, Security::VIEW_SCOPE);
350
351
        $connection = $this->entityManager->getConnection();
352
353
        /**
354
         * @var \Doctrine\DBAL\Query\QueryBuilder
355
         */
356
        $queryBuilder = $connection->createQueryBuilder();
357
        $queryBuilder
358
            ->select('*')
359
            ->from($this->getEntityAuditTableName($entity))
360
            ->where('id = :id')
361
            ->setParameter('id', $id)
362
        ;
363
364
        $this->filterByType($queryBuilder, $this->filters);
365
366
        /** @var Statement $statement */
367
        $statement = $queryBuilder->execute();
368
        $statement->setFetchMode(PDO::FETCH_CLASS, AuditEntry::class);
369
370
        return $statement->fetchAll();
371
    }
372
373
    /**
374
     * Returns the table name of $entity.
375
     *
376
     * @param string $entity
377
     *
378
     * @return string
379
     */
380
    public function getEntityTableName(string $entity): string
381
    {
382
        return $this->entityManager->getClassMetadata($entity)->getTableName();
383
    }
384
385
    /**
386
     * Returns the audit table name for $entity.
387
     *
388
     * @param string $entity
389
     *
390
     * @return string
391
     */
392
    public function getEntityAuditTableName(string $entity): string
393
    {
394
        $schema = '';
395
        if ($this->entityManager->getClassMetadata($entity)->getSchemaName()) {
396
            $schema = $this->entityManager->getClassMetadata($entity)->getSchemaName().'.';
397
        }
398
399
        return sprintf('%s%s%s%s', $schema, $this->configuration->getTablePrefix(), $this->getEntityTableName($entity), $this->configuration->getTableSuffix());
400
    }
401
402
    /**
403
     * @return EntityManagerInterface
404
     */
405
    public function getEntityManager(): EntityManagerInterface
406
    {
407
        return $this->entityManager;
408
    }
409
410
    private function filterByType(QueryBuilder $queryBuilder, array $filters): QueryBuilder
411
    {
412
        (new TypesCondition($filters))->apply($queryBuilder);
413
414
        return $queryBuilder;
415
    }
416
417
    private function filterByTransaction(QueryBuilder $queryBuilder, ?string $transactionHash): QueryBuilder
418
    {
419
        (new TransactionCondition($transactionHash))->apply($queryBuilder);
420
421
        return $queryBuilder;
422
    }
423
424
    private function filterByDate(QueryBuilder $queryBuilder, ?DateTime $startDate, ?DateTime $endDate): QueryBuilder
425
    {
426
        (new DateRangeCondition($startDate, $endDate))->apply($queryBuilder);
427
428
        return $queryBuilder;
429
    }
430
431
    /**
432
     * @param QueryBuilder    $queryBuilder
433
     * @param null|int|string $id
434
     *
435
     * @return QueryBuilder
436
     */
437
    private function filterByObjectId(QueryBuilder $queryBuilder, $id): QueryBuilder
438
    {
439
        (new ObjectIdCondition($id))->apply($queryBuilder);
440
441
        return $queryBuilder;
442
    }
443
444
    /**
445
     * Returns an array of audited entries/operations.
446
     *
447
     * @param string          $entity
448
     * @param null|int|string $id
449
     * @param null|int        $page
450
     * @param null|int        $pageSize
451
     * @param null|string     $transactionHash
452
     * @param bool            $strict
453
     * @param null|DateTime   $startDate
454
     * @param null|DateTime   $endDate
455
     *
456
     * @throws AccessDeniedException
457
     * @throws InvalidArgumentException
458
     *
459
     * @return QueryBuilder
460
     */
461
    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
462
    {
463
        $this->checkAuditable($entity);
464
        $this->checkRoles($entity, Security::VIEW_SCOPE);
465
466
        if (null !== $page && $page < 1) {
467
            throw new \InvalidArgumentException('$page must be greater or equal than 1.');
468
        }
469
470
        if (null !== $pageSize && $pageSize < 1) {
471
            throw new \InvalidArgumentException('$pageSize must be greater or equal than 1.');
472
        }
473
474
        $storage = $this->configuration->getEntityManager() ?? $this->entityManager;
475
        $connection = $storage->getConnection();
476
477
        $queryBuilder = $connection->createQueryBuilder();
478
        $queryBuilder
479
            ->select('*')
480
            ->from($this->getEntityAuditTableName($entity), 'at')
481
            ->orderBy('created_at', 'DESC')
482
            ->addOrderBy('id', 'DESC')
483
        ;
484
485
        $metadata = $this->entityManager->getClassMetadata($entity);
486
        if ($strict && $metadata instanceof ORMMetadata && ORMMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $metadata->inheritanceType) {
487
            $queryBuilder
488
                ->andWhere('discriminator = :discriminator')
489
                ->setParameter('discriminator', $entity)
490
            ;
491
        }
492
493
        $this->applyArgumentFilters($queryBuilder, $id, $transactionHash, $startDate, $endDate);
494
        $this->applyConditions($queryBuilder);
495
496
        if (null !== $pageSize) {
497
            $queryBuilder
498
                ->setFirstResult(($page - 1) * $pageSize)
499
                ->setMaxResults($pageSize)
500
            ;
501
        }
502
503
        return $queryBuilder;
504
    }
505
506
    /**
507
     * Throws an InvalidArgumentException if given entity is not auditable.
508
     *
509
     * @param string $entity
510
     *
511
     * @throws InvalidArgumentException
512
     */
513
    private function checkAuditable(string $entity): void
514
    {
515
        if (!$this->configuration->isAuditable($entity)) {
516
            throw new InvalidArgumentException('Entity '.$entity.' is not auditable.');
517
        }
518
    }
519
520
    /**
521
     * Throws an AccessDeniedException if user not is granted to access audits for the given entity.
522
     *
523
     * @param string $entity
524
     * @param string $scope
525
     *
526
     * @throws AccessDeniedException
527
     */
528
    private function checkRoles(string $entity, string $scope): void
529
    {
530
        $userProvider = $this->configuration->getUserProvider();
531
        $user = null === $userProvider ? null : $userProvider->getUser();
532
        $security = null === $userProvider ? null : $userProvider->getSecurity();
533
534
        if (!($user instanceof UserInterface) || !($security instanceof CoreSecurity)) {
535
            // If no security defined or no user identified, consider access granted
536
            return;
537
        }
538
539
        $entities = $this->configuration->getEntities();
540
541
        $roles = $entities[$entity]['roles'] ?? null;
542
543
        if (null === $roles) {
544
            // If no roles are configured, consider access granted
545
            return;
546
        }
547
548
        $scope = $roles[$scope] ?? null;
549
550
        if (null === $scope) {
551
            // If no roles for the given scope are configured, consider access granted
552
            return;
553
        }
554
555
        // roles are defined for the give scope
556
        foreach ($scope as $role) {
557
            if ($security->isGranted($role)) {
558
                // role granted => access granted
559
                return;
560
            }
561
        }
562
563
        // access denied
564
        throw new AccessDeniedException('You are not allowed to access audits of '.$entity.' entity.');
565
    }
566
567
    private function applyConditions(QueryBuilder $queryBuilder)
568
    {
569
        foreach ($this->conditions as $condition) {
570
            $condition->apply($queryBuilder);
571
        }
572
    }
573
574
    /**
575
     * @param QueryBuilder $queryBuilder
576
     * @param $id
577
     * @param string|null $transactionHash
578
     * @param DateTime|null $startDate
579
     * @param DateTime|null $endDate
580
     */
581
    private function applyArgumentFilters(
582
        QueryBuilder $queryBuilder,
583
        $id,
584
        ?string $transactionHash,
585
        ?DateTime $startDate,
586
        ?DateTime $endDate
587
    ): void {
588
        if (isset($id)) {
589
            $this->filterByObjectId($queryBuilder, $id);
590
        }
591
        if (isset($this->filters)) {
592
            $this->filterByType($queryBuilder, $this->filters);
593
        }
594
        if (isset($transactionHash)) {
595
            $this->filterByTransaction($queryBuilder, $transactionHash);
596
        }
597
        if (isset($startDate) && isset($endDate)) {
598
            $this->filterByDate($queryBuilder, $startDate, $endDate);
599
        }
600
    }
601
}
602