EventLoggerSubscriber::logElementDeleted()   B
last analyzed

Complexity

Conditions 10
Paths 56

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 20
c 0
b 0
f 0
nc 56
nop 2
dl 0
loc 32
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as published
9
 * by the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
declare(strict_types=1);
22
23
namespace App\EventSubscriber\LogSystem;
24
25
use App\Entity\Attachments\Attachment;
26
use App\Entity\Base\AbstractDBElement;
27
use App\Entity\LogSystem\AbstractLogEntry;
28
use App\Entity\LogSystem\CollectionElementDeleted;
29
use App\Entity\LogSystem\ElementCreatedLogEntry;
30
use App\Entity\LogSystem\ElementDeletedLogEntry;
31
use App\Entity\LogSystem\ElementEditedLogEntry;
32
use App\Entity\Parameters\AbstractParameter;
33
use App\Entity\Parts\PartLot;
34
use App\Entity\PriceInformations\Orderdetail;
35
use App\Entity\PriceInformations\Pricedetail;
36
use App\Entity\UserSystem\User;
37
use App\Services\LogSystem\EventCommentHelper;
38
use App\Services\LogSystem\EventLogger;
39
use App\Services\LogSystem\EventUndoHelper;
40
use Doctrine\Common\EventSubscriber;
41
use Doctrine\ORM\EntityManagerInterface;
42
use Doctrine\ORM\Event\OnFlushEventArgs;
43
use Doctrine\ORM\Event\PostFlushEventArgs;
44
use Doctrine\ORM\Events;
45
use Doctrine\Persistence\Event\LifecycleEventArgs;
46
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
47
use Symfony\Component\Serializer\SerializerInterface;
48
49
/**
50
 * This event subscriber write to event log when entities are changed, removed, created.
51
 */
52
class EventLoggerSubscriber implements EventSubscriber
53
{
54
    /**
55
     * @var array The given fields will not be saved, because they contain sensitive informations
56
     */
57
    protected const FIELD_BLACKLIST = [
58
        User::class => ['password', 'need_pw_change', 'googleAuthenticatorSecret', 'backupCodes', 'trustedDeviceCookieVersion', 'pw_reset_token', 'backupCodesGenerationDate'],
59
    ];
60
61
    /**
62
     * @var array If elements of the given class are deleted, a log for the given fields will be triggered
63
     */
64
    protected const TRIGGER_ASSOCIATION_LOG_WHITELIST = [
65
        PartLot::class => ['part'],
66
        Orderdetail::class => ['part'],
67
        Pricedetail::class => ['orderdetail'],
68
        Attachment::class => ['element'],
69
        AbstractParameter::class => ['element'],
70
    ];
71
72
    protected const MAX_STRING_LENGTH = 2000;
73
74
    protected EventLogger $logger;
75
    protected SerializerInterface $serializer;
76
    protected EventCommentHelper $eventCommentHelper;
77
    protected EventUndoHelper $eventUndoHelper;
78
    protected bool $save_changed_fields;
79
    protected bool $save_changed_data;
80
    protected bool $save_removed_data;
81
    protected PropertyAccessorInterface $propertyAccessor;
82
83
    public function __construct(EventLogger $logger, SerializerInterface $serializer, EventCommentHelper $commentHelper,
84
        bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, PropertyAccessorInterface $propertyAccessor,
85
        EventUndoHelper $eventUndoHelper)
86
    {
87
        $this->logger = $logger;
88
        $this->serializer = $serializer;
89
        $this->eventCommentHelper = $commentHelper;
90
        $this->propertyAccessor = $propertyAccessor;
91
        $this->eventUndoHelper = $eventUndoHelper;
92
93
        $this->save_changed_fields = $save_changed_fields;
94
        $this->save_changed_data = $save_changed_data;
95
        $this->save_removed_data = $save_removed_data;
96
    }
97
98
    public function onFlush(OnFlushEventArgs $eventArgs): void
99
    {
100
        $em = $eventArgs->getEntityManager();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\Event\OnFlu...rgs::getEntityManager() has been deprecated: 2.13. Use {@see getObjectManager} instead. ( Ignorable by Annotation )

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

100
        $em = /** @scrutinizer ignore-deprecated */ $eventArgs->getEntityManager();

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

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

Loading history...
101
        $uow = $em->getUnitOfWork();
102
103
        /*
104
         * Log changes and deletions of entites.
105
         * We can not log persist here, because the entities do not have IDs yet...
106
         */
107
108
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
109
            if ($this->validEntity($entity)) {
110
                $this->logElementEdited($entity, $em);
111
            }
112
        }
113
114
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
115
            if ($this->validEntity($entity)) {
116
                $this->logElementDeleted($entity, $em);
117
            }
118
        }
