Passed
Push — master ( 08267b...db956f )
by Jan
04:49
created

EventLoggerSubscriber::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
c 0
b 0
f 0
nc 1
nop 8
dl 0
loc 13
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 - 2020 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
/**
24
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
25
 *
26
 * Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
27
 *
28
 * This program is free software: you can redistribute it and/or modify
29
 * it under the terms of the GNU Affero General Public License as published
30
 * by the Free Software Foundation, either version 3 of the License, or
31
 * (at your option) any later version.
32
 *
33
 * This program is distributed in the hope that it will be useful,
34
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
35
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36
 * GNU Affero General Public License for more details.
37
 *
38
 * You should have received a copy of the GNU Affero General Public License
39
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
40
 */
41
42
namespace App\EventSubscriber\LogSystem;
43
44
use App\Entity\Attachments\Attachment;
45
use App\Entity\Base\AbstractDBElement;
46
use App\Entity\LogSystem\AbstractLogEntry;
47
use App\Entity\LogSystem\CollectionElementDeleted;
48
use App\Entity\LogSystem\ElementCreatedLogEntry;
49
use App\Entity\LogSystem\ElementDeletedLogEntry;
50
use App\Entity\LogSystem\ElementEditedLogEntry;
51
use App\Entity\Parameters\AbstractParameter;
52
use App\Entity\Parts\PartLot;
53
use App\Entity\PriceInformations\Orderdetail;
54
use App\Entity\PriceInformations\Pricedetail;
55
use App\Entity\UserSystem\User;
56
use App\Services\LogSystem\EventCommentHelper;
57
use App\Services\LogSystem\EventLogger;
58
use App\Services\LogSystem\EventUndoHelper;
59
use Doctrine\Common\EventSubscriber;
60
use Doctrine\ORM\EntityManagerInterface;
61
use Doctrine\ORM\Event\OnFlushEventArgs;
62
use Doctrine\ORM\Event\PostFlushEventArgs;
63
use Doctrine\ORM\Events;
64
use Doctrine\Persistence\Event\LifecycleEventArgs;
65
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
66
use Symfony\Component\Serializer\SerializerInterface;
67
68
/**
69
 * This event subscriber write to event log when entities are changed, removed, created.
70
 */
71
class EventLoggerSubscriber implements EventSubscriber
72
{
73
    /**
74
     * @var array The given fields will not be saved, because they contain sensitive informations
75
     */
76
    protected const FIELD_BLACKLIST = [
77
        User::class => ['password', 'need_pw_change', 'googleAuthenticatorSecret', 'backupCodes', 'trustedDeviceCookieVersion', 'pw_reset_token', 'backupCodesGenerationDate'],
78
    ];
79
80
    /**
81
     * @var array If elements of the given class are deleted, a log for the given fields will be triggered
82
     */
83
    protected const TRIGGER_ASSOCIATION_LOG_WHITELIST = [
84
        PartLot::class => ['part'],
85
        Orderdetail::class => ['part'],
86
        Pricedetail::class => ['orderdetail'],
87
        Attachment::class => ['element'],
88
        AbstractParameter::class => ['element'],
89
    ];
90
91
    protected const MAX_STRING_LENGTH = 2000;
92
93
    protected $logger;
94
    protected $serializer;
95
    protected $eventCommentHelper;
96
    protected $eventUndoHelper;
97
    protected $save_changed_fields;
98
    protected $save_changed_data;
99
    protected $save_removed_data;
100
    protected $propertyAccessor;
101
102
    public function __construct(EventLogger $logger, SerializerInterface $serializer, EventCommentHelper $commentHelper,
103
        bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, PropertyAccessorInterface $propertyAccessor,
104
        EventUndoHelper $eventUndoHelper)
105
    {
106
        $this->logger = $logger;
107
        $this->serializer = $serializer;
108
        $this->eventCommentHelper = $commentHelper;
109
        $this->propertyAccessor = $propertyAccessor;
110
        $this->eventUndoHelper = $eventUndoHelper;
111
112
        $this->save_changed_fields = $save_changed_fields;
113
        $this->save_changed_data = $save_changed_data;
114
        $this->save_removed_data = $save_removed_data;
115
    }
116
117
    public function onFlush(OnFlushEventArgs $eventArgs): void
118
    {
119
        $em = $eventArgs->getEntityManager();
120
        $uow = $em->getUnitOfWork();
121
122
        /*
123
         * Log changes and deletions of entites.
124
         * We can not log persist here, because the entities do not have IDs yet...
125
         */
126
127
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
128
            if ($this->validEntity($entity)) {
129
                $this->logElementEdited($entity, $em);
130
            }
131
        }
132
133
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
134
            if ($this->validEntity($entity)) {
135
                $this->logElementDeleted($entity, $em);
136
            }
137
        }
