Passed
Push — master ( 2d425f...eb03d1 )
by Jan
04:57 queued 10s
created

EventLoggerSubscriber::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
c 1
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
namespace App\EventSubscriber;
22
23
use App\Entity\Attachments\Attachment;
24
use App\Entity\Attachments\AttachmentType;
25
use App\Entity\Base\AbstractDBElement;
26
use App\Entity\Base\AbstractPartsContainingDBElement;
27
use App\Entity\Base\AbstractStructuralDBElement;
28
use App\Entity\LogSystem\AbstractLogEntry;
29
use App\Entity\LogSystem\CollectionElementDeleted;
30
use App\Entity\LogSystem\ElementCreatedLogEntry;
31
use App\Entity\LogSystem\ElementDeletedLogEntry;
32
use App\Entity\LogSystem\ElementEditedLogEntry;
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
class EventLoggerSubscriber implements EventSubscriber
50
{
51
    /** @var array The given fields will not be saved, because they contain sensitive informations */
52
    protected const FIELD_BLACKLIST = [
53
        User::class => ['password', 'need_pw_change', 'googleAuthenticatorSecret', 'backupCodes', 'trustedDeviceCookieVersion', 'pw_reset_token', 'backupCodesGenerationDate'],
54
    ];
55
56
    /** @var array If elements of the given class are deleted, a log for the given fields will be triggered */
57
    protected const TRIGGER_ASSOCIATION_LOG_WHITELIST = [
58
        PartLot::class => ['part'],
59
        Orderdetail::class => ['part'],
60
        Pricedetail::class => ['orderdetail'],
61
        Attachment::class => ['element'],
62
    ];
63
64
    protected const MAX_STRING_LENGTH = 2000;
65
66
    protected $logger;
67
    protected $serializer;
68
    protected $eventCommentHelper;
69
    protected $eventUndoHelper;
70
    protected $save_changed_fields;
71
    protected $save_changed_data;
72
    protected $save_removed_data;
73
    protected $propertyAccessor;
74
75
    public function __construct(EventLogger $logger, SerializerInterface $serializer, EventCommentHelper $commentHelper,
76
        bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, PropertyAccessorInterface $propertyAccessor,
77
        EventUndoHelper $eventUndoHelper)
78
    {
79
        $this->logger = $logger;
80
        $this->serializer = $serializer;
81
        $this->eventCommentHelper = $commentHelper;
82
        $this->propertyAccessor = $propertyAccessor;
83
        $this->eventUndoHelper = $eventUndoHelper;
84
85
        $this->save_changed_fields = $save_changed_fields;
86
        $this->save_changed_data = $save_changed_data;
87
        $this->save_removed_data = $save_removed_data;
88
    }
89
90
    public function onFlush(OnFlushEventArgs $eventArgs)
91
    {
92
        $em = $eventArgs->getEntityManager();
93
        $uow = $em->getUnitOfWork();
94
95
        /*
96
         * Log changes and deletions of entites.
97
         * We can not log persist here, because the entities do not have IDs yet...
98
         */
99
100
        foreach ($uow->getScheduledEntityUpdates() as $entity) {
101
            if ($this->validEntity($entity)) {
102
                $this->logElementEdited($entity, $em);
103
            }
104
        }
105
106
        foreach ($uow->getScheduledEntityDeletions() as $entity) {
107
            if ($this->validEntity($entity)) {
108
                $this->logElementDeleted($entity, $em);
109
            }
110
        }
111
112
        $uow->computeChangeSets();
113
    }
114
115
    public function postPersist(LifecycleEventArgs $args)
116
    {
117
        //Create an log entry, we have to do this post persist, cause we have to know the ID
118
119
        /** @var AbstractDBElement $entity */
120
        $entity = $args->getObject();
121
        if ($this->validEntity($entity)) {
122
            $log = new ElementCreatedLogEntry($entity);
123
            //Add user comment to log entry
124
            if ($this->eventCommentHelper->isMessageSet()) {
125
                $log->setComment($this->eventCommentHelper->getMessage());
126
            }
127
            if ($this->eventUndoHelper->isUndo()) {
128
                $undoEvent = $this->eventUndoHelper->getUndoneEvent();
129
130
                $log->setUndoneEvent($undoEvent, $this->eventUndoHelper->getMode());
131
132
                if($undoEvent instanceof ElementDeletedLogEntry && $undoEvent->getTargetClass() === $log->getTargetClass()) {
133
                    $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

133
                    $log->setTargetElementID(/** @scrutinizer ignore-type */ $undoEvent->getTargetID());
Loading history...
134
                }
135
                if($undoEvent instanceof CollectionElementDeleted && $undoEvent->getDeletedElementClass() === $log->getTargetClass()) {
136
                    $log->setTargetElementID($undoEvent->getDeletedElementID());
137
                }
138
            }
139
            $this->logger->log($log);
140
        }
141
    }
142
143
    public function postFlush(PostFlushEventArgs $args)
144
    {
145
        $em = $args->getEntityManager();
146
        $uow = $em->getUnitOfWork();
147
        // If the we have added any ElementCreatedLogEntries added in postPersist, we flush them here.
148
        if ($uow->hasPendingInsertions()) {
149
            $em->flush();
150
        }
151
152
        //Clear the message provided by user.
153
        $this->eventCommentHelper->clearMessage();
154
        $this->eventUndoHelper->clearUndoneEvent();
155
    }
156
157
    protected function logElementDeleted(AbstractDBElement $entity, EntityManagerInterface $em): void
158
    {
159
        $log = new ElementDeletedLogEntry($entity);
160
        //Add user comment to log entry
161
        if ($this->eventCommentHelper->isMessageSet()) {
162
            $log->setComment($this->eventCommentHelper->getMessage());
163
        }
164
        if ($this->eventUndoHelper->isUndo()) {
165
            $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
166
        }
167
        if ($this->save_removed_data) {
168
            //The 4th param is important here, as we delete the element...
169
            $this->saveChangeSet($entity, $log, $em, true);
170
        }
171
        $this->logger->log($log);
172
173
        //Check if we have to log CollectionElementDeleted entries
174
        if ($this->save_changed_data) {
175
            $metadata = $em->getClassMetadata(get_class($entity));
176
            $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

176
            /** @scrutinizer ignore-call */ 
177
            $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...
177
            //Check if class is whitelisted for CollectionElementDeleted entry
178
            foreach (static::TRIGGER_ASSOCIATION_LOG_WHITELIST as $class => $whitelist) {
179
                if (is_a($entity, $class)) {
180
                    //Check names
181
                    foreach ($mappings as $field => $mapping) {
182
                        if (in_array($field, $whitelist)) {
183
                            $changed = $this->propertyAccessor->getValue($entity, $field);
184
                            $log = new CollectionElementDeleted($changed, $mapping['inversedBy'], $entity);
185
                            if ($this->eventUndoHelper->isUndo()) {
186
                                $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
187
                            }
188
                            $this->logger->log($log);
189
                        }
190
                    }
191
                }
192
            }
193
        }
194
    }
195
196
    protected function logElementEdited(AbstractDBElement $entity, EntityManagerInterface $em): void
197
    {
198
        $uow = $em->getUnitOfWork();
199
200
        $log = new ElementEditedLogEntry($entity);
201
        if ($this->save_changed_data) {
202
            $this->saveChangeSet($entity, $log, $em);
203
        } elseif ($this->save_changed_fields) {
204
            $changed_fields = array_keys($uow->getEntityChangeSet($entity));
205
            //Remove lastModified field, as this is always changed (gives us no additional info)
206
            $changed_fields = array_diff($changed_fields, ['lastModified']);
207
            $log->setChangedFields($changed_fields);
208
        }
209
        //Add user comment to log entry
210
        if ($this->eventCommentHelper->isMessageSet()) {
211
            $log->setComment($this->eventCommentHelper->getMessage());
212
        }
213
        if ($this->eventUndoHelper->isUndo()) {
214
            $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode());
215
        }
216
        $this->logger->log($log);
217
    }
