Passed
Pull Request — master (#172)
by
unknown
05:28
created

AuditReader::getAuditsByDate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
c 1
b 0
f 0
nc 1
nop 8
dl 0
loc 12
rs 10

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