WidgetDataWarmer::hasLinkTrait()   B
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 7
nop 1
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'])) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
159
160
                    //Even if Widget is cached, we need its Criterias used before cache call
161
                    if (!$widgetCached || $targetClass === Criteria::class) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
162
163
                        //If Collection is not null, treat it
164
                        if ($this->accessor->getValue($entity, $association['fieldName'])) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
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
        }
183
184
        $newEntities = $this->setAssociatedEntities($associatedEntities);
185
        $this->setPagesForLinks($linkIds);
186
187
        //Recursive call if previous has return new entities to warm
188
        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...
189
            $this->extractAssociatedEntities($newEntities);
190
        }
191
    }
192
193
    /**
194
     * Set all missing associated entities.
195
     *
196
     * @param array $repositories
197
     *
198
     * @throws \Throwable
199
     * @throws \TypeError
200
     *
201
     * @return array
202
     */
203
    private function setAssociatedEntities(array $repositories)
204
    {
205
        $newEntities = [];
206
207
        foreach ($repositories as $repositoryName => $findMethods) {
208
            foreach ($findMethods as $findMethod => $associatedEntitiesToWarm) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
209
210
                //Extract ids to search
211
                $idsToSearch = array_map(function ($associatedEntityToWarm) {
212
                    return $associatedEntityToWarm->getEntityId();
213
                }, $associatedEntitiesToWarm);
214
215
                //Find by id for ManyToOne associations based on target entity id
216
                //Find by mappedBy value for OneToMany associations based on owner entity id
217
                $foundEntities = $this->em->getRepository($repositoryName)->findBy([
218
                    $findMethod => array_values($idsToSearch),
219
                ]);
220
221
                /* @var AssociatedEntityToWarm[] $associatedEntitiesToWarm */
222
                foreach ($associatedEntitiesToWarm as $associatedEntityToWarm) {
223
                    foreach ($foundEntities as $foundEntity) {
224
                        if ($associatedEntityToWarm->getType() === AssociatedEntityToWarm::TYPE_MANY_TO_ONE
225
                            && $foundEntity->getId() === $associatedEntityToWarm->getEntityId()
226
                        ) {
227
                            $inheritorEntity = $associatedEntityToWarm->getInheritorEntity();
228
                            $inheritorPropertyName = $associatedEntityToWarm->getInheritorPropertyName();
229
                            $this->accessor->setValue($inheritorEntity, $inheritorPropertyName, $foundEntity);
230
                            continue;
231
                        } elseif ($associatedEntityToWarm->getType() === AssociatedEntityToWarm::TYPE_ONE_TO_MANY
232
                            && $this->accessor->getValue($foundEntity, $findMethod) === $associatedEntityToWarm->getInheritorEntity()
233
                        ) {
234
                            $inheritorEntity = $associatedEntityToWarm->getInheritorEntity();
235
                            $inheritorPropertyName = $associatedEntityToWarm->getInheritorPropertyName();
236
237
                            //Don't use Collection getter directly and override Collection
238
                            //default behaviour to avoid useless query
239
                            $getter = 'get'.ucwords($inheritorPropertyName);
240
                            $inheritorEntity->$getter()->add($foundEntity);
241
                            $inheritorEntity->$getter()->setDirty(false);
242
                            $inheritorEntity->$getter()->setInitialized(true);
243
244
                            //Store new entities to warm if necessary
245
                            $newEntities[] = $foundEntity;
246
                            continue;
247
                        }
248
                    }
249
                }
250
            }
251
        }
252
253
        return $newEntities;
254
    }
255
256
    /**
257
     * Set viewReferencePage for each link.
258
     *
259
     * @param array $linkIds
260
     */
261
    private function setPagesForLinks(array $linkIds)
262
    {
263
        $viewReferences = [];
264
265
        /* @var Link[] $links */
266
        $links = $this->em->getRepository('VictoireCoreBundle:Link')->findById($linkIds);
267
268
        foreach ($links as $link) {
269
            if ($link->getParameters()['linkType'] == 'viewReference') {
270
                $viewReference = $this->viewReferenceRepository->getOneReferenceByParameters([
271
                    'id'     => $link->getParameters()['viewReference'],
272
                    'locale' => $link->getParameters()['locale'],
273
                ]);
274
275
                if ($viewReference instanceof ViewReference) {
276
                    $viewReferences[$link->getId()] = $viewReference;
277
                }
278
            }
279
        }
280
281
        /* @var Page[] $pages */
282
        $pages = $this->em->getRepository('VictoireCoreBundle:View')->findByViewReferences($viewReferences);
283
284
        foreach ($links as $link) {
285
            foreach ($pages as $page) {
286
                if (!($page instanceof BusinessTemplate) && $page->getReference() && $link->getViewReference() == $page->getReference()->getId()) {
287
                    $link->setViewReferencePage($page);
288
                }
289
            }
290
        }
291
    }
292
293
    /**
294
     * Check if reflection class has LinkTrait.
295
     *
296
     * @param \ReflectionClass $reflect
297
     *
298
     * @return bool
299
     */
300
    private function hasLinkTrait(\ReflectionClass $reflect)
301
    {
302
        $traits = $reflect->getTraits();
303
        foreach ($traits as $trait) {
304
            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...
305
                return true;
306
            }
307
        }
308
309
        if ($parentClass = $reflect->getParentClass()) {
310
            if ($this->hasLinkTrait($parentClass)) {
311
                return true;
312
            }
313
        }
314
315
        return false;
316
    }
317
}
318