138
139
        $uow->computeChangeSets();
140
    }
141
142
    public function postPersist(LifecycleEventArgs $args): void
143
    {
144
        //Create an log entry, we have to do this post persist, cause we have to know the ID
145
146
        /** @var AbstractDBElement $entity */
147
        $entity = $args->getObject();
148
        if ($this->validEntity($entity)) {
149
            $log = new ElementCreatedLogEntry($entity);
150
            //Add user comment to log entry
151
            if ($this->eventCommentHelper->isMessageSet()) {
152
                $log->setComment($this->eventCommentHelper->getMessage());
153
            }
154
            if ($this->eventUndoHelper->isUndo()) {
155
                $undoEvent = $this->eventUndoHelper->getUndoneEvent();
156
157
                $log->setUndoneEvent($undoEvent, $this->eventUndoHelper->getMode());
158
159
                if ($undoEvent instanceof ElementDeletedLogEntry && $undoEvent->getTargetClass() === $log->getTargetClass()) {
160
                    $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

160
                    $log->setTargetElementID(/** @scrutinizer ignore-type */ $undoEvent->getTargetID());
Loading history...
161
                }
162
                if ($undoEvent instanceof CollectionElementDeleted && $undoEvent->getDeletedElementClass() === $log->getTargetClass()) {
163
                    $log->setTargetElementID($undoEvent->getDeletedElementID());
164
                }
165
            }
166
            $this->logger->log($log);
167
        }
168
    }
169
170
    public function postFlush(PostFlushEventArgs $args): void
171
    {
172
        $em = $args->getEntityManager();
173
        $uow = $em->getUnitOfWork();
174
        // If the we have added any ElementCreatedLogEntries added in postPersist, we flush them here.
175
        $uow->computeChangeSets();
176
        if ($uow->hasPendingInsertions() || !empty($uow->getScheduledEntityUpdates())) {
177
            $em->flush();
178
        }
179
180
        //Clear the message provided by user.
181
        $this->eventCommentHelper->clearMessage();
182
        $this->eventUndoHelper->clearUndoneEvent();
183
    }
184
185
    /**
186
     * Check if the given element class has restrictions to its fields.
187
     *
188
     * @return bool True if there are restrictions, and further checking is needed
189
     */
190
    public function hasFieldRestrictions(AbstractDBElement $element): bool
191
    {
192
        foreach (array_keys(static::FIELD_BLACKLIST) as $class) {
193
            if (is_a($element, $class)) {
194
                return true;
195
            }
196
        }
197
198
        return false;
199
    }
200
201
    /**
202
     * Checks if the field of the given element should be saved (if it is not blacklisted).
203
     *
204
     * @return bool
205
     */
206
    public function shouldFieldBeSaved(AbstractDBElement $element, string $field_name): bool
207
    {
208
        foreach (static::FIELD_BLACKLIST as $class => $blacklist) {
209
            if (is_a($element, $class) && in_array($field_name, $blacklist, true)) {
210
                return false;
211
            }
212
        }
213
214
        //By default allow every field.
215
        return true;
216
    }
217
218
    public function getSubscribedEvents()
219
    {
220
        return[
221
            Events::onFlush,
222
            Events::postPersist,
223
            Events::postFlush,
224
        ];
225
    }
226
227
    protected function logElementDeleted(AbstractDBElement $entity, EntityManagerInterface $em): void
228
    {
229
        $log = new ElementDeletedLogEntry($entity);
230
        //Add user comment to log entry
231
        if ($this->eventCommentHelper->isMessageSet()) {
232
            $log->setComment($this->eventCommentHelper->getMessage());
233
        }
234
        if ($this->eventUndoHelper->isUndo()) {
235
            $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
236
        }
237
        if ($this->save_removed_data) {
238
            //The 4th param is important here, as we delete the element...
239
            $this->saveChangeSet($entity, $log, $em, true);
240
        }
241
        $this->logger->log($log);
242
243
        //Check if we have to log CollectionElementDeleted entries
244
        if ($this->save_changed_data) {
245
            $metadata = $em->getClassMetadata(get_class($entity));
246
            $mappings = $metadata->getAssociationMappings();
0 ignored issues
show
Bug introduced by
The method getAssociationMappings() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. Did you maybe mean getAssociationNames()? ( Ignorable by Annotation )

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

246
            /** @scrutinizer ignore-call */ 
247
            $mappings = $metadata->getAssociationMappings();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
247
            //Check if class is whitelisted for CollectionElementDeleted entry
248
            foreach (static::TRIGGER_ASSOCIATION_LOG_WHITELIST as $class => $whitelist) {
249
                if (is_a($entity, $class)) {
250
                    //Check names
251
                    foreach ($mappings as $field => $mapping) {
252
                        if (in_array($field, $whitelist, true)) {
253
                            $changed = $this->propertyAccessor->getValue($entity, $field);
254
                            $log = new CollectionElementDeleted($changed, $mapping['inversedBy'], $entity);
255
                            if ($this->eventUndoHelper->isUndo()) {
256
                                $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
257
                            }
258
                            $this->logger->log($log);
259
                        }
260
                    }
261
                }
262
            }
263
        }
264
    }
