LoggableManager::addConfig()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace Oro\Bundle\DataAuditBundle\Loggable;
4
5
use Symfony\Component\Routing\Exception\InvalidParameterException;
6
use Symfony\Component\Security\Core\SecurityContextInterface;
7
8
use Doctrine\ORM\UnitOfWork;
9
use Doctrine\ORM\PersistentCollection;
10
use Doctrine\ORM\EntityManager;
11
use Doctrine\Common\Util\ClassUtils;
12
use Doctrine\DBAL\Types\Type;
13
use Doctrine\ORM\Mapping\ClassMetadata as DoctrineClassMetadata;
14
15
use Oro\Bundle\DataAuditBundle\Entity\AbstractAudit;
16
use Oro\Bundle\DataAuditBundle\Metadata\ClassMetadata;
17
use Oro\Bundle\DataAuditBundle\Metadata\PropertyMetadata;
18
use Oro\Bundle\UserBundle\Entity\AbstractUser;
19
use Oro\Bundle\OrganizationBundle\Entity\Organization;
20
use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider;
21
use Oro\Bundle\EntityConfigBundle\DependencyInjection\Utils\ServiceLink;
22
use Oro\Bundle\SecurityBundle\Authentication\Token\OrganizationContextTokenInterface;
23
24
/**
25
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
26
 * TODO: This class should be refactored  (BAP-978)
27
 */
