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' => class_exists('DoctrineDBALTypesTypes', false) ? Types::INTEGER : Type::INTEGER, |
|
|
|
|
216
|
|
|
'options' => [ |
217
|
|
|
'autoincrement' => true, |
218
|
|
|
'unsigned' => true, |
219
|
|
|
], |
220
|
|
|
], |
221
|
|
|
'type' => [ |
222
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::STRING : Type::STRING, |
|
|
|
|
223
|
|
|
'options' => [ |
224
|
|
|
'notnull' => true, |
225
|
|
|
'length' => 10, |
226
|
|
|
], |
227
|
|
|
], |
228
|
|
|
'object_id' => [ |
229
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::STRING : Type::STRING, |
|
|
|
|
230
|
|
|
'options' => [ |
231
|
|
|
'notnull' => true, |
232
|
|
|
], |
233
|
|
|
], |
234
|
|
|
'discriminator' => [ |
235
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::STRING : Type::STRING, |
|
|
|
|
236
|
|
|
'options' => [ |
237
|
|
|
'default' => null, |
238
|
|
|
'notnull' => false, |
239
|
|
|
], |
240
|
|
|
], |
241
|
|
|
'transaction_hash' => [ |
242
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::STRING : Type::STRING, |
|
|
|
|
243
|
|
|
'options' => [ |
244
|
|
|
'notnull' => false, |
245
|
|
|
'length' => 40, |
246
|
|
|
], |
247
|
|
|
], |
248
|
|
|
'diffs' => [ |
249
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::JSON : Type::JSON, |
|
|
|
|
250
|
|
|
'options' => [ |
251
|
|
|
'default' => null, |
252
|
|
|
'notnull' => false, |
253
|
|
|
], |
254
|
|
|
], |
255
|
|
|
'blame_id' => [ |
256
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::STRING : Type::STRING, |
|
|
|
|
257
|
|
|
'options' => [ |
258
|
|
|
'default' => null, |
259
|
|
|
'notnull' => false, |
260
|
|
|
], |
261
|
|
|
], |
262
|
|
|
'blame_user' => [ |
263
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::STRING : Type::STRING, |
|
|
|
|
264
|
|
|
'options' => [ |
265
|
|
|
'default' => null, |
266
|
|
|
'notnull' => false, |
267
|
|
|
'length' => 255, |
268
|
|
|
], |
269
|
|
|
], |
270
|
|
|
'blame_user_fqdn' => [ |
271
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::STRING : Type::STRING, |
|
|
|
|
272
|
|
|
'options' => [ |
273
|
|
|
'default' => null, |
274
|
|
|
'notnull' => false, |
275
|
|
|
'length' => 255, |
276
|
|
|
], |
277
|
|
|
], |
278
|
|
|
'blame_user_firewall' => [ |
279
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::STRING : Type::STRING, |
|
|
|
|
280
|
|
|
'options' => [ |
281
|
|
|
'default' => null, |
282
|
|
|
'notnull' => false, |
283
|
|
|
'length' => 100, |
284
|
|
|
], |
285
|
|
|
], |
286
|
|
|
'ip' => [ |
287
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::STRING : Type::STRING, |
|
|
|
|
288
|
|
|
'options' => [ |
289
|
|
|
'default' => null, |
290
|
|
|
'notnull' => false, |
291
|
|
|
'length' => 45, |
292
|
|
|
], |
293
|
|
|
], |
294
|
|
|
'created_at' => [ |
295
|
|
|
'type' => class_exists('DoctrineDBALTypesTypes', false) ? Types::DATETIME_IMMUTABLE : Type::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 class_exists('DoctrineDBALTypesTypes', false) ? Types::BIGINT : Type::BIGINT: |
|
|
|
|
367
|
|
|
$convertedValue = (string) $value; |
368
|
|
|
|
369
|
|
|
break; |
370
|
|
|
case class_exists('DoctrineDBALTypesTypes', false) ? Types::INTEGER : Type::INTEGER: |
|
|
|
|
371
|
|
|
case class_exists('DoctrineDBALTypesTypes', false) ? Types::SMALLINT : Type::SMALLINT: |
|
|
|
|
372
|
|
|
$convertedValue = (int) $value; |
373
|
|
|
|
374
|
|
|
break; |
375
|
|
|
case class_exists('DoctrineDBALTypesTypes', false) ? Types::DECIMAL : Type::DECIMAL: |
|
|
|
|
376
|
|
|
case class_exists('DoctrineDBALTypesTypes', false) ? Types::FLOAT : Type::FLOAT: |
|
|
|
|
377
|
|
|
case class_exists('DoctrineDBALTypesTypes', false) ? Types::BOOLEAN : Type::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
|
|
|
|
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.