Reader   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 205
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 30
eloc 93
c 3
b 0
f 0
dl 0
loc 205
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
B createQuery() 0 46 9
A configureOptions() 0 20 3
A getEntityTableName() 0 6 1
A __construct() 0 3 1
A getAuditsByTransactionHash() 0 19 4
A checkRoles() 0 10 3
A getProvider() 0 3 1
A checkAuditable() 0 4 2
A getEntityAuditTableName() 0 19 2
A paginate() 0 18 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace DH\Auditor\Provider\Doctrine\Persistence\Reader;
6
7
use ArrayIterator;
8
use DH\Auditor\Exception\AccessDeniedException;
9
use DH\Auditor\Exception\InvalidArgumentException;
10
use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Security;
11
use DH\Auditor\Provider\Doctrine\Configuration;
12
use DH\Auditor\Provider\Doctrine\DoctrineProvider;
13
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\SimpleFilter;
14
use DH\Auditor\Provider\Doctrine\Service\AuditingService;
15
use Doctrine\ORM\Mapping\ClassMetadata as ORMMetadata;
16
use Symfony\Component\OptionsResolver\OptionsResolver;
17
18
/**
19
 * @see \DH\Auditor\Tests\Provider\Doctrine\Persistence\Reader\ReaderTest
20
 */