119
120
        /* Do not call $uow->computeChangeSets() in this function, only individual entities should be computed!
121
         * Otherwise we will run into very strange issues, that some entity changes are no longer updated!
122
         * This is almost impossible to debug, because it only happens in some cases, and it looks very unrelated to
123
         * this code (which caused the problem and which took me very long time to find out).
124
         * So just do not call $uow->computeChangeSets() here ever, even if it is tempting!!
125
         * If you need to log something from inside here, just call logFromOnFlush() instead of the normal log() function.
126
         */
127
    }
128
129
    public function postPersist(LifecycleEventArgs $args): void
130
    {
131
        //Create an log entry, we have to do this post persist, cause we have to know the ID
132
133
        /** @var AbstractDBElement $entity */
134
        $entity = $args->getObject();
135
        if ($this->validEntity($entity)) {
136
            $log = new ElementCreatedLogEntry($entity);
137
            //Add user comment to log entry
138
            if ($this->eventCommentHelper->isMessageSet()) {
139
                $log->setComment($this->eventCommentHelper->getMessage());
140
            }
141
            if ($this->eventUndoHelper->isUndo()) {
142
                $undoEvent = $this->eventUndoHelper->getUndoneEvent();
143
144
                $log->setUndoneEvent($undoEvent, $this->eventUndoHelper->getMode());
145
146
                if ($undoEvent instanceof ElementDeletedLogEntry && $undoEvent->getTargetClass() === $log->getTargetClass()) {
147
                    $log->setTargetElementID($undoEvent->getTargetID());
0 ignored issues
show
Bug introduced by
It seems like $undoEvent->getTargetID() can also be of type null; however, parameter $target_id of App\Entity\LogSystem\Abs...y::setTargetElementID() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

147
                    $log->setTargetElementID(/** @scrutinizer ignore-type */ $undoEvent->getTargetID());
Loading history...
148
                }
149
                if ($undoEvent instanceof CollectionElementDeleted && $undoEvent->getDeletedElementClass() === $log->getTargetClass()) {
150
                    $log->setTargetElementID($undoEvent->getDeletedElementID());
151
                }
152
            }
153
            $this->logger->log($log);
154
        }
155
    }
156
157
    public function postFlush(PostFlushEventArgs $args): void
158
    {
159
        $em = $args->getEntityManager();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\Event\PostF...rgs::getEntityManager() has been deprecated: 2.13. Use {@see getObjectManager} instead. ( Ignorable by Annotation )

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

159
        $em = /** @scrutinizer ignore-deprecated */ $args->getEntityManager();

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

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

Loading history...
160
        $uow = $em->getUnitOfWork();
161
        // If the we have added any ElementCreatedLogEntries added in postPersist, we flush them here.
162
        $uow->computeChangeSets();
163
        if ($uow->hasPendingInsertions() || !empty($uow->getScheduledEntityUpdates())) {
164
            $em->flush();
165
        }
166
167
        //Clear the message provided by user.
168
        $this->eventCommentHelper->clearMessage();
169
        $this->eventUndoHelper->clearUndoneEvent();
170
    }
171
172
    /**
173
     * Check if the given element class has restrictions to its fields.
174
     *
175
     * @return bool True if there are restrictions, and further checking is needed
176
     */
177
    public function hasFieldRestrictions(AbstractDBElement $element): bool
178
    {
179
        foreach (array_keys(static::FIELD_BLACKLIST) as $class) {
180
            if (is_a($element, $class)) {
181
                return true;
182
            }
183
        }
184
185
        return false;
186
    }
187
188
    /**
189
     * Checks if the field of the given element should be saved (if it is not blacklisted).
190
     */
191
    public function shouldFieldBeSaved(AbstractDBElement $element, string $field_name): bool
192
    {
193
        foreach (static::FIELD_BLACKLIST as $class => $blacklist) {
194
            if (is_a($element, $class) && in_array($field_name, $blacklist, true)) {
195
                return false;
196
            }
197
        }
198
199
        //By default allow every field.
200
        return true;
201
    }
202
203
    public function getSubscribedEvents(): array
204
    {
205
        return[
206
            Events::onFlush,
207
            Events::postPersist,
208
            Events::postFlush,
209
        ];
210
    }
211
212
    protected function logElementDeleted(AbstractDBElement $entity, EntityManagerInterface $em): void
