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

TimeTravel   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 179
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 81
c 2
b 0
f 0
dl 0
loc 179
rs 9.1199
wmc 41

6 Methods

Rating   Name   Duplication   Size   Complexity  
D revertEntityToTimestamp() 0 83 29
A undeleteEntity() 0 13 1
B applyEntry() 0 28 8
A setField() 0 6 1
A __construct() 0 4 1
A getField() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like TimeTravel often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TimeTravel, and based on these observations, apply Extract Interface, too.

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
8
 * modify it under the terms of the GNU General Public License
9
 * as published by the Free Software Foundation; either version 2
10
 * of the License, or (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 General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
20
 */
21
22
namespace App\Services\LogSystem;
23
24
25
use App\Entity\Attachments\AttachmentType;
26
use App\Entity\Base\AbstractDBElement;
27
use App\Entity\Base\AbstractStructuralDBElement;
28
use App\Entity\Contracts\TimeStampableInterface;
29
use App\Entity\Contracts\TimeTravelInterface;
30
use App\Entity\LogSystem\AbstractLogEntry;
31
use App\Entity\LogSystem\CollectionElementDeleted;
32
use App\Entity\LogSystem\ElementEditedLogEntry;
33
use Doctrine\Common\Collections\Collection;
34
use Doctrine\ORM\EntityManagerInterface;
35
use Doctrine\ORM\Mapping\ClassMetadata;
36
37
class TimeTravel
38
{
39
    protected $em;
40
    protected $repo;
41
42
    public function __construct(EntityManagerInterface $em)
43
    {
44
        $this->em = $em;
45
        $this->repo = $em->getRepository(AbstractLogEntry::class);
46
    }
47
48
    /**
49
     * Undeletes the element with the given ID.
50
     * @param  string  $class  The class name of the element that should be undeleted
51
     * @param  int  $id  The ID of the element that should be undeleted.
52
     * @return AbstractDBElement
53
     */
54
    public function undeleteEntity(string $class, int $id): AbstractDBElement
55
    {
56
        $log = $this->repo->getUndeleteDataForElement($class, $id);
57
        $element = new $class();
58
        $this->applyEntry($element, $log);
59
60
        //Set internal ID so the element can be reverted
61
        $this->setField($element, 'id', $id);
62
63
        //Let database determine when it will be created
64
        $this->setField($element,'addedDate', null);
65
66
        return $element;
67
    }
68
69
    /**
70
     * Revert the given element to the state it has on the given timestamp
71
     * @param  AbstractDBElement  $element
72
     * @param  \DateTime  $timestamp
73
     * @param  AbstractLogEntry[]  $reverted_elements
74
     * @throws \Exception
75
     */
76
    public function revertEntityToTimestamp(AbstractDBElement $element, \DateTime $timestamp, array $reverted_elements = [])
77
    {
78
        if (!$element instanceof TimeStampableInterface) {
79
            throw new \InvalidArgumentException('$element must have a Timestamp!');
80
        }
81
82
        if ($timestamp > new \DateTime('now')) {
83
            throw new \InvalidArgumentException('You can not travel to the future (yet)...');
84
        }
85
86
        //Skip this process if already were reverted...
87
        if (in_array($element, $reverted_elements)) {
88
            return;
89
        }
90
        $reverted_elements[] = $element;
91
92
        $history = $this->repo->getTimetravelDataForElement($element, $timestamp);
93
94
        /*
95
        if (!$this->repo->getElementExistedAtTimestamp($element, $timestamp)) {
96
            $element = null;
97
            return;
98
        }*/
99
100
        foreach ($history as $logEntry) {
101
            if ($logEntry instanceof ElementEditedLogEntry) {
102
                $this->applyEntry($element, $logEntry);
103
            }
104
            if ($logEntry instanceof CollectionElementDeleted) {
105
                //Undelete element and add it to collection again
106
                $undeleted = $this->undeleteEntity(
107
                    $logEntry->getDeletedElementClass(),
108
                    $logEntry->getDeletedElementID()
109
                );
110
                if ($this->repo->getElementExistedAtTimestamp($undeleted, $timestamp)) {
111
                    $this->revertEntityToTimestamp($undeleted, $timestamp, $reverted_elements);
112
                    $collection = $this->getField($element, $logEntry->getCollectionName());
113
                    if ($collection instanceof Collection) {
114
                        $collection->add($undeleted);
115
                    }
116
                }
117
            }
118
        }
119
120
        // Revert any of the associated elements
121
        $metadata = $this->em->getClassMetadata(get_class($element));
122
        $associations = $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

122
        /** @scrutinizer ignore-call */ 
123
        $associations = $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...
123
        foreach ($associations as $field => $mapping) {
124
            if (
125
                ($element instanceof AbstractStructuralDBElement && ($field === 'parts' || $field === 'children'))
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($element instanceof App...field === 'attachments', Probably Intended Meaning: $element instanceof App\...ield === 'attachments')
Loading history...
126
                || ($element instanceof AttachmentType && $field === 'attachments')
127
            ) {
128
                continue;
129
            }
130
131
132
            //Revert many to one association (one element in property)
133
            if (
134
                $mapping['type'] === ClassMetadata::MANY_TO_ONE
135
                || $mapping['type'] === ClassMetadata::ONE_TO_ONE
136
            ) {
137
                $target_element = $this->getField($element, $field);
138
                if ($target_element !== null && $element->getLastModified() > $timestamp) {
139
                    $this->revertEntityToTimestamp($target_element, $timestamp, $reverted_elements);
140
                }
141
            } elseif ( //Revert *_TO_MANY associations (collection properties)
142
                ($mapping['type'] === ClassMetadata::MANY_TO_MANY
143
                    || $mapping['type'] === ClassMetadata::ONE_TO_MANY)
144
                && $mapping['isOwningSide'] === false
145
            ) {
146
                $target_elements = $this->getField($element, $field);
147
                if ($target_elements === null || count($target_elements) > 10) {
148
                    continue;
149
                }
150
                foreach ($target_elements as $target_element) {
151
                    if ($target_element !== null && $element->getLastModified() >= $timestamp) {
152
                        //Remove the element from collection, if it did not existed at $timestamp
153
                        if (!$this->repo->getElementExistedAtTimestamp($target_element, $timestamp)) {
154
                            if ($target_elements instanceof Collection) {
155
                                $target_elements->removeElement($target_element);
156
                            }
157
                        }
158
                        $this->revertEntityToTimestamp($target_element, $timestamp, $reverted_elements);
159
                    }
160
                }
161
            }
162
163
        }
164
    }
165
166
    /**
167
     * Apply the changeset in the given LogEntry to the element
168
     * @param  AbstractDBElement  $element
169
     * @param  TimeTravelInterface  $logEntry
170
     * @throws \Doctrine\ORM\Mapping\MappingException
171
     */
172
    public function applyEntry(AbstractDBElement $element, TimeTravelInterface $logEntry): void
173
    {
174
        //Skip if this does not provide any info...
175
        if (!$logEntry->hasOldDataInformations()) {
176
            return;
177
        }
178
        if (!$element instanceof TimeStampableInterface) {
179
            return;
180
        }
181
        $metadata = $this->em->getClassMetadata(get_class($element));
182
        $old_data = $logEntry->getOldData();
183
184
        foreach ($old_data as $field => $data) {
185
            if ($metadata->hasField($field)) {
186
                $this->setField($element, $field, $data);
187
            }
188
            if ($metadata->hasAssociation($field)) {
189
                $mapping = $metadata->getAssociationMapping($field);
0 ignored issues
show
Bug introduced by
The method getAssociationMapping() 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

189
                /** @scrutinizer ignore-call */ 
190
                $mapping = $metadata->getAssociationMapping($field);

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...
190
                $target_class = $mapping['targetEntity'];
191
                //Try to extract the old ID:
192
                if (is_array($data) && isset($data['@id'])) {
193
                    $entity = $this->em->getPartialReference($target_class, $data['@id']);
194
                    $this->setField($element, $field, $entity);
195
                }
196
            }
197
        }
198
199
        $this->setField($element, 'lastModified', $logEntry->getTimestamp());
200
    }
201
202
    protected function getField(AbstractDBElement $element, string $field)
203
    {
204
        $reflection = new \ReflectionClass(get_class($element));
205
        $property = $reflection->getProperty($field);
206
        $property->setAccessible(true);
207
        return $property->getValue($element);
208
    }
209
210
    protected function setField(AbstractDBElement $element, string $field, $new_value)
211
    {
212
        $reflection = new \ReflectionClass(get_class($element));
213
        $property = $reflection->getProperty($field);
214
        $property->setAccessible(true);
215
        $property->setValue($element, $new_value);
216
    }
217
}