28
class LoggableManager
29
{
30
    /**
31
     * @var AbstractUser[]
32
     */
33
    protected static $userCache = [];
34
35
    const ACTION_CREATE = 'create';
36
    const ACTION_UPDATE = 'update';
37
    const ACTION_REMOVE = 'remove';
38
39
    /** @var EntityManager */
40
    protected $em;
41
42
    /** @var array */
43
    protected $configs = [];
44
45
    /** @var string */
46
    protected $username;
47
48
    /** @var Organization|null */
49
    protected $organization;
50
51
    /**
52
     * @deprecated 1.8.0:2.1.0 use AuditEntityMapper::getAuditEntryClass
53
     *
54
     * @var string
55
     */
56
    protected $logEntityClass;
57
58
    /**
59
     * @deprecated 1.8.0:2.1.0 use AuditEntityMapper::getAuditEntryFieldClass
60
     *
61
     * @var string
62
     */
63
    protected $logEntityFieldClass;
64
65
    /** @var array */
66
    protected $pendingLogEntityInserts = [];
67
68
    /** @var array */
69
    protected $pendingRelatedEntities = [];
70
71
    /** @var array */
72
    protected $collectionLogData = [];
73
74
    /** @var ConfigProvider */
75
    protected $auditConfigProvider;
76
77
    /** @var ServiceLink  */
78
    protected $securityContextLink;
79
80
    /** @var AuditEntityMapper  */
81
    protected $auditEntityMapper;
82
83
    /**
84
     * @param string $logEntityClass
85
     * @param string $logEntityFieldClass
86
     * @param ConfigProvider $auditConfigProvider
87
     * @param ServiceLink $securityContextLink
88
     * @param AuditEntityMapper $auditEntityMapper
89
     */
90
    public function __construct(
91
        $logEntityClass,
92
        $logEntityFieldClass,
93
        ConfigProvider $auditConfigProvider,
94
        ServiceLink $securityContextLink,
95
        AuditEntityMapper $auditEntityMapper
96
    ) {
97
        $this->auditConfigProvider = $auditConfigProvider;
98
        $this->logEntityClass = $logEntityClass;
0 ignored issues
show
Deprecated Code introduced by
The property Oro\Bundle\DataAuditBund...anager::$logEntityClass has been deprecated with message: 1.8.0:2.1.0 use AuditEntityMapper::getAuditEntryClass

This property 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 property will be removed from the class and what other property to use instead.

Loading history...
99
        $this->logEntityFieldClass = $logEntityFieldClass;
0 ignored issues
show
Deprecated Code introduced by
The property Oro\Bundle\DataAuditBund...r::$logEntityFieldClass has been deprecated with message: 1.8.0:2.1.0 use AuditEntityMapper::getAuditEntryFieldClass

This property 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 property will be removed from the class and what other property to use instead.

Loading history...
100
        $this->securityContextLink = $securityContextLink;
101
        $this->auditEntityMapper = $auditEntityMapper;
102
    }
103
104
    /**
105
     * @param ClassMetadata $metadata
106
     */
107
    public function addConfig(ClassMetadata $metadata)
108
    {
109
        $this->configs[$metadata->name] = $metadata;
110
    }
111
112
    /**
113
     * @param string $name
114
     * @return ClassMetadata
115
     * @throws InvalidParameterException
116
     */
117
    public function getConfig($name)
118
    {
119
        if (!isset($this->configs[$name])) {
120
            throw new InvalidParameterException(sprintf('invalid config name %s', $name));
121
        }
122
123
        return $this->configs[$name];
124
    }
125
126
    /**
127
     * @param string $username
128
     * @throws \InvalidArgumentException
129
     */
130
    public function setUsername($username)
131
    {
132
        if (is_string($username)) {
133
            $this->username = $username;
134
        } elseif (is_object($username) && method_exists($username, 'getUsername')) {
135
            $this->username = (string) $username->getUsername();
136
        } else {
137
            throw new \InvalidArgumentException('Username must be a string, or object should have method: getUsername');
138
        }
139
    }
140
141
    /**
142
     * @return null|Organization
143
     */
144
    protected function getOrganization()
145
    {
146
        /** @var SecurityContextInterface $securityContext */
147
        $securityContext = $this->securityContextLink->getService();
148
149
        $token = $securityContext->getToken();
150
        if (!$token) {
151
            return null;
152
        }
153
154
        if (!$token instanceof OrganizationContextTokenInterface) {
155
            return null;
156
        }
157
158
        return $token->getOrganizationContext();
159
    }
160
161
    /**
162
     * @param EntityManager $em
163
     */
164
    public function handleLoggable(EntityManager $em)
0 ignored issues
show
Bug introduced by
You have injected the EntityManager via parameter $em. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

Loading history...
165
    {
166
        $this->em = $em;
167
        $uow      = $em->getUnitOfWork();
168
169
        $collections = array_merge($uow->getScheduledCollectionUpdates(), $uow->getScheduledCollectionDeletions());
170
        foreach ($collections as $collection) {
171
            $this->calculateActualCollectionData($collection);
172
        }
173
174
        $entities = array_merge(
175
            $uow->getScheduledEntityDeletions(),
176
            $uow->getScheduledEntityInsertions(),
177
            $uow->getScheduledEntityUpdates()
178
        );
179
180
        $updatedEntities = [];
181
        foreach ($entities as $entity) {
182
            $entityMeta      = $this->em->getClassMetadata(ClassUtils::getClass($entity));
183
            $updatedEntities = array_merge(
184
                $updatedEntities,
185
                $this->calculateManyToOneData($entityMeta, $entity)
186
            );
187
        }
188
189
        foreach ($uow->getScheduledEntityInsertions() as $entity) {
190
            $this->createLogEntity(self::ACTION_CREATE, $entity);
191
        }
192
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
193
            $this->createLogEntity(self::ACTION_UPDATE, $entity);
194
        }
195
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
196
            $this->createLogEntity(self::ACTION_REMOVE, $entity);
197
        }
198
199
        foreach ($this->collectionLogData as $entityData) {
200
            foreach ($entityData as $identifier => $values) {
201
                if (!isset($updatedEntities[$identifier])) {
202
                    continue;
203
                }
204
205
                $this->createLogEntity(static::ACTION_UPDATE, $updatedEntities[$identifier]);
206
            }
207
        }
208
    }
209
210
    /**
211
     * @param object $entity
212
     * @param EntityManager $em
213
     */
