Passed
Push — master ( 486204...15f778 )
by Damien
03:09
created

AuditReader   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 456
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 145
c 5
b 0
f 0
dl 0
loc 456
rs 7.92
wmc 51

19 Methods

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

How to fix   Complexity   

Complex Class

Complex classes like AuditReader 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 AuditReader, and based on these observations, apply Extract Interface, too.

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