213
    {
214
        $log = new ElementDeletedLogEntry($entity);
215
        //Add user comment to log entry
216
        if ($this->eventCommentHelper->isMessageSet()) {
217
            $log->setComment($this->eventCommentHelper->getMessage());
218
        }
219
        if ($this->eventUndoHelper->isUndo()) {
220
            $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
221
        }
222
        if ($this->save_removed_data) {
223
            //The 4th param is important here, as we delete the element...
224
            $this->saveChangeSet($entity, $log, $em, true);
225
        }
226
        $this->logger->logFromOnFlush($log);
227
228
        //Check if we have to log CollectionElementDeleted entries
229
        if ($this->save_changed_data) {
230
            $metadata = $em->getClassMetadata(get_class($entity));
231
            $mappings = $metadata->getAssociationMappings();
232
            //Check if class is whitelisted for CollectionElementDeleted entry
233
            foreach (static::TRIGGER_ASSOCIATION_LOG_WHITELIST as $class => $whitelist) {
234
                if (is_a($entity, $class)) {
235
                    //Check names
236
                    foreach ($mappings as $field => $mapping) {
237
                        if (in_array($field, $whitelist, true)) {
238
                            $changed = $this->propertyAccessor->getValue($entity, $field);
239
                            $log = new CollectionElementDeleted($changed, $mapping['inversedBy'], $entity);
240
                            if ($this->eventUndoHelper->isUndo()) {
241
                                $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
242
                            }
243
                            $this->logger->logFromOnFlush($log);
244
                        }
245
                    }
246
                }
247
            }
248
        }
249
    }
250
251
    protected function logElementEdited(AbstractDBElement $entity, EntityManagerInterface $em): void
252
    {
253
        $uow = $em->getUnitOfWork();
254
255
        /* We have to call that here again, so the foreign entity timestamps, that were changed in updateTimestamp
256
           get persisted */
257
        $changeSet = $uow->getEntityChangeSet($entity);
258
259
        //Skip log entry, if only the lastModified field has changed...
260
        if (isset($changeSet['lastModified']) && count($changeSet)) {
261
            return;
262
        }
263
264
        $log = new ElementEditedLogEntry($entity);
265
        if ($this->save_changed_data) {
266
            $this->saveChangeSet($entity, $log, $em);
267
        } elseif ($this->save_changed_fields) {
268
            $changed_fields = array_keys($uow->getEntityChangeSet($entity));
269
            //Remove lastModified field, as this is always changed (gives us no additional info)
270
            $changed_fields = array_diff($changed_fields, ['lastModified']);
271
            $log->setChangedFields($changed_fields);
272
        }
273
        //Add user comment to log entry
274
        if ($this->eventCommentHelper->isMessageSet()) {
275
            $log->setComment($this->eventCommentHelper->getMessage());
276
        }
277
        if ($this->eventUndoHelper->isUndo()) {
278
            $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
279
        }
280
        $this->logger->logFromOnFlush($log);
281
    }
282
283
    /**
284
     * Filter out every forbidden field and return the cleaned array.
285
     */
286
    protected function filterFieldRestrictions(AbstractDBElement $element, array $fields): array
287
    {
288
        unset($fields['lastModified']);
289
290
        if (!$this->hasFieldRestrictions($element)) {
291
            return $fields;
292
        }
293
294
        return array_filter($fields, function ($value, $key) use ($element) {
295
            //Associative array (save changed data) case
296
            if (is_string($key)) {
297
                return $this->shouldFieldBeSaved($element, $key);
298
            }
299
300
            return $this->shouldFieldBeSaved($element, $value);
301
        }, ARRAY_FILTER_USE_BOTH);
302
    }
303
304
    protected function saveChangeSet(AbstractDBElement $entity, AbstractLogEntry $logEntry, EntityManagerInterface $em, bool $element_deleted = false): void
305
    {
306
        $uow = $em->getUnitOfWork();
307
308
        if (!$logEntry instanceof ElementEditedLogEntry && !$logEntry instanceof ElementDeletedLogEntry) {
309
            throw new \InvalidArgumentException('$logEntry must be ElementEditedLogEntry or ElementDeletedLogEntry!');
310
        }
311
312
        if ($element_deleted) { //If the element was deleted we can use getOriginalData to save its content
313
            $old_data = $uow->getOriginalEntityData($entity);
314
        } else { //Otherwise we have to get it from entity changeset
315
            $changeSet = $uow->getEntityChangeSet($entity);
316
            $old_data = array_combine(array_keys($changeSet), array_column($changeSet, 0));
317
        }
318
        $old_data = $this->filterFieldRestrictions($entity, $old_data);
319
320
        //Restrict length of string fields, to save memory...
321
        $old_data = array_map(
322
            static function ($value) {
323
                if (is_string($value)) {
324
                    return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...');
325
                }
326
327
                return $value;
328
            }, $old_data);
329
330
        $logEntry->setOldData($old_data);
331
    }
332
333
    /**
334
     * Check if the given entity can be logged.
335
     *
336
     * @return bool true, if the given entity can be logged
337
     */
338
    protected function validEntity(object $entity): bool
339
    {
340
        //Dont log logentries itself!
341
        return $entity instanceof AbstractDBElement && !$entity instanceof AbstractLogEntry;
342
    }
343
}
344