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

TimeTravel::getField()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 7
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
7
 *
8
 * Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
9
 *
10
 * This program is free software; you can redistribute it and/or
11
 * modify it under the terms of the GNU General Public License
12
 * as published by the Free Software Foundation; either version 2
13
 * of the License, or (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program; if not, write to the Free Software
22
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
23
 */
24
25
namespace App\Services\LogSystem;
26
27
use App\Entity\Attachments\AttachmentType;
28
use App\Entity\Base\AbstractDBElement;
29
use App\Entity\Base\AbstractStructuralDBElement;
30
use App\Entity\Contracts\TimeStampableInterface;
31
use App\Entity\Contracts\TimeTravelInterface;
32
use App\Entity\LogSystem\AbstractLogEntry;
33
use App\Entity\LogSystem\CollectionElementDeleted;
34
use App\Entity\LogSystem\ElementEditedLogEntry;
35
use Brick\Math\BigDecimal;
36
use Doctrine\Common\Collections\Collection;
37
use Doctrine\ORM\EntityManagerInterface;
38
use Doctrine\ORM\Mapping\ClassMetadata;
39
40
class TimeTravel
41
{
42
    protected $em;
43
    protected $repo;
44
45
    public function __construct(EntityManagerInterface $em)
46
    {
47
        $this->em = $em;
48
        $this->repo = $em->getRepository(AbstractLogEntry::class);
49
    }
50
51
    /**
52
     * Undeletes the element with the given ID.
53
     *
54
     * @param string $class The class name of the element that should be undeleted
55
     * @param int    $id    The ID of the element that should be undeleted.
56
     *
57
     * @return AbstractDBElement
58
     */
59
    public function undeleteEntity(string $class, int $id): AbstractDBElement
60
    {
61
        $log = $this->repo->getUndeleteDataForElement($class, $id);
62
        $element = new $class();
63
        $this->applyEntry($element, $log);
64
65
        //Set internal ID so the element can be reverted
66
        $this->setField($element, 'id', $id);
67
68
        //Let database determine when it will be created
69
        $this->setField($element, 'addedDate', null);
70
71
        return $element;
72
    }
73
74
    /**
75
     * Revert the given element to the state it has on the given timestamp.
76
     *
77
     * @param AbstractLogEntry[] $reverted_elements
78
     *
79
     * @throws \Exception
80
     */
81
    public function revertEntityToTimestamp(AbstractDBElement $element, \DateTime $timestamp, array $reverted_elements = []): void
82
    {
83
        if (! $element instanceof TimeStampableInterface) {
84
            throw new \InvalidArgumentException('$element must have a Timestamp!');
85
        }
86
87
        if ($timestamp > new \DateTime('now')) {
88
            throw new \InvalidArgumentException('You can not travel to the future (yet)...');
89
        }
90
91
        //Skip this process if already were reverted...
92
        if (in_array($element, $reverted_elements, true)) {
93
            return;
94
        }
95
        $reverted_elements[] = $element;
96
97
        $history = $this->repo->getTimetravelDataForElement($element, $timestamp);
98
99
        /*
100
        if (!$this->repo->getElementExistedAtTimestamp($element, $timestamp)) {
101
            $element = null;
102
            return;
103
        }*/
104
105
        foreach ($history as $logEntry) {
106
            if ($logEntry instanceof ElementEditedLogEntry) {
107
                $this->applyEntry($element, $logEntry);
108
            }
109
            if ($logEntry instanceof CollectionElementDeleted) {
110
                //Undelete element and add it to collection again
111
                $undeleted = $this->undeleteEntity(
112
                    $logEntry->getDeletedElementClass(),
113
                    $logEntry->getDeletedElementID()
114
                );
115
                if ($this->repo->getElementExistedAtTimestamp($undeleted, $timestamp)) {
116
                    $this->revertEntityToTimestamp($undeleted, $timestamp, $reverted_elements);
117
                    $collection = $this->getField($element, $logEntry->getCollectionName());
118
                    if ($collection instanceof Collection) {
119
                        $collection->add($undeleted);
120
                    }
121
                }
122
            }
123
        }
124
125
        // Revert any of the associated elements
126
        $metadata = $this->em->getClassMetadata(get_class($element));
127
        $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

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

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

189
                if ($metadata->/** @scrutinizer ignore-call */ getFieldMapping($field)['type'] === 'big_decimal') {
Loading history...
190
                    //We need to convert the string to a BigDecimal first
191
                    if (!$data instanceof BigDecimal) {
192
                        $data = BigDecimal::of($data);
193
                    }
194
                }
195
196
                $this->setField($element, $field, $data);
0 ignored issues
show
Bug introduced by
$data of type Brick\Math\BigDecimal is incompatible with the type DateTime|integer|null expected by parameter $new_value of App\Services\LogSystem\TimeTravel::setField(). ( Ignorable by Annotation )

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

196
                $this->setField($element, $field, /** @scrutinizer ignore-type */ $data);
Loading history...
197
            }
198
            if ($metadata->hasAssociation($field)) {
199
                $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

199
                /** @scrutinizer ignore-call */ 
200
                $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...
200
                $target_class = $mapping['targetEntity'];
201
                //Try to extract the old ID:
202
                if (is_array($data) && isset($data['@id'])) {
203
                    $entity = $this->em->getPartialReference($target_class, $data['@id']);
204
                    $this->setField($element, $field, $entity);
205
                }
206
            }
207
        }
208
209
        $this->setField($element, 'lastModified', $logEntry->getTimestamp());
210
    }
211
212
    protected function getField(AbstractDBElement $element, string $field)
213
    {
214
        $reflection = new \ReflectionClass(get_class($element));
215
        $property = $reflection->getProperty($field);
216
        $property->setAccessible(true);
217
218
        return $property->getValue($element);
219
    }
220
221
    /**
222
     * @param \DateTime|int|null $new_value
223
     */
224
    protected function setField(AbstractDBElement $element, string $field, $new_value): void
225
    {
226
        $reflection = new \ReflectionClass(get_class($element));
227
        $property = $reflection->getProperty($field);
228
        $property->setAccessible(true);
229
230
231
        $property->setValue($element, $new_value);
232
    }
233
}
234