218
219
    /**
220
     * Check if the given element class has restrictions to its fields
221
     * @param  AbstractDBElement  $element
222
     * @return bool True if there are restrictions, and further checking is needed
223
     */
224
    public function hasFieldRestrictions(AbstractDBElement $element): bool
225
    {
226
        foreach (static::FIELD_BLACKLIST as $class => $blacklist) {
227
            if (is_a($element, $class)) {
228
                return true;
229
            }
230
        }
231
232
        return false;
233
    }
234
235
    /**
236
     * Filter out every forbidden field and return the cleaned array.
237
     * @param  AbstractDBElement  $element
238
     * @param  array  $fields
239
     * @return array
240
     */
241
    protected function filterFieldRestrictions(AbstractDBElement $element, array $fields): array
242
    {
243
        unset($fields['lastModified']);
244
245
        if (!$this->hasFieldRestrictions($element)) {
246
            return $fields;
247
        }
248
249
        return array_filter($fields, function ($value, $key) use ($element) {
250
            //Associative array (save changed data) case
251
            if (is_string($key)) {
252
                return $this->shouldFieldBeSaved($element, $key);
253
            }
254
255
            return $this->shouldFieldBeSaved($element, $value);
256
        }, ARRAY_FILTER_USE_BOTH);
257
    }
