Reader   D
last analyzed

Complexity

Total Complexity 58

Size/Duplication

Total Lines 513
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 163
dl 0
loc 513
rs 4.5599
c 0
b 0
f 0
wmc 58

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getFilters() 0 3 1
A filterBy() 0 9 2
A getAuditsByDate() 0 12 1
A filterByObjectId() 0 10 2
A getAudits() 0 12 1
A getEntities() 0 16 4
A filterByDate() 0 21 6
A getEntityAuditTableName() 0 8 2
B getAuditsQueryBuilder() 0 45 9
A getEntityTableName() 0 3 1
A getAudit() 0 25 1
A filterByTransaction() 0 10 2
A getAuditsCount() 0 13 2
A getEntityManager() 0 3 1
A filterByType() 0 10 2
A __construct() 0 6 1
B checkRoles() 0 37 9
A getAuditsByTransactionHash() 0 17 4
A getConfiguration() 0 3 1
A getAuditsPager() 0 20 4
A checkAuditable() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like Reader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Reader, and based on these observations, apply Extract Interface, too.

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