265
266
    protected function logElementEdited(AbstractDBElement $entity, EntityManagerInterface $em): void
267
    {
268
        $uow = $em->getUnitOfWork();
269
270
        /* We have to call that here again, so the foreign entity timestamps, that were changed in updateTimestamp
271
           get persisted */
272
        $changeSet = $uow->getEntityChangeSet($entity);
273
274
        //Skip log entry, if only the lastModified field has changed...
275
        if (isset($changeSet['lastModified']) && count($changeSet)) {
276
            return;
277
        }
278
279
        $log = new ElementEditedLogEntry($entity);
280
        if ($this->save_changed_data) {
281
            $this->saveChangeSet($entity, $log, $em);
282
        } elseif ($this->save_changed_fields) {
283
            $changed_fields = array_keys($uow->getEntityChangeSet($entity));
284
            //Remove lastModified field, as this is always changed (gives us no additional info)
285
            $changed_fields = array_diff($changed_fields, ['lastModified']);
286
            $log->setChangedFields($changed_fields);
287
        }
288
        //Add user comment to log entry
289
        if ($this->eventCommentHelper->isMessageSet()) {
290
            $log->setComment($this->eventCommentHelper->getMessage());
291
        }
292
        if ($this->eventUndoHelper->isUndo()) {
293
            $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
294
        }
295
        $this->logger->log($log);
296
    }
297
298
    /**
299
     * Filter out every forbidden field and return the cleaned array.
300
     *
301
     * @return array
302
     */
303
    protected function filterFieldRestrictions(AbstractDBElement $element, array $fields): array
304
    {
305
        unset($fields['lastModified']);
306
307
        if (! $this->hasFieldRestrictions($element)) {
308
            return $fields;
309
        }
310
311
        return array_filter($fields, function ($value, $key) use ($element) {
312
            //Associative array (save changed data) case
313
            if (is_string($key)) {
314
                return $this->shouldFieldBeSaved($element, $key);
315
            }
316
317
            return $this->shouldFieldBeSaved($element, $value);
318
        }, ARRAY_FILTER_USE_BOTH);
319
    }
320
321
    protected function saveChangeSet(AbstractDBElement $entity, AbstractLogEntry $logEntry, EntityManagerInterface $em, bool $element_deleted = false): void
322
    {
323
        $uow = $em->getUnitOfWork();
324
325
        if (! $logEntry instanceof ElementEditedLogEntry && ! $logEntry instanceof ElementDeletedLogEntry) {
326
            throw new \InvalidArgumentException('$logEntry must be ElementEditedLogEntry or ElementDeletedLogEntry!');
327
        }
328
329
        if ($element_deleted) { //If the element was deleted we can use getOriginalData to save its content
330
            $old_data = $uow->getOriginalEntityData($entity);
331
        } else { //Otherwise we have to get it from entity changeset
332
            $changeSet = $uow->getEntityChangeSet($entity);
333
            $old_data = array_combine(array_keys($changeSet), array_column($changeSet, 0));
334
        }
335
        $old_data = $this->filterFieldRestrictions($entity, $old_data);
0 ignored issues
show
Bug introduced by
It seems like $old_data can also be of type false; however, parameter $fields of App\EventSubscriber\LogS...lterFieldRestrictions() does only seem to accept array, 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

335
        $old_data = $this->filterFieldRestrictions($entity, /** @scrutinizer ignore-type */ $old_data);
Loading history...
336
337
        //Restrict length of string fields, to save memory...
338
        $old_data = array_map(function ($value) {
339
            if (is_string($value)) {
340
                return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...');
341
            }
342
343
            return $value;
344
        }, $old_data);
345
346
        $logEntry->setOldData($old_data);
347
    }
348
349
    /**
350
     * Check if the given entity can be logged.
351
     *
352
     * @return bool True, if the given entity can be logged.
353
     */
354
    protected function validEntity(object $entity): bool
355
    {
356
        //Dont log logentries itself!
357
        if ($entity instanceof AbstractDBElement && ! $entity instanceof AbstractLogEntry) {
358
            return true;
359
        }
360
361
        return false;
362
    }
363
}
364