Passed
Push — master ( 6caf60...e73a90 )
by Jan
10:20
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
    public function shouldFieldBeSaved(AbstractDBElement $element, string $field_name): bool
205
    {
206
        foreach (static::FIELD_BLACKLIST as $class => $blacklist) {
207
            if (is_a($element, $class) && in_array($field_name, $blacklist, true)) {
208
                return false;
209
            }
210
        }
211
212
        //By default allow every field.
213
        return true;
214
    }
215
216
    public function getSubscribedEvents()
217
    {
218
        return[
219
            Events::onFlush,
220
            Events::postPersist,
221
            Events::postFlush,
222
        ];
223
    }
224
225
    protected function logElementDeleted(AbstractDBElement $entity, EntityManagerInterface $em): void
226
    {
227
        $log = new ElementDeletedLogEntry($entity);
228
        //Add user comment to log entry
229
        if ($this->eventCommentHelper->isMessageSet()) {
230
            $log->setComment($this->eventCommentHelper->getMessage());
231
        }
232
        if ($this->eventUndoHelper->isUndo()) {
233
            $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
234
        }
235
        if ($this->save_removed_data) {
236
            //The 4th param is important here, as we delete the element...
237
            $this->saveChangeSet($entity, $log, $em, true);
238
        }
239
        $this->logger->log($log);
240
241
        //Check if we have to log CollectionElementDeleted entries
242
        if ($this->save_changed_data) {
243
            $metadata = $em->getClassMetadata(get_class($entity));
244
            $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

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

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