214
    public function handlePostPersist($entity, EntityManager $em)
0 ignored issues
show
Bug introduced by
You have injected the EntityManager via parameter $em. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

Loading history...
215
    {
216
        $this->em = $em;
217
        $uow = $em->getUnitOfWork();
218
        $oid = spl_object_hash($entity);
219
        $logEntryMeta = null;
220
221
        if ($this->pendingLogEntityInserts && array_key_exists($oid, $this->pendingLogEntityInserts)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->pendingLogEntityInserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
222
            $logEntry     = $this->pendingLogEntityInserts[$oid];
223
            $logEntryMeta = $em->getClassMetadata(ClassUtils::getClass($logEntry));
224
225
            $id = $this->getIdentifier($entity);
226
            $logEntryMeta->getReflectionProperty('objectId')->setValue($logEntry, $id);
227
228
            $uow->scheduleExtraUpdate($logEntry, ['objectId' => [null, $id]]);
229
            $uow->setOriginalEntityProperty(spl_object_hash($logEntry), 'objectId', $id);
230
231
            unset($this->pendingLogEntityInserts[$oid]);
232
        }
233
234
        if ($this->pendingRelatedEntities && array_key_exists($oid, $this->pendingRelatedEntities)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->pendingRelatedEntities of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
235
            $identifiers = $uow->getEntityIdentifier($entity);
236
237
            foreach ($this->pendingRelatedEntities[$oid] as $props) {
238
                /** @var AbstractAudit $logEntry */
239
                $logEntry = $props['log'];
240
                $data     = $logEntry->getData();
241
                if (empty($data[$props['field']]['new'])) {
242
                    $data[$props['field']]['new'] = implode(', ', $identifiers);
243
                    $oldField = $logEntry->getField($props['field']);
244
                    $logEntry->createField(
245
                        $oldField->getField(),
246
                        $oldField->getDataType(),
247
                        $data[$props['field']]['new'],
248
                        $oldField->getOldValue()
249
                    );
250
251
                    if ($logEntryMeta) {
252
                        $uow->computeChangeSet($logEntryMeta, $logEntry);
253
                    }
254
                    $uow->setOriginalEntityProperty(spl_object_hash($logEntry), 'objectId', $data);
255
                }
256
            }
257
258
            unset($this->pendingRelatedEntities[$oid]);
259
        }
260
    }
261
262
    /**
263
     * @param DoctrineClassMetadata $entityMeta
264
     * @param object                $entity
265
     *
266
     * @return array [entityIdentifier => entity, ...]
267
     */
268
    protected function calculateManyToOneData(DoctrineClassMetadata $entityMeta, $entity)
269
    {
270
        $entities = [];
271
        foreach ($entityMeta->associationMappings as $assoc) {
272
            if ($assoc['type'] !== DoctrineClassMetadata::MANY_TO_ONE
273
                || empty($assoc['inversedBy'])
274
            ) {
275
                continue;
276
            }
277
278
            $owner = $entityMeta->getReflectionProperty($assoc['fieldName'])->getValue($entity);
279
            if (!$owner) {
280
                continue;
281
            }
282
283
            $ownerMeta = $this->em->getClassMetadata($assoc['targetEntity']);
284
            $collection = $ownerMeta->getReflectionProperty($assoc['inversedBy'])->getValue($owner);
285
            if (!$collection instanceof PersistentCollection) {
286
                continue;
287
            }
288
289
            $entityIdentifier = $this->getEntityIdentifierString($owner);
290
            $this->calculateActualCollectionData($collection, $entityIdentifier);
291
292
            $entities[$entityIdentifier] = $owner;
293
        }
294
295
        return $entities;
296
    }
297
298
    /**
299
     * @param PersistentCollection $collection
300
     * @param string $entityIdentifier
301
     */
302
    protected function calculateActualCollectionData(PersistentCollection $collection, $entityIdentifier = null)
