Passed
Push — master ( e7036c...5b2684 )
by Damien
03:25
created

AuditReader   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 439
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 135
c 5
b 0
f 0
dl 0
loc 439
rs 8.4
wmc 50

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getConfiguration() 0 3 1
A filterBy() 0 9 2
A getFilter() 0 3 1
A getAuditsByTransactionHash() 0 17 4
A getEntities() 0 16 4
A getAudits() 0 12 1
A getAuditsPager() 0 13 1
A getClassMetadata() 0 5 1
B checkRoles() 0 28 9
A getAudit() 0 30 2
A getAuditsCount() 0 13 2
A checkAuditable() 0 4 2
A getEntityTableName() 0 4 1
A getEntityManager() 0 3 1
A selectStorage() 0 3 1
A getEntityAuditTableName() 0 6 3
C getAuditsQueryBuilder() 0 61 13

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\AuditConfiguration;
6
use DH\DoctrineAuditBundle\Exception\AccessDeniedException;
7
use DH\DoctrineAuditBundle\Exception\InvalidArgumentException;
8
use DH\DoctrineAuditBundle\User\UserInterface;
9
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
10
use Doctrine\DBAL\Query\QueryBuilder;
11
use Doctrine\DBAL\Statement;
12
use Doctrine\ORM\EntityManagerInterface;
13
use Doctrine\ORM\Mapping\ClassMetadata as ORMMetadata;
14
use Pagerfanta\Adapter\DoctrineDbalSingleTableAdapter;
15
use Pagerfanta\Pagerfanta;
16
use Symfony\Component\Security\Core\Security;
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 ?string
40
     */
41
    private $filter;
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 for AuditEntry retrieving.
67
     *
68
     * @param string $filter
69
     *
70
     * @return AuditReader
71
     */
72
    public function filterBy(string $filter): self
73
    {
74
        if (!\in_array($filter, [self::UPDATE, self::ASSOCIATE, self::DISSOCIATE, self::INSERT, self::REMOVE], true)) {
75
            $this->filter = null;
76
        } else {
77
            $this->filter = $filter;
78
        }
79
80
        return $this;
81
    }
82
83
    /**
84
     * Returns current filter.
85
     *
86
     * @return null|string
87
     */
88
    public function getFilter(): ?string