258
259
    /**
260
     * Checks if the field of the given element should be saved (if it is not blacklisted).
261
     * @param  AbstractDBElement  $element
262
     * @param  string  $field_name
263
     * @return bool
264
     */
265
    public function shouldFieldBeSaved(AbstractDBElement $element, string $field_name): bool
266
    {
267
        foreach (static::FIELD_BLACKLIST as $class => $blacklist) {
268
            if (is_a($element, $class) && in_array($field_name, $blacklist)) {
269
                return false;
270
            }
271
        }
272
273
        //By default allow every field.
274
        return true;
275
    }
276
277
    protected function saveChangeSet(AbstractDBElement $entity, AbstractLogEntry $logEntry, EntityManagerInterface $em, $element_deleted = false): void
278
    {
279
        $uow = $em->getUnitOfWork();
280
281
        if (!$logEntry instanceof ElementEditedLogEntry && !$logEntry instanceof ElementDeletedLogEntry) {
282
            throw new \InvalidArgumentException('$logEntry must be ElementEditedLogEntry or ElementDeletedLogEntry!');
283
        }
284
285
        if ($element_deleted) { //If the element was deleted we can use getOriginalData to save its content
286
            $old_data = $uow->getOriginalEntityData($entity);
287
        } else { //Otherwise we have to get it from entity changeset
288
            $changeSet = $uow->getEntityChangeSet($entity);
289
            $old_data = array_combine(array_keys($changeSet), array_column($changeSet, 0));
290
        }
291
        $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\Even...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

291
        $old_data = $this->filterFieldRestrictions($entity, /** @scrutinizer ignore-type */ $old_data);
Loading history...
292
293
        //Restrict length of string fields, to save memory...
294
        $old_data = array_map(function ($value) {
295
            if (is_string($value)) {
296
                return mb_strimwidth($value, 0, self::MAX_STRING_LENGTH, '...');
297
            }
298
299
            return $value;
300
        }, $old_data);
301
302
        $logEntry->setOldData($old_data);
303
    }
304
305
    /**
306
     * Check if the given entity can be logged.
307
     * @param object $entity
308
     * @return bool True, if the given entity can be logged.
309
     */
310
    protected function validEntity(object $entity): bool
311
    {
312
        //Dont log logentries itself!
313
        if ($entity instanceof AbstractDBElement && !$entity instanceof AbstractLogEntry) {
314
            return true;
315
        }
316
317
        return false;
318
    }
319
320
    /**
321
     * @inheritDoc
322
     */
323
    public function getSubscribedEvents()
324
    {
325
        return[
326
            Events::onFlush,
327
            Events::postPersist,
328
            Events::postFlush
329
        ];
330
    }
331
}