303
    {
304
        $ownerEntity = $collection->getOwner();
305
        $entityState = $this->em->getUnitOfWork()->getEntityState($ownerEntity, UnitOfWork::STATE_NEW);
306
        if ($entityState === UnitOfWork::STATE_REMOVED) {
307
            return;
308
        }
309
310
        $this->calculateCollectionData($collection, $entityIdentifier);
311
    }
312
313
    /**
314
     * @param PersistentCollection $collection
315
     * @param string $entityIdentifier
316
     */
317
    protected function calculateCollectionData(PersistentCollection $collection, $entityIdentifier = null)
318
    {
319
        $ownerEntity          = $collection->getOwner();
320
        $ownerEntityClassName = $this->getEntityClassName($ownerEntity);
321
322
        if ($this->checkAuditable($ownerEntityClassName)) {
323
            $meta              = $this->getConfig($ownerEntityClassName);
324
            $collectionMapping = $collection->getMapping();
325
326
            if (isset($meta->propertyMetadata[$collectionMapping['fieldName']])) {
327
                $method = $meta->propertyMetadata[$collectionMapping['fieldName']]->method;
328
329
                // calculate collection changes
330
                $newCollection = $collection->toArray();
331
                $oldCollection = $collection->getSnapshot();
332
333
                $oldCollectionWithOldData = [];
334
                foreach ($oldCollection as $entity) {
335
                    $oldCollectionWithOldData[] = $this->getOldEntity($entity);
336
                }
337
338
                $oldData = array_reduce(
339
                    $oldCollectionWithOldData,
340
                    function ($result, $item) use ($method) {
341
                        return $result . ($result ? ', ' : '') . $item->{$method}();
342
                    }
343
                );
344
345
                $newData = array_reduce(
346
                    $newCollection,
347
                    function ($result, $item) use ($method) {
348
                        return $result . ($result ? ', ' : '') . $item->{$method}();
349
                    }
350
                );
351
352
                if (!$entityIdentifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $entityIdentifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
353
                    $entityIdentifier = $this->getEntityIdentifierString($ownerEntity);
354
                }
355
356
                $fieldName = $collectionMapping['fieldName'];
357
                $this->collectionLogData[$ownerEntityClassName][$entityIdentifier][$fieldName] = [
358
                    'old' => $oldData,
359
                    'new' => $newData,
360
                ];
361
            }
362
        }
363
    }
364
365
    /**
366
     * @param object $currentEntity
367
     *
368
     * @return object
369
     */
370
    protected function getOldEntity($currentEntity)
371
    {
372
        $changeSet = $this->em->getUnitOfWork()->getEntityChangeSet($currentEntity);
373
374
        if (!$changeSet) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changeSet of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
375
            return $currentEntity;
376
        }
377
378
        $metadata = $this->em->getClassMetadata(ClassUtils::getClass($currentEntity));
379
        $oldEntity = clone $currentEntity;
380
        foreach ($changeSet as $property => $values) {
381
            $metadata->getReflectionProperty($property)->setValue($oldEntity, $values[0]);
382
        }
383
384
        return $oldEntity;
385
    }
386
387
    /**
388
     * @param string $action
389
     * @param object $entity
390
     * @SuppressWarnings(PHPMD.NPathComplexity)
391
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
392
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
393
     *
394
     * @throws \ReflectionException
395
     */
396
    protected function createLogEntity($action, $entity)