89
    {
90
        return $this->filter;
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 object|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 null|bool       $strict
127
     *
128
     * @throws AccessDeniedException
129
     * @throws InvalidArgumentException
130
     *
131
     * @return array
132
     */
133
    public function getAudits($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);
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 $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 Pagerfanta
189
     */
190
    public function getAuditsPager($entity, $id = null, int $page = 1, int $pageSize = self::PAGE_SIZE): Pagerfanta
191
    {
192
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
193
194
        $adapter = new DoctrineDbalSingleTableAdapter($queryBuilder, 'at.id');
195
196
        $pagerfanta = new Pagerfanta($adapter);
197
        $pagerfanta
198
            ->setMaxPerPage($pageSize)
199
            ->setCurrentPage($page)
200
        ;
201
202
        return $pagerfanta;
203
    }
204
205
    /**
206
     * Returns the amount of audited entries/operations.
207
     *
208
     * @param $entity
209
     * @param null|int|string $id
210
     *
211
     * @throws AccessDeniedException
212
     * @throws InvalidArgumentException
213
     *
214
     * @return int
215
     */
216
    public function getAuditsCount($entity, $id = null): int
217
    {
218
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
219
220
        $result = $queryBuilder
221
            ->resetQueryPart('select')
222
            ->resetQueryPart('orderBy')
223
            ->select('COUNT(id)')
224
            ->execute()
225
            ->fetchColumn(0)
226
        ;
227
228
        return false === $result ? 0 : $result;
229
    }
230
231
    /**
232
     * Returns an array of audited entries/operations.
233
     *
234
     * @param $entity
235
     * @param null|int|string $id
236
     * @param null|int        $page
237
     * @param null|int        $pageSize
238
     * @param null|string     $transactionHash
239
     * @param null|bool       $strict
240
     *
241
     * @throws AccessDeniedException
242
     * @throws InvalidArgumentException
243
     *
244
     * @return QueryBuilder
245
     */
246
    private function getAuditsQueryBuilder($entity, $id = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true): QueryBuilder
247
    {
248
        $this->checkAuditable($entity);
249
        $this->checkRoles($entity);
250
251
        if (null !== $page && $page < 1) {
252
            throw new \InvalidArgumentException('$page must be greater or equal than 1.');
253
        }
254
255
        if (null !== $pageSize && $pageSize < 1) {
256
            throw new \InvalidArgumentException('$pageSize must be greater or equal than 1.');
257
        }
258
259
        $storage = $this->selectStorage();
260
        $connection = $storage->getConnection();
261
262
        $queryBuilder = $connection->createQueryBuilder();
263
        $queryBuilder
264
            ->select('*')
265
            ->from($this->getEntityAuditTableName($entity), 'at')
266
            ->orderBy('created_at', 'DESC')
267
            ->addOrderBy('id', 'DESC')
268
        ;
269
270
        $metadata = $this->getClassMetadata($entity);
271
        if ($strict && $metadata instanceof ORMMetadata && ORMMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $metadata->inheritanceType) {
272
            $queryBuilder
273
                ->andWhere('discriminator = :discriminator')
274
                ->setParameter('discriminator', \is_object($entity) ? \get_class($entity) : $entity)
275
            ;
276
        }
277
278
        if (null !== $pageSize) {
279
            $queryBuilder
280
                ->setFirstResult(($page - 1) * $pageSize)
281
                ->setMaxResults($pageSize)
282
            ;
283
        }
284
285
        if (null !== $id) {
286
            $queryBuilder
287
                ->andWhere('object_id = :object_id')
288
                ->setParameter('object_id', $id)
289
            ;
290
        }
291
292
        if (null !== $this->filter) {
293
            $queryBuilder
294
                ->andWhere('type = :filter')
295
                ->setParameter('filter', $this->filter)
296
            ;
297
        }
298
299
        if (null !== $transactionHash) {
300
            $queryBuilder
301
                ->andWhere('transaction_hash = :transaction_hash')
302
                ->setParameter('transaction_hash', $transactionHash)
303
            ;
304
        }
305
306
        return $queryBuilder;
307
    }
308
309
    /**
310
     * @param $entity
311
     * @param $id
312
     *
313
     * @throws AccessDeniedException
314
     * @throws InvalidArgumentException
315
     *
316
     * @return mixed[]
317
     */
318
    public function getAudit($entity, $id)
319
    {
320
        $this->checkAuditable($entity);
321
        $this->checkRoles($entity);
322
323
        $connection = $this->entityManager->getConnection();
324
325
        /**
326
         * @var \Doctrine\DBAL\Query\QueryBuilder
327
         */
328
        $queryBuilder = $connection->createQueryBuilder();
329
        $queryBuilder
330
            ->select('*')
331
            ->from($this->getEntityAuditTableName($entity))
332
            ->where('id = :id')
333
            ->setParameter('id', $id)
334
        ;
335
336
        if (null !== $this->filter) {
337
            $queryBuilder
338
                ->andWhere('type = :filter')
339
                ->setParameter('filter', $this->filter)
340
            ;
341
        }
342
343
        /** @var Statement $statement */
344
        $statement = $queryBuilder->execute();
345
        $statement->setFetchMode(\PDO::FETCH_CLASS, AuditEntry::class);
346
347
        return $statement->fetchAll();
348
    }
349
350
    /**
351
     * @param $entity
352
     *
353
     * @return ClassMetadata
354
     */
355
    private function getClassMetadata($entity): ClassMetadata
356
    {
357
        return $this
358
            ->entityManager
359
            ->getClassMetadata($entity)
360
        ;
361
    }
362
363
    /**
364
     * Returns the table name of $entity.
365
     *
366
     * @param object|string $entity
367
     *
368
     * @return string
369
     */
370
    public function getEntityTableName($entity): string
371
    {
372
        return $this->getClassMetadata($entity)
373
            ->getTableName()
374
        ;
375
    }
376
377
    /**
378
     * Returns the audit table name for $entity.
379
     *
380
     * @param mixed $entity
381
     *
382
     * @return string
383
     */
384
    public function getEntityAuditTableName($entity): string
385
    {
386
        $entityName = \is_string($entity) ? $entity : \get_class($entity);
387
        $schema = $this->getClassMetadata($entityName)->getSchemaName() ? $this->getClassMetadata($entityName)->getSchemaName().'.' : '';
388
389
        return sprintf('%s%s%s%s', $schema, $this->configuration->getTablePrefix(), $this->getEntityTableName($entityName), $this->configuration->getTableSuffix());
390
    }
391
392
    /**
393
     * @return EntityManagerInterface
394
     */
395
    private function selectStorage(): EntityManagerInterface
396
    {
397
        return $this->configuration->getEntityManager() ?? $this->entityManager;
398
    }
399
400
    /**
401
     * @return EntityManagerInterface
402
     */
403
    public function getEntityManager(): EntityManagerInterface
404
    {
405
        return $this->entityManager;
406
    }
407
408
    /**
409
     * Throws an InvalidArgumentException if given entity is not auditable.
410
     *
411
     * @param $entity
412
     *
413
     * @throws InvalidArgumentException
414
     */
415
    private function checkAuditable($entity): void
416
    {
417
        if (!$this->configuration->isAuditable($entity)) {
418
            throw new InvalidArgumentException('Entity '.$entity.' is not auditable.');
419
        }
420
    }
421
422
    /**
423
     * Throws an AccessDeniedException if user not is granted to access audits for the given entity.
424
     *
425
     * @param $entity
426
     *
427
     * @throws AccessDeniedException
428
     */
429
    private function checkRoles($entity): void
430
    {
431
        $userProvider = $this->configuration->getUserProvider();
432
        $user = null === $userProvider ? null : $userProvider->getUser();
433
        $security = null === $userProvider ? null : $userProvider->getSecurity();
434
435
        if (!($user instanceof UserInterface) || !($security instanceof Security)) {
436
            // If no security defined or no user identified, consider access granted
437
            return;
438
        }
439
440
        $entities = $this->configuration->getEntities();
441
442
        if (!isset($entities[$entity]['roles']) || null === $entities[$entity]['roles']) {
443
            // If no roles are configured, consider access granted
444
            return;
445
        }
446
447
        // roles are defined
448
        foreach ($entities[$entity]['roles'] as $role) {
449
            if ($security->isGranted($role)) {
450
                // role granted => access granted
451
                return;
452
            }
453
        }
454
455
        // access denied
456
        throw new AccessDeniedException('You are not allowed to access audits of '.$entity.' entity.');
457
    }
458
}
459