21
final class Reader
22
{
23
    /**
24
     * @var int
25
     */
26
    public const PAGE_SIZE = 50;
27
28
    private DoctrineProvider $provider;
29
30
    /**
31
     * Reader constructor.
32
     */
33
    public function __construct(DoctrineProvider $provider)
34
    {
35
        $this->provider = $provider;
36
    }
37
38
    public function getProvider(): DoctrineProvider
39
    {
40
        return $this->provider;
41
    }
42
43
    public function createQuery(string $entity, array $options = []): Query
44
    {
45
        $this->checkAuditable($entity);
46
        $this->checkRoles($entity, Security::VIEW_SCOPE);
47
48
        $resolver = new OptionsResolver();
49
        $this->configureOptions($resolver);
50
        $config = $resolver->resolve($options);
51
52
        $connection = $this->provider->getStorageServiceForEntity($entity)->getEntityManager()->getConnection();
53
        $timezone = $this->provider->getAuditor()->getConfiguration()->getTimezone();
54
55
        $query = new Query($this->getEntityAuditTableName($entity), $connection, $timezone);
56
        $query
57
            ->addOrderBy(Query::CREATED_AT, 'DESC')
58
            ->addOrderBy(Query::ID, 'DESC')
59
        ;
60
61
        if (null !== $config['type']) {
62
            $query->addFilter(new SimpleFilter(Query::TYPE, $config['type']));
63
        }
64
65
        if (null !== $config['object_id']) {
66
            $query->addFilter(new SimpleFilter(Query::OBJECT_ID, $config['object_id']));
67
        }
68
69
        if (null !== $config['transaction_hash']) {
70
            $query->addFilter(new SimpleFilter(Query::TRANSACTION_HASH, $config['transaction_hash']));
71
        }
72
73
        if (null !== $config['page'] && null !== $config['page_size']) {
74
            $query->limit($config['page_size'], ($config['page'] - 1) * $config['page_size']);
75
        }
76
77
        /** @var AuditingService $auditingService */
78
        $auditingService = $this->provider->getAuditingServiceForEntity($entity);
79
        $metadata = $auditingService->getEntityManager()->getClassMetadata($entity);
80
        if (
81
            $config['strict']
82
            && $metadata instanceof ORMMetadata
83
            && ORMMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $metadata->inheritanceType
84
        ) {
85
            $query->addFilter(new SimpleFilter(Query::DISCRIMINATOR, $entity));
86
        }
87
88
        return $query;
89
    }
90
91
    /**
92
     * Returns an array of all audited entries/operations for a given transaction hash
93
     * indexed by entity FQCN.
94
     */
95
    public function getAuditsByTransactionHash(string $transactionHash): array
96
    {
97
        /** @var Configuration $configuration */
98
        $configuration = $this->provider->getConfiguration();
99
        $results = [];
100
101
        $entities = $configuration->getEntities();
102
        foreach (array_keys($entities) as $entity) {
103
            try {
104
                $audits = $this->createQuery($entity, ['transaction_hash' => $transactionHash])->execute();
105
                if ([] !== $audits) {
106
                    $results[$entity] = $audits;
107
                }
108
            } catch (AccessDeniedException) {
109
                // access denied
110
            }
111
        }
112
113
        return $results;
114
    }
115
116
    /**
117
     * @return array{results: ArrayIterator<int|string, \DH\Auditor\Model\Entry>, currentPage: int, hasPreviousPage: bool, hasNextPage: bool, previousPage: null|int, nextPage: null|int, numPages: int, haveToPaginate: bool, numResults: int, pageSize: int}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{results: ArrayIter...ts: int, pageSize: int} at position 4 could not be parsed: Expected '}' at position 4, but found 'ArrayIterator'.
Loading history...
118
     */
119
    public function paginate(Query $query, int $page = 1, int $pageSize = self::PAGE_SIZE): array
120
    {
121
        $numResults = $query->count();
122
        $currentPage = $page < 1 ? 1 : $page;
123
        $hasPreviousPage = $currentPage > 1;
124
        $hasNextPage = ($currentPage * $pageSize) < $numResults;
125
126
        return [
127
            'results' => new ArrayIterator($query->execute()),
128
            'currentPage' => $currentPage,
129
            'hasPreviousPage' => $hasPreviousPage,
130
            'hasNextPage' => $hasNextPage,
131
            'previousPage' => $hasPreviousPage ? $currentPage - 1 : null,
132
            'nextPage' => $hasNextPage ? $currentPage + 1 : null,
133
            'numPages' => (int) ceil($numResults / $pageSize),
134
            'haveToPaginate' => $numResults > $pageSize,
135
            'numResults' => $numResults,
136
            'pageSize' => $pageSize,
137
        ];
138
    }
139
140
    /**
141
     * Returns the table name of $entity.
142
     */
143
    public function getEntityTableName(string $entity): string
144
    {
145
        /** @var AuditingService $auditingService */
146
        $auditingService = $this->provider->getAuditingServiceForEntity($entity);
147
148
        return $auditingService->getEntityManager()->getClassMetadata($entity)->getTableName();
149
    }
150
151
    /**
152
     * Returns the audit table name for $entity.
153
     */
154
    public function getEntityAuditTableName(string $entity): string
155
    {
156
        /** @var Configuration $configuration */
157
        $configuration = $this->provider->getConfiguration();
158
159
        /** @var AuditingService $auditingService */
160
        $auditingService = $this->provider->getAuditingServiceForEntity($entity);
161
        $entityManager = $auditingService->getEntityManager();
162
        $schema = '';
163
        if ($entityManager->getClassMetadata($entity)->getSchemaName()) {
164
            $schema = $entityManager->getClassMetadata($entity)->getSchemaName().'.';
165
        }
166
167
        return sprintf(
168
            '%s%s%s%s',
169
            $schema,
170
            $configuration->getTablePrefix(),
171
            $this->getEntityTableName($entity),
172
            $configuration->getTableSuffix()
173
        );
174
    }
175
176
    private function configureOptions(OptionsResolver $resolver): void
177
    {
178
        // https://symfony.com/doc/current/components/options_resolver.html
179
        $resolver
180
            ->setDefaults([
181
                'type' => null,
182
                'object_id' => null,
183
                'transaction_hash' => null,
184
                'page' => 1,
185
                'page_size' => self::PAGE_SIZE,
186
                'strict' => true,
187
            ])
188
            ->setAllowedTypes('type', ['null', 'string', 'array'])
189
            ->setAllowedTypes('object_id', ['null', 'int', 'string', 'array'])
190
            ->setAllowedTypes('transaction_hash', ['null', 'string', 'array'])
191
            ->setAllowedTypes('page', ['null', 'int'])
192
            ->setAllowedTypes('page_size', ['null', 'int'])
193
            ->setAllowedTypes('strict', ['null', 'bool'])
194
            ->setAllowedValues('page', static fn (?int $value): bool => null === $value || $value >= 1)
195
            ->setAllowedValues('page_size', static fn (?int $value): bool => null === $value || $value >= 1)
196
        ;
197
    }
198
199
    /**
200
     * Throws an InvalidArgumentException if given entity is not auditable.
201
     *
202
     * @throws InvalidArgumentException
203
     */
204
    private function checkAuditable(string $entity): void
205
    {
206
        if (!$this->provider->isAuditable($entity)) {
207
            throw new InvalidArgumentException('Entity '.$entity.' is not auditable.');
208
        }
209
    }
210
211
    /**
212
     * Throws an AccessDeniedException if user not is granted to access audits for the given entity.
213
     *
214
     * @throws AccessDeniedException
215
     */
216
    private function checkRoles(string $entity, string $scope): void
217
    {
218
        $roleChecker = $this->provider->getAuditor()->getConfiguration()->getRoleChecker();
219
220
        if (null === $roleChecker || $roleChecker($entity, $scope)) {
221
            return;
222
        }
223
224
        // access denied
225
        throw new AccessDeniedException('You are not allowed to access audits of "'.$entity.'" entity.');
226
    }
227
}
228