Passed
Push — master ( a299b8...c1af7f )
by Damien
02:34
created

AuditHelper::getDoctrineType()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace DH\DoctrineAuditBundle\Helper;
4
5
use DH\DoctrineAuditBundle\AuditConfiguration;
6
use DH\DoctrineAuditBundle\User\UserInterface;
7
use Doctrine\DBAL\Types\Type;
8
use Doctrine\DBAL\Types\Types;
9
use Doctrine\ORM\EntityManagerInterface;
10
use Doctrine\ORM\Mapping\ClassMetadata;
11
12
class AuditHelper
13
{
14
    /**
15
     * @var \DH\DoctrineAuditBundle\AuditConfiguration
16
     */
17
    private $configuration;
18
19
    /**
20
     * @param AuditConfiguration $configuration
21
     */
22
    public function __construct(AuditConfiguration $configuration)
23
    {
24
        $this->configuration = $configuration;
25
    }
26
27
    /**
28
     * @return \DH\DoctrineAuditBundle\AuditConfiguration
29
     */
30
    public function getConfiguration(): AuditConfiguration
31
    {
32
        return $this->configuration;
33
    }
34
35
    /**
36
     * Returns the primary key value of an entity.
37
     *
38
     * @param EntityManagerInterface $em
39
     * @param object                 $entity
40
     *
41
     * @throws \Doctrine\DBAL\DBALException
42
     * @throws \Doctrine\ORM\Mapping\MappingException
43
     *
44
     * @return mixed
45
     */
46
    public function id(EntityManagerInterface $em, $entity)
47
    {
48
        /** @var ClassMetadata $meta */
49
        $meta = $em->getClassMetadata(\get_class($entity));
50
        $pk = $meta->getSingleIdentifierFieldName();
51
52
        if (isset($meta->fieldMappings[$pk])) {
53
            $type = Type::getType($meta->fieldMappings[$pk]['type']);
54
55
            return $this->value($em, $type, $meta->getReflectionProperty($pk)->getValue($entity));
56
        }
57
58
        /**
59
         * Primary key is not part of fieldMapping.
60
         *
61
         * @see https://github.com/DamienHarper/DoctrineAuditBundle/issues/40
62
         * @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/composite-primary-keys.html#identity-through-foreign-entities
63
         * We try to get it from associationMapping (will throw a MappingException if not available)
64
         */
65
        $targetEntity = $meta->getReflectionProperty($pk)->getValue($entity);
66
67
        $mapping = $meta->getAssociationMapping($pk);
68
69
        /** @var ClassMetadata $meta */
70
        $meta = $em->getClassMetadata($mapping['targetEntity']);
71
        $pk = $meta->getSingleIdentifierFieldName();
72
        $type = Type::getType($meta->fieldMappings[$pk]['type']);
73
74
        return $this->value($em, $type, $meta->getReflectionProperty($pk)->getValue($targetEntity));
75
    }
76
77
    /**
78
     * Computes a usable diff.
79
     *
80
     * @param EntityManagerInterface $em
81
     * @param object                 $entity
82
     * @param array                  $ch
83
     *
84
     * @throws \Doctrine\DBAL\DBALException
85
     * @throws \Doctrine\ORM\Mapping\MappingException
86
     *
87
     * @return array
88
     */
89
    public function diff(EntityManagerInterface $em, $entity, array $ch): array
90
    {
91
        /** @var ClassMetadata $meta */
92
        $meta = $em->getClassMetadata(\get_class($entity));
93
        $diff = [];
94
95
        foreach ($ch as $fieldName => list($old, $new)) {
96
            $o = null;
97
            $n = null;
98
99
            if (
100
                $meta->hasField($fieldName) &&
101
                !isset($meta->embeddedClasses[$fieldName]) &&
102
                $this->configuration->isAuditedField($entity, $fieldName)
103
            ) {
104
                $mapping = $meta->fieldMappings[$fieldName];
105
                $type = Type::getType($mapping['type']);
106
                $o = $this->value($em, $type, $old);
107
                $n = $this->value($em, $type, $new);
108
            } elseif (
109
                $meta->hasAssociation($fieldName) &&
110
                $meta->isSingleValuedAssociation($fieldName) &&
111
                $this->configuration->isAuditedField($entity, $fieldName)
112
            ) {
113
                $o = $this->summarize($em, $old);
114
                $n = $this->summarize($em, $new);
115
            }
116
117
            if ($o !== $n) {
118
                $diff[$fieldName] = [
119
                    'old' => $o,
120
                    'new' => $n,
121
                ];
122
            }
123
        }
124
        ksort($diff);
125
126
        return $diff;
127
    }
128
129
    /**
130
     * Blames an audit operation.
131
     *
132
     * @return array
133
     */
134
    public function blame(): array
135
    {
136
        $user_id = null;
137
        $username = null;
138
        $client_ip = null;
139
        $user_fqdn = null;
140
        $user_firewall = null;
141
142
        $request = $this->configuration->getRequestStack()->getCurrentRequest();
143
        if (null !== $request) {
144
            $client_ip = $request->getClientIp();
145
            $user_firewall = null === $this->configuration->getFirewallMap()->getFirewallConfig($request) ? null : $this->configuration->getFirewallMap()->getFirewallConfig($request)->getName();
146
        }
147
148
        $user = null === $this->configuration->getUserProvider() ? null : $this->configuration->getUserProvider()->getUser();
149
        if ($user instanceof UserInterface) {
150
            $user_id = $user->getId();
151
            $username = $user->getUsername();
152
            $user_fqdn = \get_class($user);
153
        }
154
155
        return [
156
            'user_id' => $user_id,
157
            'username' => $username,
158
            'client_ip' => $client_ip,
159
            'user_fqdn' => $user_fqdn,
160
            'user_firewall' => $user_firewall,
161
        ];
162
    }
163
164
    /**
165
     * Returns an array describing an entity.
166
     *
167
     * @param EntityManagerInterface $em
168
     * @param object                 $entity
169
     * @param mixed                  $id
170
     *
171
     * @throws \Doctrine\DBAL\DBALException
172
     * @throws \Doctrine\ORM\Mapping\MappingException
173
     *
174
     * @return array
175
     */
176
    public function summarize(EntityManagerInterface $em, $entity = null, $id = null): ?array
177
    {
178
        if (null === $entity) {
179
            return null;
180
        }
181
182
        $em->getUnitOfWork()->initializeObject($entity); // ensure that proxies are initialized
183
        /** @var ClassMetadata $meta */
184
        $meta = $em->getClassMetadata(\get_class($entity));
185
        $pkName = $meta->getSingleIdentifierFieldName();
186
        $pkValue = $id ?? $this->id($em, $entity);
187
        // An added guard for proxies that fail to initialize.
188
        if (null === $pkValue) {
189
            return null;
190
        }
191
192
        if (method_exists($entity, '__toString')) {
193
            $label = (string) $entity;
194
        } else {
195
            $label = \get_class($entity).'#'.$pkValue;
196
        }
197
198
        return [
199
            'label' => $label,
200
            'class' => $meta->name,
201
            'table' => $meta->getTableName(),
202
            $pkName => $pkValue,
203
        ];
204
    }
205
206
    /**
207
     * Return columns of audit tables.
208
     *
209
     * @return array
210
     */
211
    public function getAuditTableColumns(): array
212
    {
213
        return [
214
            'id' => [
215
                'type' => self::getDoctrineType('INTEGER'),
216
                'options' => [
217
                    'autoincrement' => true,
218
                    'unsigned' => true,
219
                ],
220
            ],
221
            'type' => [
222
                'type' => self::getDoctrineType('STRING'),
223
                'options' => [
224
                    'notnull' => true,
225
                    'length' => 10,
226
                ],
227
            ],
228
            'object_id' => [
229
                'type' => self::getDoctrineType('STRING'),
230
                'options' => [
231
                    'notnull' => true,
232
                ],
233
            ],
234
            'discriminator' => [
235
                'type' => self::getDoctrineType('STRING'),
236
                'options' => [
237
                    'default' => null,
238
                    'notnull' => false,
239
                ],
240
            ],
241
            'transaction_hash' => [
242
                'type' => self::getDoctrineType('STRING'),
243
                'options' => [
244
                    'notnull' => false,
245
                    'length' => 40,
246
                ],
247
            ],
248
            'diffs' => [
249
                'type' => self::getDoctrineType('JSON'),
250
                'options' => [
251
                    'default' => null,
252
                    'notnull' => false,
253
                ],
254
            ],
255
            'blame_id' => [
256
                'type' => self::getDoctrineType('STRING'),
257
                'options' => [
258
                    'default' => null,
259
                    'notnull' => false,
260
                ],
261
            ],
262
            'blame_user' => [
263
                'type' => self::getDoctrineType('STRING'),
264
                'options' => [
265
                    'default' => null,
266
                    'notnull' => false,
267
                    'length' => 255,
268
                ],
269
            ],
270
            'blame_user_fqdn' => [
271
                'type' => self::getDoctrineType('STRING'),
272
                'options' => [
273
                    'default' => null,
274
                    'notnull' => false,
275
                    'length' => 255,
276
                ],
277
            ],
278
            'blame_user_firewall' => [
279
                'type' => self::getDoctrineType('STRING'),
280
                'options' => [
281
                    'default' => null,
282
                    'notnull' => false,
283
                    'length' => 100,
284
                ],
285
            ],
286
            'ip' => [
287
                'type' => self::getDoctrineType('STRING'),
288
                'options' => [
289
                    'default' => null,
290
                    'notnull' => false,
291
                    'length' => 45,
292
                ],
293
            ],
294
            'created_at' => [
295
                'type' => self::getDoctrineType('DATETIME_IMMUTABLE'),
296
                'options' => [
297
                    'notnull' => true,
298
                ],
299
            ],
300
        ];
301
    }
302
303
    public function getAuditTableIndices(string $tablename): array
304
    {
305
        return [
306
            'id' => [
307
                'type' => 'primary',
308
            ],
309
            'type' => [
310
                'type' => 'index',
311
                'name' => 'type_'.md5($tablename).'_idx',
312
            ],
313
            'object_id' => [
314
                'type' => 'index',
315
                'name' => 'object_id_'.md5($tablename).'_idx',
316
            ],
317
            'discriminator' => [
318
                'type' => 'index',
319
                'name' => 'discriminator_'.md5($tablename).'_idx',
320
            ],
321
            'transaction_hash' => [
322
                'type' => 'index',
323
                'name' => 'transaction_hash_'.md5($tablename).'_idx',
324
            ],
325
            'blame_id' => [
326
                'type' => 'index',
327
                'name' => 'blame_id_'.md5($tablename).'_idx',
328
            ],
329
            'created_at' => [
330
                'type' => 'index',
331
                'name' => 'created_at_'.md5($tablename).'_idx',
332
            ],
333
        ];
334
    }
335
336
    public static function paramToNamespace(string $entity): string
337
    {
338
        return str_replace('-', '\\', $entity);
339
    }
340
341
    public static function namespaceToParam(string $entity): string
342
    {
343
        return str_replace('\\', '-', $entity);
344
    }
345
346
    /**
347
     * Type converts the input value and returns it.
348
     *
349
     * @param EntityManagerInterface $em
350
     * @param Type                   $type
351
     * @param mixed                  $value
352
     *
353
     * @throws \Doctrine\DBAL\DBALException
354
     *
355
     * @return mixed
356
     */
357
    private function value(EntityManagerInterface $em, Type $type, $value)
358
    {
359
        if (null === $value) {
360
            return null;
361
        }
362
363
        $platform = $em->getConnection()->getDatabasePlatform();
364
365
        switch ($type->getName()) {
366
            case self::getDoctrineType('BIGINT'):
367
                $convertedValue = (string) $value;
368
369
                break;
370
            case self::getDoctrineType('INTEGER'):
371
            case self::getDoctrineType('SMALLINT'):
372
                $convertedValue = (int) $value;
373
374
                break;
375
            case self::getDoctrineType('DECIMAL'):
376
            case self::getDoctrineType('FLOAT'):
377
            case self::getDoctrineType('BOOLEAN'):
378
                $convertedValue = $type->convertToPHPValue($value, $platform);
379
380
                break;
381
            default:
382
                $convertedValue = $type->convertToDatabaseValue($value, $platform);
383
        }
384
385
        return $convertedValue;
386
    }
387
388
    private static function getDoctrineType(string $type): string
389
    {
390
        return \constant((class_exists(Types::class, false) ? Types::class : Type::class).'::'.$type);
391
    }
392
}
393