397
    {
398
        $entityClassName = $this->getEntityClassName($entity);
399
        if (!$this->checkAuditable($entityClassName)) {
400
            return;
401
        }
402
403
        $user = $this->getLoadedUser();
404
        $organization = $this->getOrganization();
405
        if (!$organization) {
406
            return;
407
        }
408
409
        $uow = $this->em->getUnitOfWork();
410
411
        $meta       = $this->getConfig($entityClassName);
412
        $entityMeta = $this->em->getClassMetadata($entityClassName);
413
414
        $logEntryMeta = $this->em->getClassMetadata($this->getLogEntityClass());
415
        /** @var AbstractAudit $logEntry */
416
        $logEntry = $logEntryMeta->newInstance();
417
        $logEntry->setAction($action);
418
        $logEntry->setObjectClass($meta->name);
419
        $logEntry->setLoggedAt();
420
        $logEntry->setUser($user);
0 ignored issues
show
Bug introduced by
It seems like $user defined by $this->getLoadedUser() on line 403 can also be of type object; however, Oro\Bundle\DataAuditBund...bstractAudit::setUser() does only seem to accept null|object<Oro\Bundle\U...le\Entity\AbstractUser>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
421
        $logEntry->setOrganization($organization);
422
        $logEntry->setObjectName(method_exists($entity, '__toString') ? (string)$entity : $meta->name);
423
424
        $entityId = $this->getIdentifier($entity);
425
426
        if (!$entityId && $action === self::ACTION_CREATE) {
427
            $this->pendingLogEntityInserts[spl_object_hash($entity)] = $logEntry;
428
        }
429
430
        $logEntry->setObjectId($entityId);
431
432
        $newValues = [];
433
434
        if ($action !== self::ACTION_REMOVE && count($meta->propertyMetadata)) {
435
            foreach ($uow->getEntityChangeSet($entity) as $field => $changes) {
436
                if (!isset($meta->propertyMetadata[$field])) {
437
                    continue;
438
                }
439
440
                $old = $changes[0];
441
                $new = $changes[1];
442
443
                if ($old == $new) {
444
                    continue;
445
                }
446
447
                $fieldMapping = null;
0 ignored issues
show
Unused Code introduced by
$fieldMapping is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
448
                if ($entityMeta->hasField($field)) {
449
                    $fieldMapping = $entityMeta->getFieldMapping($field);
450
                    if ($fieldMapping['type'] === 'date') {
451
                        // leave only date
452
                        $utc = new \DateTimeZone('UTC');
453 View Code Duplication
                        if ($old && $old instanceof \DateTime) {
454
                            $old->setTimezone($utc);
455
                            $old = new \DateTime($old->format('Y-m-d'), $utc);
456
                        }
457 View Code Duplication
                        if ($new && $new instanceof \DateTime) {
458
                            $new->setTimezone($utc);
459
                            $new = new \DateTime($new->format('Y-m-d'), $utc);
460
                        }
461
                    }
462
                }
463
464
                if ($old instanceof \DateTime && $new instanceof \DateTime
465
                    && $old->getTimestamp() == $new->getTimestamp()
466
                ) {
467
                    continue;
468
                }
469
470
                if ($entityMeta->isSingleValuedAssociation($field) && $new) {
471
                    $oid   = spl_object_hash($new);
472
                    $value = $this->getIdentifier($new);
473
474
                    if (!is_array($value) && !$value) {
475
                        $this->pendingRelatedEntities[$oid][] = [
476
                            'log'   => $logEntry,
477
                            'field' => $field
478
                        ];
479
                    }
480
481
                    $method = $meta->propertyMetadata[$field]->method;
482 View Code Duplication
                    if ($old !== null) {
483
                        // check if an object has the required method to avoid a fatal error
484
                        if (!method_exists($old, $method)) {
485
                            throw new \ReflectionException(
486
                                sprintf('Try to call to undefined method %s::%s', get_class($old), $method)
487
                            );
488
                        }
489
                        $old = $old->{$method}();
490
                    }
491 View Code Duplication
                    if ($new !== null) {
492
                        // check if an object has the required method to avoid a fatal error
493
                        if (!method_exists($new, $method)) {
494
                            throw new \ReflectionException(
495
                                sprintf('Try to call to undefined method %s::%s', get_class($new), $method)
496
                            );
497
                        }
498
                        $new = $new->{$method}();
499
                    }
500
                }
501
502
                $newValues[$field] = [
503
                    'old' => $old,
504
                    'new' => $new,
505
                    'type' => $this->getFieldType($entityMeta, $field),
506
                ];
507
            }
508
509
            $entityIdentifier = $this->getEntityIdentifierString($entity);
510
            if (!empty($this->collectionLogData[$entityClassName][$entityIdentifier])) {
511
                $collectionData = $this->collectionLogData[$entityClassName][$entityIdentifier];
512
                foreach ($collectionData as $field => $changes) {
513
                    if (!isset($meta->propertyMetadata[$field])) {
514
                        continue;
515
                    }
516
517
                    if ($changes['old'] != $changes['new']) {
518
                        $newValues[$field] = $changes;
519
                        $newValues[$field]['type'] = $this->getFieldType($entityMeta, $field);
520
                    }
521
                }
522
523
                unset($this->collectionLogData[$entityClassName][$entityIdentifier]);
524
                if (!$this->collectionLogData[$entityClassName]) {
525
                    unset($this->collectionLogData[$entityClassName]);
526
                }
527
            }
528
529
            foreach ($newValues as $field => $newValue) {
530
                $logEntry->createField($field, $newValue['type'], $newValue['new'], $newValue['old']);
531
            }
532
        }
533
534
        if ($action === self::ACTION_UPDATE && 0 === count($newValues)) {
535
            return;
536
        }
537
538
        $version = 1;
539
540
        if ($action !== self::ACTION_CREATE) {
541
            $version = $this->getNewVersion($logEntryMeta, $entity);
542
543
            if (empty($version)) {
544
                // was versioned later
545
                $version = 1;
546
            }
547
        }
548
549
        $logEntry->setVersion($version);
550
551
        $this->em->persist($logEntry);
552
        $uow->computeChangeSet($logEntryMeta, $logEntry);
553
554
        $logEntryFieldMeta = $this->em->getClassMetadata(
555
            $this->auditEntityMapper->getAuditEntryFieldClass($this->getLoadedUser())
556
        );
557
        foreach ($logEntry->getFields() as $field) {
558
            $this->em->persist($field);
559
            $uow->computeChangeSet($logEntryFieldMeta, $field);
560
        }
561
    }
