ReferenceSubscriber::updatePersist()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 4
c 0
b 0
f 0
nc 2
nop 3
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 3
rs 10
1
<?php
2
3
/**
4
 * DoctrineExtendListener.php
5
 *
6
 * @since 06/03/18
7
 * @author gseidel
8
 */
9
10
namespace Enhavo\Bundle\DoctrineExtensionBundle\EventListener;
11
12
use Doctrine\Common\EventSubscriber;
13
use Doctrine\ORM\Event\LifecycleEventArgs;
14
use Doctrine\ORM\EntityManagerInterface;
15
use Doctrine\ORM\Event\PostFlushEventArgs;
16
use Doctrine\ORM\Event\PreFlushEventArgs;
17
use Doctrine\ORM\Events;
18
use Doctrine\ORM\Proxy\Proxy;
19
use Enhavo\Bundle\DoctrineExtensionBundle\EntityResolver\EntityResolverInterface;
20
use Enhavo\Bundle\DoctrineExtensionBundle\Metadata\Metadata;
21
use Enhavo\Bundle\DoctrineExtensionBundle\Metadata\Reference;
22
use Enhavo\Component\Metadata\MetadataRepository;
23
use Symfony\Component\PropertyAccess\PropertyAccess;
24
use Doctrine\ORM\UnitOfWork;
25
26
/**
27
 * Class ReferenceSubscriber
28
 *
29
 * A reference extension for doctrine, to reference multiple entities within one property.
30
 */
