Passed
Push — master ( c1af7f...0d4bea )
by Damien
03:24
created

AuditReader::getAuditsQueryBuilder()   B

Complexity

Conditions 11
Paths 12

Size

Total Lines 46
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 28
c 0
b 0
f 0
nc 12
nop 6
dl 0
loc 46
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Pagerfanta\Adapter\DoctrineDbalSingleTableAdapter;
16
use Pagerfanta\Pagerfanta;
17
use PDO;
18
use Symfony\Component\Security\Core\Security as CoreSecurity;
19
20
class AuditReader
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 AuditConfiguration
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 AuditConfiguration     $configuration
49
     * @param EntityManagerInterface $entityManager
50
     */
51
    public function __construct(
52
        AuditConfiguration $configuration,
53
        EntityManagerInterface $entityManager
54
    ) {
55
        $this->configuration = $configuration;
56
        $this->entityManager = $entityManager;
57
    }
58
59
    /**
60
     * @return AuditConfiguration
61
     */
62
    public function getConfiguration(): AuditConfiguration
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 AuditReader
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 object|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($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, AuditEntry::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 object|string   $entity
183
     * @param null|int|string $id
184
     * @param int             $page
185
     * @param int             $pageSize
186
     *
187
     * @throws AccessDeniedException
188
     * @throws InvalidArgumentException
189
     *
190
     * @return Pagerfanta
191
     */
192
    public function getAuditsPager($entity, $id = null, int $page = 1, int $pageSize = self::PAGE_SIZE): Pagerfanta
193
    {
194
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
195
196
        $adapter = new DoctrineDbalSingleTableAdapter($queryBuilder, 'at.id');
197
198
        $pagerfanta = new Pagerfanta($adapter);
199
        $pagerfanta
200
            ->setMaxPerPage($pageSize)
201
            ->setCurrentPage($page)
202
        ;
203
204
        return $pagerfanta;
205
    }
206
207
    /**
208
     * Returns the amount of audited entries/operations.
209
     *
210
     * @param object|string   $entity
211
     * @param null|int|string $id
212
     *
213
     * @throws AccessDeniedException
214
     * @throws InvalidArgumentException
215
     *
216
     * @return int
217
     */
218
    public function getAuditsCount($entity, $id = null): int
219
    {
220
        $queryBuilder = $this->getAuditsQueryBuilder($entity, $id);
221
222
        $result = $queryBuilder
223
            ->resetQueryPart('select')
224
            ->resetQueryPart('orderBy')
225
            ->select('COUNT(id)')
226
            ->execute()
227
            ->fetchColumn(0)
228
        ;
229
230
        return false === $result ? 0 : $result;
231
    }
232
233
    /**
234
     * @param object|string $entity
235
     * @param string        $id
236
     *
237
     * @throws AccessDeniedException
238
     * @throws InvalidArgumentException
239
     *
240
     * @return mixed[]
241
     */
242
    public function getAudit($entity, $id): array
243
    {
244
        $this->checkAuditable($entity);
245
        $this->checkRoles($entity, Security::VIEW_SCOPE);
246
247
        $connection = $this->entityManager->getConnection();
248
249
        /**
250
         * @var \Doctrine\DBAL\Query\QueryBuilder
251
         */
252
        $queryBuilder = $connection->createQueryBuilder();
253
        $queryBuilder
254
            ->select('*')
255
            ->from($this->getEntityAuditTableName($entity))
256
            ->where('id = :id')
257
            ->setParameter('id', $id)
258
        ;
259
260
        $this->filterByType($queryBuilder, $this->filters);
261
262
        /** @var Statement $statement */
263
        $statement = $queryBuilder->execute();
264
        $statement->setFetchMode(PDO::FETCH_CLASS, AuditEntry::class);
265
266
        return $statement->fetchAll();
267
    }
268
269
    /**
270
     * Returns the table name of $entity.
271
     *
272
     * @param object|string $entity
273
     *
274
     * @return string
275
     */
276
    public function getEntityTableName($entity): string
277
    {
278
        $entityName = \is_string($entity) ? $entity : \get_class($entity);
279
280
        return $this->entityManager->getClassMetadata($entityName)->getTableName();
281
    }
282
283
    /**
284
     * Returns the audit table name for $entity.
285
     *
286
     * @param object|string $entity
287
     *
288
     * @return string
289
     */
290
    public function getEntityAuditTableName($entity): string
291
    {
292
        $entityName = \is_string($entity) ? $entity : \get_class($entity);
293
294
        $schema = '';
295
        if ($this->entityManager->getClassMetadata($entityName)->getSchemaName()) {
296
            $schema = $this->entityManager->getClassMetadata($entityName)->getSchemaName().'.';
297
        }
298
299
        return sprintf('%s%s%s%s', $schema, $this->configuration->getTablePrefix(), $this->getEntityTableName($entityName), $this->configuration->getTableSuffix());
300
    }
301
302
    /**
303
     * @return EntityManagerInterface
304
     */
305
    public function getEntityManager(): EntityManagerInterface
306
    {
307
        return $this->entityManager;
308
    }
309
310
    private function filterByType(QueryBuilder $queryBuilder, array $filters): QueryBuilder
311
    {
312
        if (!empty($filters)) {
313
            $queryBuilder
314
                ->andWhere('type IN (:filters)')
315
                ->setParameter('filters', $filters, Connection::PARAM_STR_ARRAY)
316
            ;
317
        }
318
319
        return $queryBuilder;
320
    }
321
322
    private function filterByTransaction(QueryBuilder $queryBuilder, ?string $transactionHash): QueryBuilder
323
    {
324
        if (null !== $transactionHash) {
325
            $queryBuilder
326
                ->andWhere('transaction_hash = :transaction_hash')
327
                ->setParameter('transaction_hash', $transactionHash)
328
            ;
329
        }
330
331
        return $queryBuilder;
332
    }
333
334
    /**
335
     * @param QueryBuilder    $queryBuilder
336
     * @param null|int|string $id
337
     *
338
     * @return QueryBuilder
339
     */
340
    private function filterByObjectId(QueryBuilder $queryBuilder, $id): QueryBuilder
341
    {
342
        if (null !== $id) {
343
            $queryBuilder
344
                ->andWhere('object_id = :object_id')
345
                ->setParameter('object_id', $id)
346
            ;
347
        }
348
349
        return $queryBuilder;
350
    }
351
352
    /**
353
     * Returns an array of audited entries/operations.
354
     *
355
     * @param object|string   $entity
356
     * @param null|int|string $id
357
     * @param null|int        $page
358
     * @param null|int        $pageSize
359
     * @param null|string     $transactionHash
360
     * @param bool            $strict
361
     *
362
     * @throws AccessDeniedException
363
     * @throws InvalidArgumentException
364
     *
365
     * @return QueryBuilder
366
     */
367
    private function getAuditsQueryBuilder($entity, $id = null, ?int $page = null, ?int $pageSize = null, ?string $transactionHash = null, bool $strict = true): QueryBuilder
368
    {
369
        $entityName = \is_string($entity) ? $entity : \get_class($entity);
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($entityName);
394
        if ($strict && $metadata instanceof ORMMetadata && ORMMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $metadata->inheritanceType) {
395
            $queryBuilder
396
                ->andWhere('discriminator = :discriminator')
397
                ->setParameter('discriminator', \is_object($entity) ? \get_class($entity) : $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 object|string $entity
419
     *
420
     * @throws InvalidArgumentException
421
     */
422
    private function checkAuditable($entity): void
423
    {
424
        if (!$this->configuration->isAuditable($entity)) {
425
            throw new InvalidArgumentException('Entity '.$entity.' is not auditable.');
0 ignored issues
show
Bug introduced by
Are you sure $entity of type object|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

425
            throw new InvalidArgumentException('Entity './** @scrutinizer ignore-type */ $entity.' is not auditable.');
Loading history...
426
        }
427
    }
428
429
    /**
430
     * Throws an AccessDeniedException if user not is granted to access audits for the given entity.
431
     *
432
     * @param object|string $entity
433
     * @param string        $scope
434
     *
435
     * @throws AccessDeniedException
436
     */
437
    private function checkRoles($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
        if (!isset($entities[$entity]['roles']) || null === $entities[$entity]['roles']) {
451
            // If no roles are configured, consider access granted
452
            return;
453
        }
454
455
        if (!isset($entities[$entity]['roles'][$scope]) || null === $entities[$entity]['roles'][$scope]) {
456
            // If no roles for the given scope are configured, consider access granted
457
            return;
458
        }
459
460
        // roles are defined for the give scope
461
        foreach ($entities[$entity]['roles'][$scope] as $role) {
462
            if ($security->isGranted($role)) {
463
                // role granted => access granted
464
                return;
465
            }
466
        }
467
468
        // access denied
469
        throw new AccessDeniedException('You are not allowed to access audits of '.$entity.' entity.');
0 ignored issues
show
Bug introduced by
Are you sure $entity of type object|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

469
        throw new AccessDeniedException('You are not allowed to access audits of './** @scrutinizer ignore-type */ $entity.' entity.');
Loading history...
470
    }
471
}
472