562
563
    /**
564
     * @return AbstractUser|null
565
     */
566
    protected function getLoadedUser()
567
    {
568
        if (!$this->username) {
569
            return null;
570
        }
571
572
        $isInCache = array_key_exists($this->username, self::$userCache);
573
        if (!$isInCache
574
            || ($isInCache && !$this->em->getUnitOfWork()->isInIdentityMap(self::$userCache[$this->username]))
575
        ) {
576
            /** @var SecurityContextInterface $securityContext */
577
            $securityContext = $this->securityContextLink->getService();
578
            $token = $securityContext->getToken();
579
            if ($token) {
580
                /** @var AbstractUser $user */
581
                $user = $token->getUser();
582
                self::$userCache[$this->username] = $this->em->getReference(
583
                    ClassUtils::getClass($user),
584
                    $user->getId()
585
                );
586
            }
587
        }
588
589
        return self::$userCache[$this->username];
590
    }
591
592
    /**
593
     * Get the LogEntry class
594
     *
595
     * @return string
596
     */
597
    protected function getLogEntityClass()
598
    {
599
        return $this->auditEntityMapper->getAuditEntryClass($this->getLoadedUser());
600
    }
601
602
    /**
603
     * @param DoctrineClassMetadata $logEntityMeta
604
     * @param object $entity
605
     * @return mixed
606
     */
607
    protected function getNewVersion($logEntityMeta, $entity)