31
class ReferenceSubscriber implements EventSubscriber
32
{
33
    /** @var MetadataRepository */
34
    private $metadataRepository;
35
36
    /** @var EntityResolverInterface */
37
    private $entityResolver;
38
39 3
    public function __construct(MetadataRepository $metadataRepository, EntityResolverInterface $entityResolver)
40
    {
41 3
        $this->metadataRepository = $metadataRepository;
42 3
        $this->entityResolver = $entityResolver;
43
    }
44
45
    /**
46
     * @param $object
47
     * @return Metadata|null
48
     */
49 3
    private function getMetadata($object): ?Metadata
50
    {
51
        /** @var Metadata $metadata */
52 3
        $metadata = $this->metadataRepository->getMetadata($object);
53 3
        if ($metadata === null) {
54 3
            return null;
55
        }
56 3
        return $metadata;
57
    }
58
59
    /**
60
     * @return Metadata[]
61
     */
62 3
    private function getAllMetadata(): array
63
    {
64 3
        $data = [];
65
        /** @var Metadata $metadata */
66 3
        foreach ($this->metadataRepository->getAllMetadata() as $metadata) {
67 3
            if (count($metadata->getReferences()) > 0 && !in_array($metadata, $data)) {
0 ignored issues
show
Bug introduced by
The method getReferences() does not exist on Enhavo\Component\Metadata\Metadata. It seems like you code against a sub-type of Enhavo\Component\Metadata\Metadata such as Enhavo\Bundle\DoctrineEx...undle\Metadata\Metadata. ( Ignorable by Annotation )

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

67
            if (count($metadata->/** @scrutinizer ignore-call */ getReferences()) > 0 && !in_array($metadata, $data)) {
Loading history...
68 3
                $data[] = $metadata;
69
            }
70
        }
71 3
        return $data;
72
    }
73
74
    /**
75
     * {@inheritDoc}
76
     */
77 3
    public function getSubscribedEvents()
78
    {
79 3
        return [
80 3
            Events::postLoad,
81 3
            Events::preFlush,
82 3
            Events::postFlush,
83 3
        ];
84
    }
85
86
    /**
87
     * Insert target class on load
88
     *
89
     * @param $args LifecycleEventArgs
90
     */
91 3
    public function postLoad(LifecycleEventArgs $args)
92
    {
93 3
        $object = $args->getObject();
94
95 3
        $metadata = $this->getMetadata($object);
96 3
        if ($metadata === null) {
97 3
            return;
98
        }
99
100 3
        if (get_class($object) === $metadata->getClassName() || $this->isParentClass($object, $metadata->getClassName())) {
101 3
            foreach ($metadata->getReferences() as $reference) {
102 3
                $this->loadEntity($reference, $object);
103
            }
104
        }
105
    }
106
107 2
    private function isParentClass($object, $class)
108
    {
109 2
        $parentClass = get_parent_class($object);
110 2
        if ($parentClass === false) {
111
            return false;
112 2
        } elseif ($parentClass === $class) {
113 2
            return true;
114
        } else {
115
            return $this->isParentClass($parentClass, $class);
116
        }
117
    }
118
119 3
    private function loadEntity(Reference $reference, $entity)
120
    {
121 3
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
122 3
        $id = $propertyAccessor->getValue($entity, $reference->getIdField());
123 3
        $class = $propertyAccessor->getValue($entity, $reference->getNameField());
124
125 3
        if ($id && $class) {
126 3
            $targetEntity = $this->entityResolver->getEntity($id, $class);
127 3
            if ($targetEntity !== null) {
128 3
                $propertyAccessor->setValue($entity, $reference->getProperty(), $targetEntity);
129
            }
130
        }
131
    }
132
133
    /**
134
     * Update entity before flush to prevent possible changes after flush
135
     *
136
     * @param PreFlushEventArgs $args
137
     */
138 3
    public function preFlush(PreFlushEventArgs $args)
139
    {
140 3
        $uow = $args->getEntityManager()->getUnitOfWork();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\Event\PreFl...rgs::getEntityManager() has been deprecated: 2.13. Use {@see getObjectManager} instead. ( Ignorable by Annotation )

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

140
        $uow = /** @scrutinizer ignore-deprecated */ $args->getEntityManager()->getUnitOfWork();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
141 3
        $em = $args->getEntityManager();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\Event\PreFl...rgs::getEntityManager() has been deprecated: 2.13. Use {@see getObjectManager} instead. ( Ignorable by Annotation )

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

141
        $em = /** @scrutinizer ignore-deprecated */ $args->getEntityManager();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
142 3
        $identityMap = $uow->getIdentityMap();
143
144 3
        foreach ($this->getAllMetadata() as $metadata) {
145 3
            if (isset($identityMap[$metadata->getClassName()])) {
146 2
                foreach ($identityMap[$metadata->getClassName()] as $entity) {
147 2
                    foreach ($metadata->getReferences() as $reference) {
148 2
                        $this->updateEntity($reference, $entity);
149 2
                        $this->updatePersist($reference, $entity, $em);
150
                    }
151
                }
152
            }
153
154 3
            foreach ($uow->getScheduledEntityInsertions() as $entity) {
155 3
                if (get_class($entity) === $metadata->getClassName()) {
156 3
                    foreach ($metadata->getReferences() as $reference) {
157 3
                        $this->updateEntity($reference, $entity);
158 3
                        $this->updatePersist($reference, $entity, $em);
159
                    }
160
                }
161
            }
162
        }
163
    }
164
165 3
    private function updatePersist(Reference $reference, $entity, EntityManagerInterface $em)
166
    {
167 3
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
168 3
        $targetProperty = $propertyAccessor->getValue($entity, $reference->getProperty());
169
170 3
        if ($targetProperty && $em->getUnitOfWork()->getEntityState($targetProperty) !== UnitOfWork::STATE_MANAGED) {
171 3
            $em->persist($targetProperty);
172
        }
173
    }
174
175
    /**
176
     * Check if entity is not up to date and execute changes
177
     *
178
     * @param PostFlushEventArgs $args
179
     */
180 3
    public function postFlush(PostFlushEventArgs $args)
181
    {
182 3
        $persist = false;
183 3
        $changes = [];
184
185 3
        $em = $args->getEntityManager();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\Event\PostF...rgs::getEntityManager() has been deprecated: 2.13. Use {@see getObjectManager} instead. ( Ignorable by Annotation )

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

185
        $em = /** @scrutinizer ignore-deprecated */ $args->getEntityManager();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
186 3
        $uow = $args->getEntityManager()->getUnitOfWork();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\Event\PostF...rgs::getEntityManager() has been deprecated: 2.13. Use {@see getObjectManager} instead. ( Ignorable by Annotation )

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

186
        $uow = /** @scrutinizer ignore-deprecated */ $args->getEntityManager()->getUnitOfWork();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
187 3
        $result = $uow->getIdentityMap();
188
189 3
        foreach ($this->getAllMetadata() as $metadata) {
190 3
            if (isset($result[$metadata->getClassName()])) {
191 3
                foreach ($result[$metadata->getClassName()] as $entity) {
192 3
                    foreach ($metadata->getReferences() as $reference) {
193 3
                        $entityChanges = $this->updateEntity($reference, $entity);
194 3
                        foreach ($entityChanges as $entityChange) {
195 3
                            $changes[] = $entityChange;
196
                        }
197
198 3
                        $propertyAccessor = PropertyAccess::createPropertyAccessor();
199 3
                        $targetProperty = $propertyAccessor->getValue($entity, $reference->getProperty());
200
201 3
                        if ($targetProperty && $uow->getEntityState($targetProperty) !== UnitOfWork::STATE_MANAGED) {
202 1
                            $em->persist($targetProperty);
203 1
                            $persist = true;
204
                        }
205
                    }
206
                }
207
            }
208
        }
209
210 3
        if (count($changes)) {
211 3
            $this->executeChanges($changes, $args->getEntityManager());
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\Event\PostF...rgs::getEntityManager() has been deprecated: 2.13. Use {@see getObjectManager} instead. ( Ignorable by Annotation )

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

211
            $this->executeChanges($changes, /** @scrutinizer ignore-deprecated */ $args->getEntityManager());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
212
        }
213
214 3
        if ($persist) {
215 1
            $em->flush();
216
        }
217
    }
218
219
    /**
220
     * Update entity values
221
     *
222
     * @param Reference $reference
223
     * @param object $entity
224
     * @return ReferenceChange[]
225
     */
226 3
    private function updateEntity(Reference $reference, $entity)
227
    {
228 3
        $changes = [];
229
230 3
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
231 3
        $targetProperty = $propertyAccessor->getValue($entity, $reference->getProperty());
232
233
        // if entity state is still proxy, we try to load its entity to make sure that target property is correct
234 3
        if ($entity instanceof Proxy && $targetProperty === null) {
235
            $this->loadEntity($reference, $entity);
236
            $targetProperty = $propertyAccessor->getValue($entity, $reference->getProperty());
237
        }
238
239 3
        if ($targetProperty) {
240
            // update id property
241 3
            $idProperty = $propertyAccessor->getValue($entity, $reference->getIdField());
242 3
            $id = $propertyAccessor->getValue($targetProperty, 'id');
243 3
            if ($idProperty != $id) {
244 3
                $propertyAccessor->setValue($entity, $reference->getIdField(), $id);
245 3
                $changes[] = new ReferenceChange($entity, $reference->getIdField(), $id);
246
            }
247
248
            // update class property
249 3
            $classProperty = $propertyAccessor->getValue($entity, $reference->getNameField());
250 3
            $class = $this->entityResolver->getName($targetProperty);
251 3
            if ($classProperty != $class) {
252 3
                $propertyAccessor->setValue($entity, $reference->getNameField(), $class);
253 3
                $changes[] = new ReferenceChange($entity, $reference->getNameField(), $class);
254
            }
255
        } else {
256 1
            if (null !== $propertyAccessor->getValue($entity, $reference->getIdField())) {
257 1
                $propertyAccessor->setValue($entity, $reference->getIdField(), null);
258 1
                $changes[] = new ReferenceChange($entity, $reference->getIdField(), null);
259
            }
260
261 1
            if (null !== $propertyAccessor->getValue($entity, $reference->getNameField())) {
262 1
                $propertyAccessor->setValue($entity, $reference->getNameField(), null);
263 1
                $changes[] = new ReferenceChange($entity, $reference->getNameField(), null);
264
            }
265
        }
266
267 3
        return $changes;
268
    }
269
270
    /**
271
     * @param ReferenceChange[] $changes
272
     * @param EntityManagerInterface $em
273
     */
274 3
    private function executeChanges($changes, EntityManagerInterface $em)
275
    {
276 3
        $queries = [];
277 3
        $entities = [];
278 3
        foreach ($changes as $change) {
279 3
            $query = $em->createQueryBuilder()
280 3
                ->update(get_class($change->getEntity()), 'e')
281 3
                ->set(sprintf('e.%s', $change->getProperty()), ':value')
282 3
                ->where('e.id = :id')
283 3
                ->setParameter('value', $change->getValue())
284 3
                ->setParameter('id', $change->getEntity()->getId())
285 3
                ->getQuery()
286 3
            ;
287
288 3
            $queries[] = $query;
289
290 3
            if (!in_array($change->getEntity(), $entities)) {
291 3
                $entities[] = $change->getEntity();
292
            }
293
        }
294
295 3
        foreach ($queries as $query) {
296 3
            $query->execute();
297
        }
298
299 3
        foreach ($entities as $entity) {
300 3
            $em->refresh($entity);
301
        }
302
    }
303
}
304