DoctrineProvider::loadAnnotations()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 14
c 2
b 0
f 0
dl 0
loc 22
rs 9.7998
nc 3
nop 2
cc 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace DH\Auditor\Provider\Doctrine;
6
7
use DH\Auditor\Event\LifecycleEvent;
8
use DH\Auditor\Exception\InvalidArgumentException;
9
use DH\Auditor\Exception\ProviderException;
10
use DH\Auditor\Provider\AbstractProvider;
11
use DH\Auditor\Provider\ConfigurationInterface;
12
use DH\Auditor\Provider\Doctrine\Auditing\Annotation\AnnotationLoader;
13
use DH\Auditor\Provider\Doctrine\Auditing\Event\DoctrineSubscriber;
14
use DH\Auditor\Provider\Doctrine\Auditing\Transaction\TransactionManager;
15
use DH\Auditor\Provider\Doctrine\Persistence\Event\CreateSchemaListener;
16
use DH\Auditor\Provider\Doctrine\Persistence\Event\TableSchemaListener;
17
use DH\Auditor\Provider\Doctrine\Persistence\Helper\DoctrineHelper;
18
use DH\Auditor\Provider\Doctrine\Service\AuditingService;
19
use DH\Auditor\Provider\Doctrine\Service\StorageService;
20
use DH\Auditor\Provider\ProviderInterface;
21
use DH\Auditor\Provider\Service\AuditingServiceInterface;
22
use Doctrine\ORM\EntityManagerInterface;
23
use Doctrine\ORM\Events;
24
use Doctrine\ORM\Tools\ToolEvents;
25
use Exception;
26
27
/**
28
 * @see \DH\Auditor\Tests\Provider\Doctrine\DoctrineProviderTest
29
 */