608
    {
609
        $entityMeta = $this->em->getClassMetadata($this->getEntityClassName($entity));
610
        $entityId   = $this->getIdentifier($entity);
611
612
        $qb = $this->em->createQueryBuilder();
613
        $query = $qb
614
            ->select($qb->expr()->max('log.version'))
615
            ->from($logEntityMeta->name, 'log')
616
            ->where(
617
                $qb->expr()->andX(
618
                    $qb->expr()->eq('log.objectId', ':objectId'),
619
                    $qb->expr()->eq('log.objectClass', ':objectClass')
620
                )
621
            )
622
            ->setParameter('objectId', $entityId)
623
            ->setParameter('objectClass', $entityMeta->name)
624
            ->getQuery();
625
626
        return $query->getSingleScalarResult() + 1;
627
    }
628
629
    /**
630
     * @param  object $entity
631
     * @param  DoctrineClassMetadata|null $entityMeta
632
     * @return mixed
633
     */
634
    protected function getIdentifier($entity, $entityMeta = null)
635
    {
636
        $entityMeta      = $entityMeta ?: $this->em->getClassMetadata($this->getEntityClassName($entity));
637
        $identifierField = $entityMeta->getSingleIdentifierFieldName();
638
639
        return $entityMeta->getReflectionProperty($identifierField)->getValue($entity);
640
    }
641
642
    /**
643
     * @param string $entityClassName
644
     * @return bool
645
     */
646
    protected function checkAuditable($entityClassName)
647
    {
648
        if ($this->auditConfigProvider->hasConfig($entityClassName)
649
            && $this->auditConfigProvider->getConfig($entityClassName)->is('auditable')
650
        ) {
651
            $reflection    = new \ReflectionClass($entityClassName);
652
            $classMetadata = new ClassMetadata($reflection->getName());
653
654
            foreach ($reflection->getProperties() as $reflectionProperty) {
655
                $fieldName = $reflectionProperty->getName();
656
657
                if ($this->auditConfigProvider->hasConfig($entityClassName, $fieldName)
658
                    && ($fieldConfig = $this->auditConfigProvider->getConfig($entityClassName, $fieldName))
659
                    && $fieldConfig->is('auditable')
660
                ) {
661
                    $propertyMetadata         = new PropertyMetadata($entityClassName, $reflectionProperty->getName());
662
                    $propertyMetadata->method = '__toString';
0 ignored issues
show
Documentation Bug introduced by
The property $method was declared of type boolean, but '__toString' is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
663
664
                    $classMetadata->addPropertyMetadata($propertyMetadata);
665
                }
666
            }
667
668
            if (count($classMetadata->propertyMetadata)) {
669
                $this->addConfig($classMetadata);
670
671
                return true;
672
            }
673
        }
674
675
        return false;
676
    }
677
678
    /**
679
     * @param object|string $entity
680
     * @return string
681
     */
682
    private function getEntityClassName($entity)
683
    {
684
        if (is_object($entity)) {
685
            return ClassUtils::getClass($entity);
686
        }
687
688
        return $entity;
689
    }
690
691
    /**
692
     * @param object $entity
693
     * @return string
694
     */
695
    protected function getEntityIdentifierString($entity)
696
    {
697
        $className = $this->getEntityClassName($entity);
698
        $metadata  = $this->em->getClassMetadata($className);
699
700
        return serialize($metadata->getIdentifierValues($entity));
701
    }
702
703
    /**
704
     * @param DoctrineClassMetadata $entityMeta
705
     * @param string                $field
706
     *
707
     * @return string
708
     */
709
    private function getFieldType(DoctrineClassMetadata $entityMeta, $field)
710
    {
711
        $type = null;
0 ignored issues
show
Unused Code introduced by
$type is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
712
        if ($entityMeta->hasField($field)) {
713
            $type = $entityMeta->getTypeOfField($field);
714
            if ($type instanceof Type) {
715
                $type = $type->getName();
716
            }
717
        } elseif ($entityMeta->hasAssociation($field)) {
718
            $type = Type::STRING;
719
        } else {
720
            throw new \InvalidArgumentException(sprintf(
721
                'Field "%s" is not mapped field of "%s" entity.',
722
                $field,
723
                $entityMeta->getName()
724
            ));
725
        }
726
727
        return $type;
728
    }
729
}
730