Passed
Push — master ( ecadea...14db22 )
by Damien
03:21
created

Reader::getAuditsQueryBuilder()   B

Complexity

Conditions 9
Paths 6

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 28
nc 6
nop 8
dl 0
loc 45
rs 8.0555
c 0
b 0
f 0

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\Configuration;
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 Reader
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 Configuration
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 Configuration          $configuration
48
     * @param EntityManagerInterface $entityManager
49
     */
50
    public function __construct(
51
        Configuration $configuration,
52
        EntityManagerInterface $entityManager
53
    ) {
54
        $this->configuration = $configuration;
55
        $this->entityManager = $entityManager;
56
    }
57
58
    /**
59
     * @return Configuration
60
     */
61
    public function getConfiguration(): Configuration
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 Reader
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, Entry::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, Entry::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, Entry::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 && null !== $endDate && $endDate < $startDate) {
371
            throw new \InvalidArgumentException('$endDate must be greater than $startDate.');
372
        }
373
374
        if (null !== $startDate) {
375
            $queryBuilder
376
                ->andWhere('created_at >= :start_date')
377
                ->setParameter('start_date', $startDate->format('Y-m-d H:i:s'))
378
            ;
379
        }
380
381
        if (null !== $endDate) {
382
            $queryBuilder
383
                ->andWhere('created_at <= :end_date')
384
                ->setParameter('end_date', $endDate->format('Y-m-d H:i:s'))
385
            ;
386
        }
387
388
        return $queryBuilder;
389
    }
390
391
    /**
392
     * @param QueryBuilder    $queryBuilder
393
     * @param null|int|string $id
394
     *
395
     * @return QueryBuilder
396
     */
397
    private function filterByObjectId(QueryBuilder $queryBuilder, $id): QueryBuilder
398
    {
399
        if (null !== $id) {
400
            $queryBuilder
401
                ->andWhere('object_id = :object_id')
402
                ->setParameter('object_id', $id)
403
            ;
404
        }
405
406
        return $queryBuilder;
407
    }
408
409
    /**
410
     * Returns an array of audited entries/operations.
411
     *
412
     * @param string          $entity
413
     * @param null|int|string $id
414
     * @param null|int        $page
415
     * @param null|int        $pageSize
416
     * @param null|string     $transactionHash
417
     * @param bool            $strict
418
     * @param null|DateTime   $startDate
419
     * @param null|DateTime   $endDate
420
     *
421
     * @throws AccessDeniedException
422
     * @throws InvalidArgumentException
423
     *
424
     * @return QueryBuilder
425
     */
426
    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
427
    {
428
        $this->checkAuditable($entity);
429
        $this->checkRoles($entity, Security::VIEW_SCOPE);
430
431
        if (null !== $page && $page < 1) {
432
            throw new \InvalidArgumentException('$page must be greater or equal than 1.');
433
        }
434
435
        if (null !== $pageSize && $pageSize < 1) {
436
            throw new \InvalidArgumentException('$pageSize must be greater or equal than 1.');
437
        }
438
439
        $storage = $this->configuration->getEntityManager() ?? $this->entityManager;
440
        $connection = $storage->getConnection();
441
442
        $queryBuilder = $connection->createQueryBuilder();
443
        $queryBuilder
444
            ->select('*')
445
            ->from($this->getEntityAuditTableName($entity), 'at')
446
            ->orderBy('created_at', 'DESC')
447
            ->addOrderBy('id', 'DESC')
448
        ;
449
450
        $metadata = $this->entityManager->getClassMetadata($entity);
451
        if ($strict && $metadata instanceof ORMMetadata && ORMMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $metadata->inheritanceType) {
452
            $queryBuilder
453
                ->andWhere('discriminator = :discriminator')
454
                ->setParameter('discriminator', $entity)
455
            ;
456
        }
457
458
        $this->filterByObjectId($queryBuilder, $id);
459
        $this->filterByType($queryBuilder, $this->filters);
460
        $this->filterByTransaction($queryBuilder, $transactionHash);
461
        $this->filterByDate($queryBuilder, $startDate, $endDate);
462
463
        if (null !== $pageSize) {
464
            $queryBuilder
465
                ->setFirstResult(($page - 1) * $pageSize)
466
                ->setMaxResults($pageSize)
467
            ;
468
        }
469
470
        return $queryBuilder;
471
    }
472
473
    /**
474
     * Throws an InvalidArgumentException if given entity is not auditable.
475
     *
476
     * @param string $entity
477
     *
478
     * @throws InvalidArgumentException
479
     */
480
    private function checkAuditable(string $entity): void
481
    {
482
        if (!$this->configuration->isAuditable($entity)) {
483
            throw new InvalidArgumentException('Entity '.$entity.' is not auditable.');
484
        }
485
    }
486
487
    /**
488
     * Throws an AccessDeniedException if user not is granted to access audits for the given entity.
489
     *
490
     * @param string $entity
491
     * @param string $scope
492
     *
493
     * @throws AccessDeniedException
494
     */
495
    private function checkRoles(string $entity, string $scope): void
496
    {
497
        $userProvider = $this->configuration->getUserProvider();
498
        $user = null === $userProvider ? null : $userProvider->getUser();
499
        $security = null === $userProvider ? null : $userProvider->getSecurity();
500
501
        if (!($user instanceof UserInterface) || !($security instanceof CoreSecurity)) {
502
            // If no security defined or no user identified, consider access granted
503
            return;
504
        }
505
506
        $entities = $this->configuration->getEntities();
507
508
        $roles = $entities[$entity]['roles'] ?? null;
509
510
        if (null === $roles) {
511
            // If no roles are configured, consider access granted
512
            return;
513
        }
514
515
        $scope = $roles[$scope] ?? null;
516
517
        if (null === $scope) {
518
            // If no roles for the given scope are configured, consider access granted
519
            return;
520
        }
521
522
        // roles are defined for the give scope
523
        foreach ($scope as $role) {
524
            if ($security->isGranted($role)) {
525
                // role granted => access granted
526
                return;
527
            }
528
        }
529
530
        // access denied
531
        throw new AccessDeniedException('You are not allowed to access audits of '.$entity.' entity.');
532
    }
533
}
534