30
final class DoctrineProvider extends AbstractProvider
31
{
32
    /**
33
     * @var array<string, string>
34
     */
35
    private const FIELDS = [
36
        'type' => '?',
37
        'object_id' => '?',
38
        'discriminator' => '?',
39
        'transaction_hash' => '?',
40
        'diffs' => '?',
41
        'blame_id' => '?',
42
        'blame_user' => '?',
43
        'blame_user_fqdn' => '?',
44
        'blame_user_firewall' => '?',
45
        'ip' => '?',
46
        'created_at' => '?',
47
    ];
48
49
    private TransactionManager $transactionManager;
50
51
    public function __construct(ConfigurationInterface $configuration)
52
    {
53
        $this->configuration = $configuration;
54
        $this->transactionManager = new TransactionManager($this);
55
56
        \assert($this->configuration instanceof Configuration);    // helps PHPStan
57
        $this->configuration->setProvider($this);
0 ignored issues
show
Bug introduced by
The method setProvider() does not exist on DH\Auditor\Provider\ConfigurationInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to DH\Auditor\Provider\ConfigurationInterface. ( Ignorable by Annotation )

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

57
        $this->configuration->/** @scrutinizer ignore-call */ 
58
                              setProvider($this);
Loading history...
58
    }
59
60
    public function registerAuditingService(AuditingServiceInterface $service): ProviderInterface
61
    {
62
        parent::registerAuditingService($service);
63
64
        \assert($service instanceof AuditingService);    // helps PHPStan
65
        $entityManager = $service->getEntityManager();
66
        $evm = $entityManager->getEventManager();
67
68
        // Register subscribers
69
        $evm->addEventListener([Events::loadClassMetadata], new TableSchemaListener($this));
70
        $evm->addEventListener([ToolEvents::postGenerateSchemaTable], new CreateSchemaListener($this));
71
        $evm->addEventSubscriber(new DoctrineSubscriber($this->transactionManager));
72
73
        return $this;
74
    }
75
76
    public function isStorageMapperRequired(): bool
77
    {
78
        return \count($this->getStorageServices()) > 1;
79
    }
80
81
    public function getAuditingServiceForEntity(string $entity): AuditingService
82
    {
83
        foreach ($this->auditingServices as $service) {
84
            \assert($service instanceof AuditingService);   // helps PHPStan
85
86
            try {
87
                // entity is managed by the entity manager of this service
88
                $service->getEntityManager()->getClassMetadata($entity)->getTableName();
89
90
                return $service;
91
            } catch (Exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
92
            }
93
        }
94
95
        throw new InvalidArgumentException(sprintf('Auditing service not found for "%s".', $entity));
96
    }
97
98
    public function getStorageServiceForEntity(string $entity): StorageService
99
    {
100
        $this->checkStorageMapper();
101
102
        \assert($this->configuration instanceof Configuration);   // helps PHPStan
103
        $storageMapper = $this->configuration->getStorageMapper();
0 ignored issues
show
Bug introduced by
The method getStorageMapper() does not exist on DH\Auditor\Provider\ConfigurationInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to DH\Auditor\Provider\ConfigurationInterface. ( Ignorable by Annotation )

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

103
        /** @scrutinizer ignore-call */ 
104
        $storageMapper = $this->configuration->getStorageMapper();
Loading history...
104
105
        if (null === $storageMapper || 1 === \count($this->getStorageServices())) {
106
            // No mapper and only 1 storage entity manager
107
            /** @var array<StorageService> $services */
108
            $services = $this->getStorageServices();
109
110
            return array_values($services)[0];
111
        }
112
113
        if (\is_string($storageMapper) && class_exists($storageMapper)) {
114
            $storageMapper = new $storageMapper();
115
        }
116
117
        \assert(\is_callable($storageMapper));   // helps PHPStan
118
119
        return $storageMapper($entity, $this->getStorageServices());
120
    }
121
122
    public function persist(LifecycleEvent $event): void
123
    {
124
        $payload = $event->getPayload();
125
        $auditTable = $payload['table'];
126
        $entity = $payload['entity'];
127
        unset($payload['table'], $payload['entity']);
128
129
        $keys = array_keys(self::FIELDS);
130
        $query = sprintf(
131
            'INSERT INTO %s (%s) VALUES (%s)',
132
            $auditTable,
133
            implode(', ', $keys),
134
            implode(', ', array_values(self::FIELDS))
135
        );
136
137
        /** @var StorageService $storageService */
138
        $storageService = $this->getStorageServiceForEntity($entity);
139
        $statement = $storageService->getEntityManager()->getConnection()->prepare($query);
140
141
        foreach ($payload as $key => $value) {
142
            $statement->bindValue(array_search($key, $keys, true) + 1, $value);
143
        }
144
145
        $statement->executeStatement();
146
147
        // let's get the last inserted ID from the database so other providers can use that info
148
        $payload = $event->getPayload();
149
        $payload['id'] = (int) $storageService->getEntityManager()->getConnection()->lastInsertId();
150
        $event->setPayload($payload);
151
    }
152
153
    /**
154
     * Returns true if $entity is auditable.
155
     */
156
    public function isAuditable(object|string $entity): bool
157
    {
158
        $class = DoctrineHelper::getRealClassName($entity);
159
        // is $entity part of audited entities?
160
        \assert($this->configuration instanceof Configuration);   // helps PHPStan
161
162
        // no => $entity is not audited
163
        return \array_key_exists($class, $this->configuration->getEntities());
0 ignored issues
show
Bug introduced by
The method getEntities() does not exist on DH\Auditor\Provider\ConfigurationInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to DH\Auditor\Provider\ConfigurationInterface. ( Ignorable by Annotation )

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

163
        return \array_key_exists($class, $this->configuration->/** @scrutinizer ignore-call */ getEntities());
Loading history...
164
    }
165
166
    /**
167
     * Returns true if $entity is audited.
168
     */
169
    public function isAudited(object|string $entity): bool
170
    {
171
        \assert($this->auditor instanceof \DH\Auditor\Auditor);
172
        if (!$this->auditor->getConfiguration()->isEnabled()) {
173
            return false;
174
        }
175
176
        /** @var Configuration $configuration */
177
        $configuration = $this->configuration;
178
        $class = DoctrineHelper::getRealClassName($entity);
179
180
        // is $entity part of audited entities?
181
        $entities = $configuration->getEntities();
182
        if (!\array_key_exists($class, $entities)) {
183
            // no => $entity is not audited
184
            return false;
185
        }
186
187
        $entityOptions = $entities[$class];
188
189
        if (isset($entityOptions['enabled'])) {
190
            return (bool) $entityOptions['enabled'];
191
        }
192
193
        return true;
194
    }
195
196
    /**
197
     * Returns true if $field is audited.
198
     */
199
    public function isAuditedField(object|string $entity, string $field): bool
200
    {
201
        // is $field is part of globally ignored columns?
202
        \assert($this->configuration instanceof Configuration);   // helps PHPStan
203
        if (\in_array($field, $this->configuration->getIgnoredColumns(), true)) {
0 ignored issues
show
Bug introduced by
The method getIgnoredColumns() does not exist on DH\Auditor\Provider\ConfigurationInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to DH\Auditor\Provider\ConfigurationInterface. ( Ignorable by Annotation )

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

203
        if (\in_array($field, $this->configuration->/** @scrutinizer ignore-call */ getIgnoredColumns(), true)) {
Loading history...
204
            // yes => $field is not audited
205
            return false;
206
        }
207
208
        // is $entity audited?
209
        if (!$this->isAudited($entity)) {
210
            // no => $field is not audited
211
            return false;
212
        }
213
214
        $class = DoctrineHelper::getRealClassName($entity);
215
        $entityOptions = $this->configuration->getEntities()[$class];
216
217
        // are columns excluded and is field part of them?
218
        // yes => $field is not audited
219
        return !(isset($entityOptions['ignored_columns'])
220
            && \in_array($field, $entityOptions['ignored_columns'], true));
221
    }
222
223
    public function supportsStorage(): bool
224
    {
225
        return true;
226
    }
227
228
    public function supportsAuditing(): bool
229
    {
230
        return true;
231
    }
232
233
    public function setStorageMapper(callable $storageMapper): void
234
    {
235
        \assert($this->configuration instanceof Configuration);   // helps PHPStan
236
        $this->configuration->setStorageMapper($storageMapper);
0 ignored issues
show
Bug introduced by
The method setStorageMapper() does not exist on DH\Auditor\Provider\ConfigurationInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to DH\Auditor\Provider\ConfigurationInterface. ( Ignorable by Annotation )

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

236
        $this->configuration->/** @scrutinizer ignore-call */ 
237
                              setStorageMapper($storageMapper);
Loading history...
237
    }
238
239
    public function loadAnnotations(EntityManagerInterface $entityManager, array $entities): self
240
    {
241
        \assert($this->configuration instanceof Configuration);   // helps PHPStan
242
        $ormConfiguration = $entityManager->getConfiguration();
243
        $metadataCache = $ormConfiguration->getMetadataCache();
244
245
        $annotationLoader = new AnnotationLoader($entityManager);
246
247
        if ($metadataCache instanceof \Psr\Cache\CacheItemPoolInterface) {
248
            $item = $metadataCache->getItem('__DH_ANNOTATIONS__');
249
            if (!$item->isHit() || !\is_array($annotationEntities = $item->get())) {
250
                $annotationEntities = $annotationLoader->load();
251
                $item->set($annotationEntities);
252
                $metadataCache->save($item);
253
            }
254
        } else {
255
            $annotationEntities = $annotationLoader->load();
256
        }
257
258
        $this->configuration->setEntities(array_merge($entities, $annotationEntities));
0 ignored issues
show
Bug introduced by
The method setEntities() does not exist on DH\Auditor\Provider\ConfigurationInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to DH\Auditor\Provider\ConfigurationInterface. ( Ignorable by Annotation )

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

258
        $this->configuration->/** @scrutinizer ignore-call */ 
259
                              setEntities(array_merge($entities, $annotationEntities));
Loading history...
259
260
        return $this;
261
    }
262
263
    private function checkStorageMapper(): self
264
    {
265
        \assert($this->configuration instanceof Configuration);   // helps PHPStan
266
        if (null === $this->configuration->getStorageMapper() && $this->isStorageMapperRequired()) {
267
            throw new ProviderException('You must provide a mapper callback to map audits to storage.');
268
        }
269
270
        return $this;
271
    }
272
}
273