Passed
Pull Request — master (#188)
by Damien
03:02
created

TransactionProcessor::audit()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 18
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 24
rs 9.6666
1
<?php
2
3
namespace DH\DoctrineAuditBundle\Transaction;
4
5
use DateTime;
6
use DateTimeZone;
7
use DH\DoctrineAuditBundle\Configuration;
8
use DH\DoctrineAuditBundle\Event\LifecycleEvent;
9
use DH\DoctrineAuditBundle\Helper\DoctrineHelper;
10
use DH\DoctrineAuditBundle\Model\Transaction;
11
use Doctrine\DBAL\Types\Type;
12
use Doctrine\ORM\EntityManagerInterface;
13
use Doctrine\ORM\Mapping\ClassMetadata;
14
use Exception;
15
16
class TransactionProcessor
17
{
18
    use AuditTrait;
0 ignored issues
show
introduced by
The trait DH\DoctrineAuditBundle\Transaction\AuditTrait requires some properties which are not provided by DH\DoctrineAuditBundle\T...on\TransactionProcessor: $fieldMappings, $embeddedClasses, $name
Loading history...
19
20
    /**
21
     * @var Configuration
22
     */
23
    private $configuration;
24
25
    /**
26
     * @var EntityManagerInterface
27
     */
28
    private $em;
29
30
    public function __construct(Configuration $configuration)
31
    {
32
        $this->configuration = $configuration;
33
        $this->em = $this->configuration->getEntityManager();
34
    }
35
36
    /**
37
     * @param Transaction $transaction
38
     *
39
     * @throws \Doctrine\DBAL\DBALException
40
     * @throws \Doctrine\ORM\Mapping\MappingException
41
     */
42
    public function process(Transaction $transaction): void
43
    {
44
        $this->processInsertions($transaction);
45
        $this->processUpdates($transaction);
46
        $this->processAssociations($transaction);
47
        $this->processDissociations($transaction);
48
        $this->processDeletions($transaction);
49
    }
50
51
    /**
52
     * @param array $payload
53
     */
54
    private function notify(array $payload): void
55
    {
56
        $dispatcher = $this->configuration->getEventDispatcher();
57
58
        if ($this->configuration->isPre43Dispatcher()) {
59
            // Symfony 3.x
60
            $dispatcher->dispatch(LifecycleEvent::class, new LifecycleEvent($payload));
0 ignored issues
show
Bug introduced by
DH\DoctrineAuditBundle\Event\LifecycleEvent::class of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

60
            $dispatcher->dispatch(/** @scrutinizer ignore-type */ LifecycleEvent::class, new LifecycleEvent($payload));
Loading history...
61
        } else {
62
            // Symfony 4.x
63
            $dispatcher->dispatch(new LifecycleEvent($payload));
64
        }
65
    }
66
67
    /**
68
     * Adds an insert entry to the audit table.
69
     *
70
     * @param EntityManagerInterface $em
71
     * @param object                 $entity
72
     * @param array                  $ch
73
     * @param string                 $transactionHash
74
     *
75
     * @throws \Doctrine\DBAL\DBALException
76
     * @throws \Doctrine\ORM\Mapping\MappingException
77
     */
78
    private function insert(EntityManagerInterface $em, $entity, array $ch, string $transactionHash): void
79
    {
80
        /** @var ClassMetadata $meta */
81
        $meta = $em->getClassMetadata(DoctrineHelper::getRealClassName($entity));
82
        $this->audit([
83
            'action' => 'insert',
84
            'blame' => $this->blame(),
85
            'diff' => $this->diff($em, $entity, $ch),
86
            'table' => $meta->getTableName(),
87
            'schema' => $meta->getSchemaName(),
88
            'id' => $this->id($em, $entity),
89
            'transaction_hash' => $transactionHash,
90
            'discriminator' => $this->getDiscriminator($entity, $meta->inheritanceType),
91
            'entity' => $meta->getName(),
92
        ]);
93
    }
94
95
    /**
96
     * Adds an update entry to the audit table.
97
     *
98
     * @param EntityManagerInterface $em
99
     * @param object                 $entity
100
     * @param array                  $ch
101
     * @param string                 $transactionHash
102
     *
103
     * @throws \Doctrine\DBAL\DBALException
104
     * @throws \Doctrine\ORM\Mapping\MappingException
105
     */
106
    private function update(EntityManagerInterface $em, $entity, array $ch, string $transactionHash): void
107
    {
108
        $diff = $this->diff($em, $entity, $ch);
109
        if (0 === \count($diff)) {
110
            return; // if there is no entity diff, do not log it
111
        }
112
        /** @var ClassMetadata $meta */
113
        $meta = $em->getClassMetadata(DoctrineHelper::getRealClassName($entity));
114
        $this->audit([
115
            'action' => 'update',
116
            'blame' => $this->blame(),
117
            'diff' => $diff,
118
            'table' => $meta->getTableName(),
119
            'schema' => $meta->getSchemaName(),
120
            'id' => $this->id($em, $entity),
121
            'transaction_hash' => $transactionHash,
122
            'discriminator' => $this->getDiscriminator($entity, $meta->inheritanceType),
123
            'entity' => $meta->getName(),
124
        ]);
125
    }
126
127
    /**
128
     * Adds a remove entry to the audit table.
129
     *
130
     * @param EntityManagerInterface $em
131
     * @param object                 $entity
132
     * @param mixed                  $id
133
     * @param string                 $transactionHash
134
     *
135
     * @throws \Doctrine\DBAL\DBALException
136
     * @throws \Doctrine\ORM\Mapping\MappingException
137
     */
138
    private function remove(EntityManagerInterface $em, $entity, $id, string $transactionHash): void
139
    {
140
        /** @var ClassMetadata $meta */
141
        $meta = $em->getClassMetadata(DoctrineHelper::getRealClassName($entity));
142
        $this->audit([
143
            'action' => 'remove',
144
            'blame' => $this->blame(),
145
            'diff' => $this->summarize($em, $entity, $id),
146
            'table' => $meta->getTableName(),
147
            'schema' => $meta->getSchemaName(),
148
            'id' => $id,
149
            'transaction_hash' => $transactionHash,
150
            'discriminator' => $this->getDiscriminator($entity, $meta->inheritanceType),
151
            'entity' => $meta->getName(),
152
        ]);
153
    }
154
155
    /**
156
     * Adds an association entry to the audit table.
157
     *
158
     * @param EntityManagerInterface $em
159
     * @param object                 $source
160
     * @param object                 $target
161
     * @param array                  $mapping
162
     * @param string                 $transactionHash
163
     *
164
     * @throws \Doctrine\DBAL\DBALException
165
     * @throws \Doctrine\ORM\Mapping\MappingException
166
     */
167
    private function associate(EntityManagerInterface $em, $source, $target, array $mapping, string $transactionHash): void
168
    {
169
        $this->associateOrDissociate('associate', $em, $source, $target, $mapping, $transactionHash);
170
    }
171
172
    /**
173
     * Adds a dissociation entry to the audit table.
174
     *
175
     * @param EntityManagerInterface $em
176
     * @param object                 $source
177
     * @param object                 $target
178
     * @param array                  $mapping
179
     * @param string                 $transactionHash
180
     *
181
     * @throws \Doctrine\DBAL\DBALException
182
     * @throws \Doctrine\ORM\Mapping\MappingException
183
     */
184
    private function dissociate(EntityManagerInterface $em, $source, $target, array $mapping, string $transactionHash): void
185
    {
186
        $this->associateOrDissociate('dissociate', $em, $source, $target, $mapping, $transactionHash);
187
    }
188
189
    /**
190
     * @param Transaction $transaction
191
     *
192
     * @throws \Doctrine\DBAL\DBALException
193
     * @throws \Doctrine\ORM\Mapping\MappingException
194
     */
195
    private function processInsertions(Transaction $transaction): void
196
    {
197
        $uow = $this->em->getUnitOfWork();
198
        foreach ($transaction->getInserted() as [$entity, $ch]) {
199
            // the changeset might be updated from UOW extra updates
200
            $ch = array_merge($ch, $uow->getEntityChangeSet($entity));
201
            $this->insert($this->em, $entity, $ch, $transaction->getTransactionHash());
202
        }
203
    }
204
205
    /**
206
     * @param Transaction $transaction
207
     *
208
     * @throws \Doctrine\DBAL\DBALException
209
     * @throws \Doctrine\ORM\Mapping\MappingException
210
     */
211
    private function processUpdates(Transaction $transaction): void
212
    {
213
        $uow = $this->em->getUnitOfWork();
214
        foreach ($transaction->getUpdated() as [$entity, $ch]) {
215
            // the changeset might be updated from UOW extra updates
216
            $ch = array_merge($ch, $uow->getEntityChangeSet($entity));
217
            $this->update($this->em, $entity, $ch, $transaction->getTransactionHash());
218
        }
219
    }
220
221
    /**
222
     * @param Transaction $transaction
223
     *
224
     * @throws \Doctrine\DBAL\DBALException
225
     * @throws \Doctrine\ORM\Mapping\MappingException
226
     */
227
    private function processAssociations(Transaction $transaction): void
228
    {
229
        foreach ($transaction->getAssociated() as [$source, $target, $mapping]) {
230
            $this->associate($this->em, $source, $target, $mapping, $transaction->getTransactionHash());
231
        }
232
    }
233
234
    /**
235
     * @param Transaction $transaction
236
     *
237
     * @throws \Doctrine\DBAL\DBALException
238
     * @throws \Doctrine\ORM\Mapping\MappingException
239
     */
240
    private function processDissociations(Transaction $transaction): void
241
    {
242
        foreach ($transaction->getDissociated() as [$source, $target, $id, $mapping]) {
243
            $this->dissociate($this->em, $source, $target, $mapping, $transaction->getTransactionHash());
244
        }
245
    }
246
247
    /**
248
     * @param Transaction $transaction
249
     *
250
     * @throws \Doctrine\DBAL\DBALException
251
     * @throws \Doctrine\ORM\Mapping\MappingException
252
     */
253
    private function processDeletions(Transaction $transaction): void
254
    {
255
        foreach ($transaction->getRemoved() as [$entity, $id]) {
256
            $this->remove($this->em, $entity, $id, $transaction->getTransactionHash());
257
        }
258
    }
259
260
    /**
261
     * Adds an association entry to the audit table.
262
     *
263
     * @param string                 $type
264
     * @param EntityManagerInterface $em
265
     * @param object                 $source
266
     * @param object                 $target
267
     * @param array                  $mapping
268
     * @param string                 $transactionHash
269
     *
270
     * @throws \Doctrine\DBAL\DBALException
271
     * @throws \Doctrine\ORM\Mapping\MappingException
272
     */
273
    private function associateOrDissociate(string $type, EntityManagerInterface $em, $source, $target, array $mapping, string $transactionHash): void
274
    {
275
        /** @var ClassMetadata $meta */
276
        $meta = $em->getClassMetadata(DoctrineHelper::getRealClassName($source));
277
        $data = [
278
            'action' => $type,
279
            'blame' => $this->blame(),
280
            'diff' => [
281
                'source' => $this->summarize($em, $source),
282
                'target' => $this->summarize($em, $target),
283
            ],
284
            'table' => $meta->getTableName(),
285
            'schema' => $meta->getSchemaName(),
286
            'id' => $this->id($em, $source),
287
            'transaction_hash' => $transactionHash,
288
            'discriminator' => $this->getDiscriminator($source, $meta->inheritanceType),
289
            'entity' => $meta->getName(),
290
        ];
291
292
        if (isset($mapping['joinTable']['name'])) {
293
            $data['diff']['table'] = $mapping['joinTable']['name'];
294
        }
295
296
        $this->audit($data);
297
    }
298
299
    /**
300
     * Adds an entry to the audit table.
301
     *
302
     * @param array $data
303
     *
304
     * @throws Exception
305
     */
306
    private function audit(array $data): void
307
    {
308
        $schema = $data['schema'] ? $data['schema'].'.' : '';
309
        $auditTable = $schema.$this->configuration->getTablePrefix().$data['table'].$this->configuration->getTableSuffix();
310
        $dt = new DateTime('now', new DateTimeZone($this->configuration->getTimezone()));
311
312
        $payload = [
313
            'entity' => $data['entity'],
314
            'table' => $auditTable,
315
            'type' => $data['action'],
316
            'object_id' => (string) $data['id'],
317
            'discriminator' => $data['discriminator'],
318
            'transaction_hash' => (string) $data['transaction_hash'],
319
            'diffs' => json_encode($data['diff']),
320
            'blame_id' => $data['blame']['user_id'],
321
            'blame_user' => $data['blame']['username'],
322
            'blame_user_fqdn' => $data['blame']['user_fqdn'],
323
            'blame_user_firewall' => $data['blame']['user_firewall'],
324
            'ip' => $data['blame']['client_ip'],
325
            'created_at' => $dt->format('Y-m-d H:i:s'),
326
        ];
327
328
        // send an `AuditEvent` event
329
        $this->notify($payload);
330
    }
331
332
    /**
333
     * @param object $entity
334
     * @param int    $inheritanceType
335
     *
336
     * @return null|string
337
     */
338
    private function getDiscriminator($entity, int $inheritanceType): ?string
339
    {
340
        return ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE === $inheritanceType ? DoctrineHelper::getRealClassName($entity) : null;
341
    }
342
}
343