Passed
Pull Request — master (#187)
by
unknown
12:29
created

AuditTrait::extraFields()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 17
rs 9.9666
cc 3
nc 3
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace DH\Auditor\Provider\Doctrine\Auditing\Transaction;
6
7
use DH\Auditor\Exception\MappingException;
8
use DH\Auditor\Provider\Doctrine\Persistence\Helper\DoctrineHelper;
9
use DH\Auditor\User\UserInterface;
10
use Doctrine\DBAL\Types\Type;
11
use Doctrine\ORM\EntityManagerInterface;
12
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
13
use Symfony\Component\PropertyAccess\PropertyAccess;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\PropertyAccess\PropertyAccess was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use Throwable;
15
use UnitEnum;
0 ignored issues
show
Bug introduced by
The type UnitEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
17
trait AuditTrait
18
{
19
    /**
20
     * Returns the primary key value of an entity.
21
     *
22
     * @throws \DH\Auditor\Exception\MappingException
23
     * @throws \Doctrine\DBAL\Exception
24
     * @throws \Doctrine\ORM\Mapping\MappingException
25
     */
26
    private function id(EntityManagerInterface $entityManager, object $entity): mixed
27
    {
28
        $meta = $entityManager->getClassMetadata(DoctrineHelper::getRealClassName($entity));
29
30
        try {
31
            $pk = $meta->getSingleIdentifierFieldName();
32
        } catch (ORMMappingException) {
33
            throw new MappingException(sprintf('Composite primary keys are not supported (%s).', $entity::class));
34
        }
35
36
        if (isset($meta->fieldMappings[$pk])) {
37
            $type = Type::getType($meta->fieldMappings[$pk]['type']);
38
39
            return $this->value($entityManager, $type, $meta->getReflectionProperty($pk)->getValue($entity));
40
        }
41
42
        /**
43
         * Primary key is not part of fieldMapping.
44
         *
45
         * @see https://github.com/DamienHarper/auditor-bundle/issues/40
46
         * @see https://www.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/composite-primary-keys.html#identity-through-foreign-entities
47
         * We try to get it from associationMapping (will throw a MappingException if not available)
48
         */
49
        $targetEntity = $meta->getReflectionProperty($pk)->getValue($entity);
50
51
        $mapping = $meta->getAssociationMapping($pk);
52
53
        $meta = $entityManager->getClassMetadata($mapping['targetEntity']);
54
        $pk = $meta->getSingleIdentifierFieldName();
55
        $type = Type::getType($meta->fieldMappings[$pk]['type']);
56
57
        \assert(\is_object($targetEntity));
58
59
        return $this->value($entityManager, $type, $meta->getReflectionProperty($pk)->getValue($targetEntity));
60
    }
61
62
    /**
63
     * Type converts the input value and returns it.
64
     *
65
     * @throws \Doctrine\DBAL\Exception
66
     * @throws \Doctrine\DBAL\Types\ConversionException
67
     */
68
    private function value(EntityManagerInterface $entityManager, Type $type, mixed $value): mixed
69
    {
70
        if (null === $value) {
71
            return null;
72
        }
73
74
        if (interface_exists(UnitEnum::class) && $value instanceof UnitEnum && property_exists($value, 'value')) { /** @phpstan-ignore-line */
75
            $value = $value->value;
76
        }
77
78
        $platform = $entityManager->getConnection()->getDatabasePlatform();
79
80
        switch (array_search($type::class, Type::getTypesMap(), true)) {
81
            case DoctrineHelper::getDoctrineType('BIGINT'):
82
                $convertedValue = (string) $value;  // @phpstan-ignore-line
83
84
                break;
85
86
            case DoctrineHelper::getDoctrineType('INTEGER'):
87
            case DoctrineHelper::getDoctrineType('SMALLINT'):
88
                $convertedValue = (int) $value; // @phpstan-ignore-line
89
90
                break;
91
92
            case DoctrineHelper::getDoctrineType('DECIMAL'):
93
            case DoctrineHelper::getDoctrineType('FLOAT'):
94
            case DoctrineHelper::getDoctrineType('BOOLEAN'):
95
                $convertedValue = $type->convertToPHPValue($value, $platform);
96
97
                break;
98
99
            case 'uuid_binary':
100
            case 'uuid_binary_ordered_time':
101
            case 'uuid':
102
            case 'ulid':
103
                // Ramsey UUID / Symfony UID (UUID/ULID)
104
                $convertedValue = (string) $value;  // @phpstan-ignore-line
105
106
                break;
107
108
            case DoctrineHelper::getDoctrineType('BLOB'):
109
            case DoctrineHelper::getDoctrineType('BINARY'):
110
                if (\is_resource($value)) {
111
                    // let's replace resources with a "simple" representation: resourceType#resourceId
112
                    $convertedValue = get_resource_type($value).'#'.get_resource_id($value);
113
                } else {
114
                    $convertedValue = $type->convertToDatabaseValue($value, $platform);
115
                }
116
117
                break;
118
119
            case DoctrineHelper::getDoctrineType('JSON'):
120
                return $value;
121
122
            default:
123
                $convertedValue = $type->convertToDatabaseValue($value, $platform);
124
        }
125
126
        return $convertedValue;
127
    }
128
129
    /**
130
     * Returns the extra fields if set.
131
     */
132
    private function extraFields(object $entity): array
133
    {
134
        $configuration = $this->provider->getConfiguration();
135
        $extraFieldProperties = array_keys($configuration->getExtraFields());
136
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
137
138
        $extraFields = [];
139
140
        foreach ($extraFieldProperties as $extraField) {
141
            if (!$propertyAccessor->isReadable($entity, $extraField)) {
142
                continue;
143
            }
144
145
            $extraFields[$extraField] = $propertyAccessor->getValue($entity, $extraField);
146
        }
147
148
        return $extraFields;
149
    }
150
151
    /**
152
     * Computes a usable diff.
153
     *
154
     * @throws \DH\Auditor\Exception\MappingException
155
     * @throws \Doctrine\DBAL\Exception
156
     * @throws \Doctrine\DBAL\Types\ConversionException
157
     * @throws \Doctrine\ORM\Mapping\MappingException
158
     */
159
    private function diff(EntityManagerInterface $entityManager, object $entity, array $changeset): array
160
    {
161
        $meta = $entityManager->getClassMetadata(DoctrineHelper::getRealClassName($entity));
162
        $diff = [
163
            '@source' => $this->summarize($entityManager, $entity),
164
        ];
165
166
        foreach ($changeset as $fieldName => [$old, $new]) {
167
            $o = null;
168
            $n = null;
169
170
            // skip if $old and $new are null
171
            if (null === $old && null === $new) {
172
                continue;
173
            }
174
175
            if (
176
                !isset($meta->embeddedClasses[$fieldName])
177
                && $meta->hasField($fieldName)
178
                && $this->provider->isAuditedField($entity, $fieldName)
179
            ) {
180
                $mapping = $meta->fieldMappings[$fieldName];
181
                $type = Type::getType($mapping['type']);
182
                $o = $this->value($entityManager, $type, $old);
183
                $n = $this->value($entityManager, $type, $new);
184
            } elseif (
185
                $meta->hasAssociation($fieldName)
186
                && $meta->isSingleValuedAssociation($fieldName)
187
                && $this->provider->isAuditedField($entity, $fieldName)
188
            ) {
189
                $o = $this->summarize($entityManager, $old);
190
                $n = $this->summarize($entityManager, $new);
191
            }
192
193
            if ($o !== $n) {
194
                $diff[$fieldName] = [
195
                    'new' => $n,
196
                    'old' => $o,
197
                ];
198
            }
199
        }
200
201
        return $diff;
202
    }
203
204
    /**
205
     * Returns an array describing an entity.
206
     *
207
     * @throws \DH\Auditor\Exception\MappingException
208
     * @throws \Doctrine\DBAL\Exception
209
     * @throws \Doctrine\ORM\Mapping\MappingException
210
     */
211
    private function summarize(EntityManagerInterface $entityManager, ?object $entity = null, array $extra = []): ?array
212
    {
213
        if (null === $entity) {
214
            return null;
215
        }
216
217
        $entityManager->getUnitOfWork()->initializeObject($entity); // ensure that proxies are initialized
218
        $meta = $entityManager->getClassMetadata(DoctrineHelper::getRealClassName($entity));
219
220
        $pkValue = $extra['id'] ?? $this->id($entityManager, $entity);
221
        $pkName = $meta->getSingleIdentifierFieldName();
222
223
        if (method_exists($entity, '__toString')) {
224
            try {
225
                $label = (string) $entity;
226
            } catch (Throwable) {
0 ignored issues
show
Unused Code introduced by
catch (\Throwable) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
227
                $label = DoctrineHelper::getRealClassName($entity).(null === $pkValue ? '' : '#'.$pkValue);
228
            }
229
        } else {
230
            $label = DoctrineHelper::getRealClassName($entity).(null === $pkValue ? '' : '#'.$pkValue);
231
        }
232
233
        if ('id' !== $pkName) {
234
            $extra['pkName'] = $pkName;
235
        }
236
237
        return [
238
            $pkName => $pkValue,
239
            'class' => $meta->name,
240
            'label' => $label,
241
            'table' => $meta->getTableName(),
242
        ] + $extra;
243
    }
244
245
    /**
246
     * Blames an audit operation.
247
     *
248
     * @return array{client_ip: null|string, user_firewall: null|string, user_fqdn: null|string, user_id: null|string, username: null|string}
249
     */
250
    private function blame(): array
251
    {
252
        $user_id = null;
253
        $username = null;
254
        $client_ip = null;
255
        $user_fqdn = null;
256
        $user_firewall = null;
257
258
        $securityProvider = $this->provider->getAuditor()->getConfiguration()->getSecurityProvider();
259
        if (null !== $securityProvider) {
260
            [$client_ip, $user_firewall] = $securityProvider();
261
        }
262
263
        $userProvider = $this->provider->getAuditor()->getConfiguration()->getUserProvider();
264
        $user = null === $userProvider ? null : $userProvider();
265
        if ($user instanceof UserInterface) {
266
            $user_id = $user->getIdentifier();
267
            $username = $user->getUsername();
268
            $user_fqdn = DoctrineHelper::getRealClassName($user);
269
        }
270
271
        return [
272
            'client_ip' => $client_ip,
273
            'user_firewall' => $user_firewall,
274
            'user_fqdn' => $user_fqdn,
275
            'user_id' => $user_id,
276
            'username' => $username,
277
        ];
278
    }
279
}
280