AuditTrait::blame()   A
last analyzed

Complexity

Conditions 4
Paths 8

Size

Total Lines 27
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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