Passed
Pull Request — master (#125)
by Damien
04:28 queued 49s
created

AuditHelper   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 398
Duplicated Lines 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 199
c 6
b 0
f 0
dl 0
loc 398
rs 9.1199
wmc 41

11 Methods

Rating   Name   Duplication   Size   Complexity  
A id() 0 29 2
A summarize() 0 27 4
A __construct() 0 3 1
B diff() 0 38 9
A namespaceToParam() 0 3 1
A getAuditTableIndices() 0 29 1
A getConfiguration() 0 3 1
A blame() 0 27 5
A paramToNamespace() 0 3 1
B getAuditTableColumns() 0 87 1
C value() 0 53 15

How to fix   Complexity   

Complex Class

Complex classes like AuditHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AuditHelper, and based on these observations, apply Extract Interface, too.

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' => Types::INTEGER,
216
                'options' => [
217
                    'autoincrement' => true,
218
                    'unsigned' => true,
219
                ],
220
            ],
221
            'type' => [
222
                'type' => Types::STRING,
223
                'options' => [
224
                    'notnull' => true,
225
                    'length' => 10,
226
                ],
227
            ],
228
            'object_id' => [
229
                'type' => Types::STRING,
230
                'options' => [
231
                    'notnull' => true,
232
                ],
233
            ],
234
            'discriminator' => [
235
                'type' => Types::STRING,
236
                'options' => [
237
                    'default' => null,
238
                    'notnull' => false,
239
                ],
240
            ],
241
            'transaction_hash' => [
242
                'type' => Types::STRING,
243
                'options' => [
244
                    'notnull' => false,
245
                    'length' => 40,
246
                ],
247
            ],
248
            'diffs' => [
249
                'type' => Types::JSON,
250
                'options' => [
251
                    'default' => null,
252
                    'notnull' => false,
253
                ],
254
            ],
255
            'blame_id' => [
256
                'type' => Types::STRING,
257
                'options' => [
258
                    'default' => null,
259
                    'notnull' => false,
260
                ],
261
            ],
262
            'blame_user' => [
263
                'type' => Types::STRING,
264
                'options' => [
265
                    'default' => null,
266
                    'notnull' => false,
267
                    'length' => 255,
268
                ],
269
            ],
270
            'blame_user_fqdn' => [
271
                'type' => Types::STRING,
272
                'options' => [
273
                    'default' => null,
274
                    'notnull' => false,
275
                    'length' => 255,
276
                ],
277
            ],
278
            'blame_user_firewall' => [
279
                'type' => Types::STRING,
280
                'options' => [
281
                    'default' => null,
282
                    'notnull' => false,
283
                    'length' => 100,
284
                ],
285
            ],
286
            'ip' => [
287
                'type' => Types::STRING,
288
                'options' => [
289
                    'default' => null,
290
                    'notnull' => false,
291
                    'length' => 45,
292
                ],
293
            ],
294
            'created_at' => [
295
                'type' => Types::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
        if (!class_exists('\Doctrine\DBAL\Types\Types', false)) {
366
            switch ($type->getName()) {
367
                case Type::BIGINT:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::BIGINT has been deprecated: Use {@see DefaultTypes::BIGINT} instead. ( Ignorable by Annotation )

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

367
                case /** @scrutinizer ignore-deprecated */ Type::BIGINT:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
368
                    $convertedValue = (string) $value;
369
370
                    break;
371
                case Type::INTEGER:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::INTEGER has been deprecated: Use {@see DefaultTypes::INTEGER} instead. ( Ignorable by Annotation )

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

371
                case /** @scrutinizer ignore-deprecated */ Type::INTEGER:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
372
                case Type::SMALLINT:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::SMALLINT has been deprecated: Use {@see DefaultTypes::SMALLINT} instead. ( Ignorable by Annotation )

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

372
                case /** @scrutinizer ignore-deprecated */ Type::SMALLINT:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
373
                    $convertedValue = (int) $value;
374
375
                    break;
376
                case Type::DECIMAL:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::DECIMAL has been deprecated: Use {@see DefaultTypes::DECIMAL} instead. ( Ignorable by Annotation )

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

376
                case /** @scrutinizer ignore-deprecated */ Type::DECIMAL:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
377
                case Type::FLOAT:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::FLOAT has been deprecated: Use {@see DefaultTypes::FLOAT} instead. ( Ignorable by Annotation )

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

377
                case /** @scrutinizer ignore-deprecated */ Type::FLOAT:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
378
                case Type::BOOLEAN:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::BOOLEAN has been deprecated: Use {@see DefaultTypes::BOOLEAN} instead. ( Ignorable by Annotation )

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

378
                case /** @scrutinizer ignore-deprecated */ Type::BOOLEAN:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
379
                    $convertedValue = $type->convertToPHPValue($value, $platform);
380
381
                    break;
382
                default:
383
                    $convertedValue = $type->convertToDatabaseValue($value, $platform);
384
            }
385
386
            return $convertedValue;
387
        }
388
389
        switch ($type->getName()) {
390
            case Types::BIGINT:
391
                $convertedValue = (string) $value;
392
393
                break;
394
            case Types::INTEGER:
395
            case Types::SMALLINT:
396
                $convertedValue = (int) $value;
397
398
                break;
399
            case Types::DECIMAL:
400
            case Types::FLOAT:
401
            case Types::BOOLEAN:
402
                $convertedValue = $type->convertToPHPValue($value, $platform);
403
404
                break;
405
            default:
406
                $convertedValue = $type->convertToDatabaseValue($value, $platform);
407
        }
408
409
        return $convertedValue;
410
    }
411
}
412