Completed
Pull Request — master (#812)
by Paul
06:29
created

WidgetDataWarmer::extractAssociatedEntities()   D

Complexity

Conditions 18
Paths 130

Size

Total Lines 80
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 80
rs 4.6966
c 0
b 0
f 0
cc 18
eloc 37
nc 130
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Victoire\Bundle\WidgetMapBundle\Warmer;
4
5
use Doctrine\ORM\EntityManager;
6
use Symfony\Component\PropertyAccess\PropertyAccess;
7
use Victoire\Bundle\BusinessEntityBundle\Resolver\BusinessEntityResolver;
8
use Victoire\Bundle\BusinessPageBundle\Entity\BusinessTemplate;
9
use Victoire\Bundle\CoreBundle\Entity\Link;
10
use Victoire\Bundle\CoreBundle\Entity\View;
11
use Victoire\Bundle\CriteriaBundle\Entity\Criteria;
12
use Victoire\Bundle\PageBundle\Entity\Page;
13
use Victoire\Bundle\ViewReferenceBundle\Connector\ViewReferenceRepository;
14
use Victoire\Bundle\ViewReferenceBundle\ViewReference\ViewReference;
15
use Victoire\Bundle\WidgetBundle\Entity\Traits\LinkTrait;
16
use Victoire\Bundle\WidgetBundle\Entity\Widget;
17
use Victoire\Bundle\WidgetBundle\Helper\WidgetHelper;
18
use Victoire\Bundle\WidgetBundle\Repository\WidgetRepository;
19
use Victoire\Bundle\WidgetMapBundle\Entity\WidgetMap;
20
21
/**
22
 * WidgetDataWarmer.
23
 *
24
 * This class prepare all widgets with their associated entities for the current View
25
 * to reduce queries during page rendering.
26
 * Only OneToMany and ManyToOne associations are handled because no OneToOne or ManyToMany
27
 * associations have been used in Widgets.
28
 *
29
 * Ref: victoire_widget_map.widget_data_warmer
30
 */
31
class WidgetDataWarmer
32
{
33
    /* @var $em EntityManager */
34
    protected $em;
35
    protected $viewReferenceRepository;
36
    protected $widgetHelper;
37
    protected $accessor;
38
    protected $manyToOneAssociations;
39
    /**
40
     * @var BusinessEntityResolver
41
     */
42
    private $businessEntityResolver;
43
44
    /**
45
     * Constructor.
46
     *
47
     * @param ViewReferenceRepository $viewReferenceRepository
48
     * @param WidgetHelper            $widgetHelper
49
     * @param BusinessEntityResolver  $businessEntityResolver
50
     */
51
    public function __construct(
52
        ViewReferenceRepository $viewReferenceRepository,
53
        WidgetHelper $widgetHelper,
54
        BusinessEntityResolver $businessEntityResolver
55
    ) {
56
        $this->viewReferenceRepository = $viewReferenceRepository;
57
        $this->widgetHelper = $widgetHelper;
58
        $this->accessor = PropertyAccess::createPropertyAccessor();
59
        $this->businessEntityResolver = $businessEntityResolver;
60
    }
61
62
    /**
63
     * Find all Widgets for current View, inject them in WidgetMap and warm associated entities.
64
     *
65
     * @param EntityManager $em
66
     * @param View          $view
67
     */
68
    public function warm(EntityManager $em, View $view)
0 ignored issues
show
Bug introduced by
You have injected the EntityManager via parameter $em. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

Loading history...
69
    {
70
        $this->em = $em;
71
72
        /* @var WidgetRepository $widgetRepo */
73
        $widgetRepo = $this->em->getRepository('Victoire\Bundle\WidgetBundle\Entity\Widget');
74
        $viewWidgets = $widgetRepo->findAllWidgetsForView($view);
75
76
        $this->injectWidgets($view, $viewWidgets);
77
78
        $this->extractAssociatedEntities($viewWidgets);
79
    }
80
81
    /**
82
     * Inject Widgets in View's builtWidgetMap.
83
     *
84
     * @param View $view
85
     * @param $viewWidgets
86
     */
87
    private function injectWidgets(View $view, $viewWidgets)
88
    {
89
        $builtWidgetMap = $view->getBuiltWidgetMap();
90
91
        foreach ($builtWidgetMap as $slot => $widgetMaps) {
92
            foreach ($widgetMaps as $i => $widgetMap) {
93
                foreach ($viewWidgets as $widget) {
94
                    if ($widget->getWidgetMap() == $widgetMap) {
95
                        $builtWidgetMap[$slot][$i]->addWidget($widget);
96
97
                        //Override Collection default behaviour to avoid useless query
98
                        $builtWidgetMap[$slot][$i]->getWidgets()->setDirty(false);
99
                        $builtWidgetMap[$slot][$i]->getWidgets()->setInitialized(true);
100
                        continue;
101
                    }
102
                }
103
            }
104
        }
105
106
        $view->setBuiltWidgetMap($builtWidgetMap);
107
    }
108
109
    /**
110
     * Pass through all widgets and associated entities to extract all missing associations,
111
     * store it by repository to group queries by entity type.
112
     *
113
     * @param Widget[] $entities Widgets and associated entities
114
     */
115
    private function extractAssociatedEntities(array $entities)
116
    {
117
        $linkIds = $associatedEntities = [];
118
119
        foreach ($entities as $entity) {
120
            $reflect = new \ReflectionClass($entity);
121
122
            //If Widget is already in cache, extract only its Criterias (used outside Widget rendering)
123
            $widgetCached = ($entity instanceof Widget && $this->widgetHelper->isCacheEnabled($entity));
124
125
            //If Widget has LinkTrait, store the entity link id
126
            if (!$widgetCached && $this->hasLinkTrait($reflect) && $entity->getLink()) {
0 ignored issues
show
Bug introduced by
The method getLink() does not seem to exist on object<Victoire\Bundle\W...etBundle\Entity\Widget>.

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...
127
                $linkIds[] = $entity->getLink()->getId();
0 ignored issues
show
Bug introduced by
The method getLink() does not seem to exist on object<Victoire\Bundle\W...etBundle\Entity\Widget>.

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
            }
129
130
            //Pass through all entity associations
131
            $metaData = $this->em->getClassMetadata(get_class($entity));
132
            foreach ($metaData->getAssociationMappings() as $association) {
133
                $targetClass = $association['targetEntity'];
134
135
                //Skip already set WidgetMap association
136
                if ($targetClass == WidgetMap::class) {
137
                    continue;
138
                }
139
140
                //If Widget has OneToOne or ManyToOne association, store target entity id to construct
141
                //a single query for this entity type
142
                if ($metaData->isSingleValuedAssociation($association['fieldName'])
143
                    && !$widgetCached
144
                ) {
145
                    //If target Entity is not null, treat it
146
                    if ($targetEntity = $this->accessor->getValue($entity, $association['fieldName'])) {
147
                        $associatedEntities[$targetClass]['id'][] = new AssociatedEntityToWarm(
148
                            AssociatedEntityToWarm::TYPE_MANY_TO_ONE,
149
                            $entity,
150
                            $association['fieldName'],
151
                            $targetEntity->getId()
152
                        );
153
                    }
154
                }
155
156
                //If Widget has OneToMany association, store owner entity id and mappedBy value
157
                //to construct a single query for this entity type
158
                elseif ($metaData->isCollectionValuedAssociation($association['fieldName'])) {
159
160
                    //Even if Widget is cached, we need its Criterias used before cache call
161
                    if (!$widgetCached || $targetClass == Criteria::class) {
162
163
                        //If Collection is not null, treat it
164
                        if ($this->accessor->getValue($entity, $association['fieldName'])) {
165
166
                            //Don't use Collection getter directly and override Collection
167
                            //default behaviour to avoid useless query
168
                            $getter = 'get'.ucwords($association['fieldName']);
169
                            $entity->$getter()->setDirty(false);
170
                            $entity->$getter()->setInitialized(true);
171
172
                            $associatedEntities[$targetClass][$association['mappedBy']][] = new AssociatedEntityToWarm(
173
                                AssociatedEntityToWarm::TYPE_ONE_TO_MANY,
174
                                $entity,
175
                                $association['fieldName'],
176
                                $entity->getId()
177
                            );
178
                        }
179
                    }
180
                }
181
            }
182
            if ($entity instanceof Widget && $proxy = $entity->getEntityProxy()) {
183
                $entity->setEntity($this->businessEntityResolver->getBusinessEntity($proxy));
184
            }
185
        }
186
187
        $newEntities = $this->setAssociatedEntities($associatedEntities);
188
        $this->setPagesForLinks($linkIds);
189
190
        //Recursive call if previous has return new entities to warm
191
        if ($newEntities) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newEntities of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
192
            $this->extractAssociatedEntities($newEntities);
193
        }
194
    }
195
196
    /**
197
     * Set all missing associated entities.
198
     *
199
     * @param array $repositories
200
     *
201
     * @throws \Throwable
202
     * @throws \TypeError
203
     *
204
     * @return array
205
     */
206
    private function setAssociatedEntities(array $repositories)
207
    {
208
        $newEntities = [];
209
210
        foreach ($repositories as $repositoryName => $findMethods) {
211
            foreach ($findMethods as $findMethod => $associatedEntitiesToWarm) {
212
213
                //Extract ids to search
214
                $idsToSearch = array_map(function ($associatedEntityToWarm) {
215
                    return $associatedEntityToWarm->getEntityId();
216
                }, $associatedEntitiesToWarm);
217
218
                //Find by id for ManyToOne associations based on target entity id
219
                //Find by mappedBy value for OneToMany associations based on owner entity id
220
                $foundEntities = $this->em->getRepository($repositoryName)->findBy([
221
                    $findMethod => array_values($idsToSearch),
222
                ]);
223
224
                /* @var AssociatedEntityToWarm[] $associatedEntitiesToWarm */
225
                foreach ($associatedEntitiesToWarm as $associatedEntityToWarm) {
226
                    foreach ($foundEntities as $foundEntity) {
227
                        if ($associatedEntityToWarm->getType() == AssociatedEntityToWarm::TYPE_MANY_TO_ONE
228
                            && $foundEntity->getId() == $associatedEntityToWarm->getEntityId()
229
                        ) {
230
                            $inheritorEntity = $associatedEntityToWarm->getInheritorEntity();
231
                            $inheritorPropertyName = $associatedEntityToWarm->getInheritorPropertyName();
232
                            $this->accessor->setValue($inheritorEntity, $inheritorPropertyName, $foundEntity);
233
                            continue;
234
                        } elseif ($associatedEntityToWarm->getType() == AssociatedEntityToWarm::TYPE_ONE_TO_MANY
235
                            && $this->accessor->getValue($foundEntity, $findMethod) == $associatedEntityToWarm->getInheritorEntity()
236
                        ) {
237
                            $inheritorEntity = $associatedEntityToWarm->getInheritorEntity();
238
                            $inheritorPropertyName = $associatedEntityToWarm->getInheritorPropertyName();
239
240
                            //Don't use Collection getter directly and override Collection
241
                            //default behaviour to avoid useless query
242
                            $getter = 'get'.ucwords($inheritorPropertyName);
243
                            $inheritorEntity->$getter()->add($foundEntity);
244
                            $inheritorEntity->$getter()->setDirty(false);
245
                            $inheritorEntity->$getter()->setInitialized(true);
246
247
                            //Store new entities to warm if necessary
248
                            $newEntities[] = $foundEntity;
249
                            continue;
250
                        }
251
                    }
252
                }
253
            }
254
        }
255
256
        return $newEntities;
257
    }
258
259
    /**
260
     * Set viewReferencePage for each link.
261
     *
262
     * @param array $linkIds
263
     */
264
    private function setPagesForLinks(array $linkIds)
265
    {
266
        $viewReferences = [];
267
268
        /* @var Link[] $links */
269
        $links = $this->em->getRepository('VictoireCoreBundle:Link')->findById($linkIds);
270
271
        foreach ($links as $link) {
272
            if ($link->getParameters()['linkType'] == 'viewReference') {
273
                $viewReference = $this->viewReferenceRepository->getOneReferenceByParameters([
274
                    'id'     => $link->getParameters()['viewReference'],
275
                    'locale' => $link->getParameters()['locale'],
276
                ]);
277
278
                if ($viewReference instanceof ViewReference) {
279
                    $viewReferences[$link->getId()] = $viewReference;
280
                }
281
            }
282
        }
283
284
        /* @var Page[] $pages */
285
        $pages = $this->em->getRepository('VictoireCoreBundle:View')->findByViewReferences($viewReferences);
286
287
        foreach ($links as $link) {
288
            foreach ($pages as $page) {
289
                if (!($page instanceof BusinessTemplate) && $page->getReference() && $link->getViewReference() == $page->getReference()->getId()) {
290
                    $link->setViewReferencePage($page);
291
                }
292
            }
293
        }
294
    }
295
296
    /**
297
     * Check if reflection class has LinkTrait.
298
     *
299
     * @param \ReflectionClass $reflect
300
     *
301
     * @return bool
302
     */
303
    private function hasLinkTrait(\ReflectionClass $reflect)
304
    {
305
        $traits = $reflect->getTraits();
306
        foreach ($traits as $trait) {
307
            if ($trait->getName() == LinkTrait::class) {
0 ignored issues
show
Bug introduced by
Consider using $trait->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
308
                return true;
309
            }
310
        }
311
312
        if ($parentClass = $reflect->getParentClass()) {
313
            if ($this->hasLinkTrait($parentClass)) {
314
                return true;
315
            }
316
        }
317
318
        return false;
319
    }
320
}
321