diff --git a/Bundle/BusinessPageBundle/Controller/BusinessTemplateController.php b/Bundle/BusinessPageBundle/Controller/BusinessTemplateController.php index a2034ca8b..080c8f482 100644 --- a/Bundle/BusinessPageBundle/Controller/BusinessTemplateController.php +++ b/Bundle/BusinessPageBundle/Controller/BusinessTemplateController.php @@ -56,11 +56,11 @@ public function indexAction() } return new JsonResponse([ - 'html' => $this->container->get('templating')->render( + 'html' => $this->container->get('templating')->render( 'VictoireBusinessPageBundle:BusinessEntity:index.html.twig', [ - 'businessEntities' => $businessEntities, - 'BusinessTemplates' => $BusinessTemplates, + 'businessEntities' => $businessEntities, + 'BusinessTemplates' => $BusinessTemplates, ] ), 'success' => true, @@ -188,8 +188,8 @@ public function newAction($id) $form = $this->createCreateForm($view); $parameters = [ - 'entity' => $view, - 'form' => $form->createView(), + 'entity' => $view, + 'form' => $form->createView(), ]; return new JsonResponse([ @@ -221,9 +221,9 @@ public function editAction(View $view) $deleteForm = $this->createDeleteForm($view->getId()); $parameters = [ - 'entity' => $view, - 'form' => $editForm->createView(), - 'delete_form' => $deleteForm->createView(), + 'entity' => $view, + 'form' => $editForm->createView(), + 'delete_form' => $deleteForm->createView(), ]; return new JsonResponse([ @@ -373,8 +373,8 @@ public function listEntitiesAction(BusinessTemplate $view) //parameters for the view return [ - 'BusinessTemplate' => $view, - 'items' => $bepHelper->getEntitiesAllowed($view, $this->get('doctrine.orm.entity_manager')), + 'BusinessTemplate' => $view, + 'items' => $bepHelper->getEntitiesAllowed($view, $this->get('doctrine.orm.entity_manager')), ]; } diff --git a/Bundle/CoreBundle/Entity/View.php b/Bundle/CoreBundle/Entity/View.php index 8591d7475..e6a194539 100644 --- a/Bundle/CoreBundle/Entity/View.php +++ b/Bundle/CoreBundle/Entity/View.php @@ -3,7 +3,6 @@ namespace Victoire\Bundle\CoreBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; use Knp\DoctrineBehaviors\Model\Translatable\Translatable; @@ -167,6 +166,11 @@ abstract class View */ protected $roles; + /** + * @var array + */ + protected $builtWidgetMap; + /** * Construct. **/ @@ -224,7 +228,7 @@ public function setTemplate($template) /** * Get template. * - * @return string + * @return View */ public function getTemplate() { @@ -521,9 +525,9 @@ public function setWidgetMaps($widgetMaps) } /** - * Get widgets. + * Get WidgetMaps. * - * @return Collection[WidgetMap] + * @return WidgetMap[] */ public function getWidgetMaps() { @@ -531,9 +535,9 @@ public function getWidgetMaps() } /** - * Add widget. + * Add WidgetMap. * - * @param Widget $widgetMap + * @param WidgetMap $widgetMap */ public function addWidgetMap(WidgetMap $widgetMap) { @@ -554,22 +558,24 @@ public function removeWidgetMap(WidgetMap $widgetMap) } /** - * Get widgets ids as array. + * Get WidgetMaps for View and its Templates. * - * @return array + * @return WidgetMap[] */ - public function getWidgetsIds() + public function getWidgetMapsForViewAndTemplates() { - $widgetIds = []; - foreach ($this->getBuiltWidgetMap() as $slot => $_widgetMaps) { - foreach ($_widgetMaps as $widgetMap) { - foreach ($widgetMap->getWidgets() as $widget) { - $widgetIds[] = $widget->getId(); - } - } + $widgetMaps = []; + + foreach ($this->getWidgetMaps() as $_widgetMap) { + $widgetMaps[] = $_widgetMap; + } + + if ($template = $this->getTemplate()) { + $templateWidgetMaps = $template->getWidgetMapsForViewAndTemplates(); + $widgetMaps = array_merge($widgetMaps, $templateWidgetMaps); } - return $widgetIds; + return $widgetMaps; } /** @@ -585,7 +591,7 @@ public function getBuiltWidgetMap() /** * Set builtWidgetMap. * - * @param string $builtWidgetMap + * @param array $builtWidgetMap * * @return $this */ @@ -717,7 +723,7 @@ public function changeCssHash() /** * @deprecated - * Get widgetMap. + * Get widgetMap * * @return widgetMap */ @@ -728,7 +734,7 @@ public function getWidgetMap() /** * @deprecated - * Get widgets. + * Get widgets * * @return string */ @@ -737,6 +743,11 @@ public function getWidgets() return $this->widgets; } + /** + * @param View $view + * + * @return bool + */ public function isTemplateOf(View $view) { while ($_view = $view->getTemplate()) { diff --git a/Bundle/CoreBundle/EventSubscriber/WidgetSubscriber.php b/Bundle/CoreBundle/EventSubscriber/WidgetSubscriber.php index 5a8a355f1..371a447f6 100644 --- a/Bundle/CoreBundle/EventSubscriber/WidgetSubscriber.php +++ b/Bundle/CoreBundle/EventSubscriber/WidgetSubscriber.php @@ -116,7 +116,7 @@ public function updateViewCss(View $view) $view->changeCssHash(); //Update css file - $this->widgetMapBuilder->build($view, $this->em, true); + $this->widgetMapBuilder->build($view, true); $widgets = $this->widgetRepo->findAllWidgetsForView($view); //Generate CSS file and set View's CSS as up to date diff --git a/Bundle/PageBundle/Helper/PageHelper.php b/Bundle/PageBundle/Helper/PageHelper.php index 81ff1a3e1..640b25aba 100644 --- a/Bundle/PageBundle/Helper/PageHelper.php +++ b/Bundle/PageBundle/Helper/PageHelper.php @@ -197,7 +197,7 @@ public function renderPage($view, $layout = null) $this->eventDispatcher->dispatch('victoire.on_render_page', $pageRenderEvent); //Build WidgetMap - $this->widgetMapBuilder->build($view, $this->entityManager, true); + $this->widgetMapBuilder->build($view, true); //Populate widgets with their data $this->widgetDataWarmer->warm($this->entityManager, $view); @@ -253,7 +253,7 @@ public function updatePageWithEntity(BusinessTemplate $page, $entity) * @param BusinessPageReference $viewReference * * @return BusinessPage - * read the cache to find entity according tu given url. + * read the cache to find entity according tu given url * @return object|null */ protected function findEntityByReference(ViewReference $viewReference) @@ -276,13 +276,13 @@ public function findPageByReference($viewReference) if ($viewReference->getViewId()) { //BusinessPage $page = $this->entityManager->getRepository('VictoireCoreBundle:View') ->findOneBy([ - 'id' => $viewReference->getViewId(), + 'id' => $viewReference->getViewId(), ]); $page->setCurrentLocale($viewReference->getLocale()); } else { //VirtualBusinessPage $page = $this->entityManager->getRepository('VictoireCoreBundle:View') ->findOneBy([ - 'id' => $viewReference->getTemplateId(), + 'id' => $viewReference->getTemplateId(), ]); if ($entity = $this->findEntityByReference($viewReference)) { if ($page instanceof BusinessTemplate) { @@ -299,7 +299,7 @@ public function findPageByReference($viewReference) } elseif ($viewReference instanceof ViewReference) { $page = $this->entityManager->getRepository('VictoireCoreBundle:View') ->findOneBy([ - 'id' => $viewReference->getViewId(), + 'id' => $viewReference->getViewId(), ]); $page->setCurrentLocale($viewReference->getLocale()); } else { diff --git a/Bundle/WidgetBundle/Entity/Widget.php b/Bundle/WidgetBundle/Entity/Widget.php index 767a03644..04318eaae 100644 --- a/Bundle/WidgetBundle/Entity/Widget.php +++ b/Bundle/WidgetBundle/Entity/Widget.php @@ -110,6 +110,7 @@ public function __construct() * @ORM\JoinColumn(name="view_id", referencedColumnName="id", onDelete="CASCADE") */ protected $view; + /** * @var WidgetMap * @@ -442,7 +443,7 @@ public function __clone() /** * @deprecated - * Get view. + * Get view * * @return string */ diff --git a/Bundle/WidgetBundle/Renderer/WidgetRenderer.php b/Bundle/WidgetBundle/Renderer/WidgetRenderer.php index 83feadcd6..2b9c35eeb 100644 --- a/Bundle/WidgetBundle/Renderer/WidgetRenderer.php +++ b/Bundle/WidgetBundle/Renderer/WidgetRenderer.php @@ -86,13 +86,16 @@ public function render(Widget $widget, View $view) $templating = $this->container->get('templating'); //the content of the widget + $parameters = $this->container->get('victoire_widget.widget_content_resolver')->getWidgetContent($widget); /* * In some cases, for example, WidgetRender in BusinessEntity mode with magic variables {{entity.id}} transformed * into the real business entity id, then if in the rendered action, we need to flush, it would persist the * modified widget which really uncomfortable ;) */ - $this->container->get('doctrine.orm.entity_manager')->refresh($widget); + if ($widget->getMode() == Widget::MODE_BUSINESS_ENTITY) { + $this->container->get('doctrine.orm.entity_manager')->refresh($widget); + } //the template displayed is in the widget bundle (with the potential theme) $showView = 'show'.ucfirst($widget->getTheme()); diff --git a/Bundle/WidgetBundle/Repository/WidgetRepository.php b/Bundle/WidgetBundle/Repository/WidgetRepository.php index 900797666..db3a31351 100644 --- a/Bundle/WidgetBundle/Repository/WidgetRepository.php +++ b/Bundle/WidgetBundle/Repository/WidgetRepository.php @@ -3,50 +3,49 @@ namespace Victoire\Bundle\WidgetBundle\Repository; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; use Victoire\Bundle\CoreBundle\Entity\View; +use Victoire\Bundle\WidgetBundle\Entity\Widget; /** - * The widget Repository. + * The Widget Repository. */ class WidgetRepository extends EntityRepository { /** - * Get all the widget within a list of ids. + * Get all Widgets for a given View. * - * @param array $widgetIds - * - * @return \Doctrine\ORM\QueryBuilder - */ - public function getAllIn(array $widgetIds) - { - return $this->createQueryBuilder('widget') - ->where('widget.id IN (:map)') - ->setParameter('map', $widgetIds); - } - - /** - * Find all the widgets in a list of ids. - * - * @param array $widgetIds + * @param View $view * - * @return multitype: + * @return QueryBuilder */ - public function findAllIn(array $widgetIds) + public function getAllForView(View $view) { - $qb = $this->getAllIn($widgetIds); + //Get all WidgetMaps ids for this View + $widgetMapsIdsToSearch = []; + foreach ($view->getBuiltWidgetMap() as $widgetMaps) { + foreach ($widgetMaps as $widgetMap) { + $widgetMapsIdsToSearch[] = $widgetMap->getId(); + } + } - return $qb->getQuery()->getResult(); + return $this->createQueryBuilder('widget') + ->join('widget.widgetMap', 'widgetMap') + ->andWhere('widgetMap.id IN (:widgetMapsIds)') + ->setParameter('widgetMapsIds', $widgetMapsIdsToSearch); } /** - * Find all widgets for a given View. + * Find all Widgets for a given View. * * @param View $view * - * @return multitype + * @return Widget[] */ public function findAllWidgetsForView(View $view) { - return $this->findAllIn($view->getWidgetsIds()); + $qb = $this->getAllForView($view); + + return $qb->getQuery()->getResult(); } } diff --git a/Bundle/WidgetBundle/Resolver/WidgetResolver.php b/Bundle/WidgetBundle/Resolver/WidgetResolver.php index 8468fdf0e..4789cdf3c 100644 --- a/Bundle/WidgetBundle/Resolver/WidgetResolver.php +++ b/Bundle/WidgetBundle/Resolver/WidgetResolver.php @@ -47,14 +47,15 @@ public function __construct(DataSourceChain $dataSourceChain, AuthorizationCheck public function resolve(WidgetMap $widgetMap) { //TODO: orderize it - $widgets = $widgetMap->getWidgets(); // if the widgetmap is linked to no widgets, it seems that it is an overwrite of the position so keep the replaced widgets for display + if ($widgetMap->getReplaced() && count($widgets) === 0) { $widgets = $widgetMap->getReplaced()->getWidgets(); } /* @var Widget $widget */ foreach ($widgets as $_widget) { + /** @var Criteria $criteria */ foreach ($_widget->getCriterias() as $criteria) { $value = $this->dataSourceChain->getData($criteria->getName()); diff --git a/Bundle/WidgetMapBundle/Builder/WidgetMapBuilder.php b/Bundle/WidgetMapBundle/Builder/WidgetMapBuilder.php index 28b1c4d2d..498e0dfd7 100644 --- a/Bundle/WidgetMapBundle/Builder/WidgetMapBuilder.php +++ b/Bundle/WidgetMapBundle/Builder/WidgetMapBuilder.php @@ -2,9 +2,9 @@ namespace Victoire\Bundle\WidgetMapBundle\Builder; -use Doctrine\ORM\EntityManager; use Victoire\Bundle\CoreBundle\Entity\View; use Victoire\Bundle\WidgetMapBundle\Entity\WidgetMap; +use Victoire\Bundle\WidgetMapBundle\Warmer\ContextualViewWarmer; /** * View WidgetMap builder. @@ -13,48 +13,48 @@ */ class WidgetMapBuilder { + private $contextualViewWarmer; + + /** + * WidgetMapBuilder constructor. + * + * @param ContextualViewWarmer $contextualViewWarmer + */ + public function __construct(ContextualViewWarmer $contextualViewWarmer) + { + $this->contextualViewWarmer = $contextualViewWarmer; + } + /** * This method build widgetmaps relativly to given view and it's templates. * - * @param View $view - * @param EntityManager $em - * @param bool $updatePage + * @param View $view + * @param bool $updatePage * * @return array */ - public function build(View $view, EntityManager $em = null, $updatePage = true) + public function build(View $view, $updatePage = true) { - $widgetMaps = []; - // populate a $widgetmaps array with widgetmaps of given view + widgetmaps of it's templates - if ($view->getWidgetMaps()) { - $widgetMaps = $view->getWidgetMaps()->toArray(); - } - $template = clone $view; $builtWidgetMap = []; - while (null !== $template = $template->getTemplate()) { - if ($template->getWidgetMaps()) { - foreach ($template->getWidgetMaps()->toArray() as $item) { - $widgetMaps[] = $item; - } - } - } + $widgetMaps = $this->contextualViewWarmer->warm($view); $slots = $this->removeOverwritedWidgetMaps($widgetMaps); $this->removeDeletedWidgetMaps($slots); - foreach ($slots as $slot => $widgetMaps) { + foreach ($slots as $slot => $slotWidgetMaps) { $mainWidgetMap = null; $builtWidgetMap[$slot] = []; - $rootWidgetMap = $this->findRootWidgetMap($widgetMaps); + $rootWidgetMap = $this->findRootWidgetMap($slotWidgetMaps); if ($rootWidgetMap) { $builtWidgetMap[$slot][] = $rootWidgetMap; - $builtWidgetMap = $this->orderizeWidgetMap($rootWidgetMap, $builtWidgetMap, $slot, $widgetMaps, $view); + $builtWidgetMap = $this->orderizeWidgetMap($rootWidgetMap, $builtWidgetMap, $slot, $slotWidgetMaps, $view); } } + if ($updatePage) { $view->setBuiltWidgetMap($builtWidgetMap); } @@ -99,25 +99,31 @@ public function getAvailablePosition(View $view) return $availablePositions; } - /* + /** * Get the children of given WidgetMap and place them recursively in the "builtWidgetMap" array at the right place - * depending of the children parents and positions + * depending of the children parents and positions. + * * @param WidgetMap $currentWidgetMap + * @param $builtWidgetMap + * @param $slot + * @param $slotWidgetMaps + * @param View $view + * + * @return mixed */ - protected function orderizeWidgetMap($currentWidgetMap, $builtWidgetMap, $slot, $widgetMaps, $view) + protected function orderizeWidgetMap(WidgetMap $currentWidgetMap, $builtWidgetMap, $slot, $slotWidgetMaps, View $view) { $children = $currentWidgetMap->getChildren($view); foreach ($children as $child) { // check if the founded child belongs to the view - if (in_array($child, $widgetMaps, true) - ) { + if (in_array($child, $slotWidgetMaps, true)) { // Find the position of the "currentWidgetMap" inside the builtWidgetMap, // add "1" to this position if wanted position is "after", 0 is it's before. $offset = array_search($currentWidgetMap, $builtWidgetMap[$slot]) + ($child->getPosition() == WidgetMap::POSITION_AFTER ? 1 : 0); // insert the child in builtWidgetMap at offset position array_splice($builtWidgetMap[$slot], $offset, 0, [$child]); // call myself with child - $builtWidgetMap = $this->orderizeWidgetMap($child, $builtWidgetMap, $slot, $widgetMaps, $view); + $builtWidgetMap = $this->orderizeWidgetMap($child, $builtWidgetMap, $slot, $slotWidgetMaps, $view); } } @@ -173,7 +179,9 @@ protected function removeDeletedWidgetMaps(&$slots) /** * Find the "root" widgetmap (the one that has no parent). * - * @param WidgetMap[] $widgetMaps + * @param $widgetMaps + * + * @return WidgetMap|null */ private function findRootWidgetMap($widgetMaps) { diff --git a/Bundle/WidgetMapBundle/Entity/WidgetMap.php b/Bundle/WidgetMapBundle/Entity/WidgetMap.php index 86d3a1592..84fe20733 100644 --- a/Bundle/WidgetMapBundle/Entity/WidgetMap.php +++ b/Bundle/WidgetMapBundle/Entity/WidgetMap.php @@ -45,6 +45,16 @@ class WidgetMap */ protected $view; + /** + * A WidgetMap has a View but also a contextualView (not persisted). + * This contextualView is set when WidgetMap is build. + * When getChilds and getSubstitutes are called, we use this contextualView to retrieve + * concerned WidgetMaps in order to avoid useless Doctrine queries. + * + * @var View + */ + protected $contextualView; + /** * @var [Widget] * @@ -218,7 +228,31 @@ public function setView(View $view) } /** - * @return mixed + * Get the current View context. + * + * @return View + */ + public function getContextualView() + { + return $this->contextualView; + } + + /** + * Store the current View context. + * + * @param View $contextualView + * + * @return $this + */ + public function setContextualView(View $contextualView) + { + $this->contextualView = $contextualView; + + return $this; + } + + /** + * @return WidgetMap */ public function getReplaced() { @@ -226,7 +260,7 @@ public function getReplaced() } /** - * @param mixed $replaced + * @param WidgetMap $replaced */ public function setReplaced($replaced) { @@ -253,56 +287,35 @@ public function setSlot($slot) } /** + * Return "after" and "before" children, + * based on contextual View and its Templates. + * * @return mixed */ public function getChildren(View $view = null) { $positions = [self::POSITION_BEFORE, self::POSITION_AFTER]; $children = []; - $widgetMap = $this; foreach ($positions as $position) { + + //Position is null by default $children[$position] = null; - if (($childs = $widgetMap->getChilds($position)) && !empty($childs)) { - foreach ($childs as $_child) { - // found child must belongs to the given view or one of it's templates - if ($view) { - // if child has a view - // and child view is same as given view or the child view is a template of given view - if ($_child->getView() && ($view === $_child->getView() || $_child->getView()->isTemplateOf($view)) - ) { - // if child is a substitute in view - if ($substitute = $_child->getSubstituteForView($view)) { - // if i'm not the parent of the substitute or i does not have the same position, child is not valid - if ($substitute->getParent() !== $this || $substitute->getPosition() !== $position) { - $_child = null; - } - } - $children[$position] = $_child; - } - } else { - $children[$position] = $_child; - } + + //Pass through all current WidgetMap children for a given position + foreach ($this->getChilds($position) as $_child) { + //If child don't have a substitute for this View and Templates, this is the one + if (null === $_child->getSubstituteForView($view)) { + $children[$position] = $_child; } } - // If I am replaced and my replacement has children for the position - if (!$children[$position] - && ($replaced = $this->getReplaced()) - && !empty($this->getReplaced()->getChilds($position))) { + + //If children has not been found for this position + //and current WidgetMap is a substitute + if (!$children[$position] && $this->getReplaced()) { + //Pass through all replaced WidgetMap children for a given position foreach ($this->getReplaced()->getChilds($position) as $_child) { - if ($view) { - // if child view is same as given view or the child view is a template of given view - if ($_child->getView() && ($view === $_child->getView() || $_child->getView()->isTemplateOf($view))) { - - // if child is a substitute in view - if ($substitute = $_child->getSubstituteForView($view)) { - // if i'm not the parent of the substitute or i does not have the same position, child is not valid - if ($substitute->getParent() != $this || $substitute->getPosition() != $position) { - $_child = null; - } - } - $children[$position] = $_child; - } - } else { + //If child don't have a substitute for this View and Templates, this is the one + if (null === $_child->getSubstituteForView($view)) { $children[$position] = $_child; } } @@ -320,6 +333,12 @@ public function getChildrenRaw() return $this->children; } + /** + * @param $position + * @param View|null $view + * + * @return bool + */ public function hasChild($position, View $view = null) { foreach ($this->getChildren($view) as $child) { @@ -347,18 +366,22 @@ public function getChild($position) } /** - * @return [WidgetMap] + * Return all children from contextual View (already loaded WidgetMaps). + * + * @return WidgetMap[] */ public function getChilds($position) { - $childs = []; - foreach ($this->children as $_child) { - if ($_child && $_child->getPosition() == $position) { - $childs[] = $_child; + $childsWidgetMaps = []; + $viewWidgetMaps = $this->getContextualView()->getWidgetMapsForViewAndTemplates(); + + foreach ($viewWidgetMaps as $viewWidgetMap) { + if ($viewWidgetMap->getParent() == $this && $viewWidgetMap->getPosition() == $position) { + $childsWidgetMaps[] = $viewWidgetMap; } } - return $childs; + return $childsWidgetMaps; } /** @@ -369,9 +392,6 @@ public function setChildren($children) $this->children = $children; } - /** - * @return void - */ public function removeChildren() { foreach ($this->children as $child) { @@ -434,25 +454,49 @@ public function setPosition($position) } /** - * @return ArrayCollection + * Return all substitutes from contextual View (already loaded WidgetMaps) + * Ideally must return only one WidgetMap per View. + * + * @return WidgetMap[] */ public function getSubstitutes() { - return $this->substitutes; + $substitutesWidgetMaps = []; + $viewWidgetMaps = $this->getContextualView()->getWidgetMapsForViewAndTemplates(); + + foreach ($viewWidgetMaps as $viewWidgetMap) { + if ($viewWidgetMap->getReplaced() == $this) { + $substitutesWidgetMaps[] = $viewWidgetMap; + } + } + + return $substitutesWidgetMaps; } /** - * @return mixed + * Return substitute if used in View. + * + * @return WidgetMap|null */ public function getSubstituteForView(View $view) { - foreach ($this->substitutes as $substitute) { + foreach ($this->getSubstitutes() as $substitute) { if ($substitute->getView() === $view) { return $substitute; } } } + /** + * Return all Substitutes (not based on contextual View). + * + * @return ArrayCollection + */ + public function getAllSubstitutes() + { + return $this->substitutes; + } + /** * @param WidgetMap $substitute */ diff --git a/Bundle/WidgetMapBundle/Manager/WidgetMapManager.php b/Bundle/WidgetMapBundle/Manager/WidgetMapManager.php index 38d636ddf..cca1c3a40 100644 --- a/Bundle/WidgetMapBundle/Manager/WidgetMapManager.php +++ b/Bundle/WidgetMapBundle/Manager/WidgetMapManager.php @@ -63,8 +63,6 @@ public function insert(Widget $widget, View $view, $slotId, $position, $widgetRe * * @param View $view * @param array $sortedWidget - * - * @return void */ public function move(View $view, $sortedWidget) { @@ -127,8 +125,8 @@ public function delete(View $view, Widget $widget) //we remove the widget from the current view if ($widgetMap->getView() === $view) { // If the widgetMap has substitutes, delete them or transform them in create mode - if (count($widgetMap->getSubstitutes()) > 0) { - foreach ($widgetMap->getSubstitutes() as $substitute) { + if (count($widgetMap->getAllSubstitutes()) > 0) { + foreach ($widgetMap->getAllSubstitutes() as $substitute) { if ($substitute->getAction() === WidgetMap::ACTION_OVERWRITE) { $substitute->setAction(WidgetMap::ACTION_CREATE); $substitute->setReplaced(null); diff --git a/Bundle/WidgetMapBundle/Resources/config/services.yml b/Bundle/WidgetMapBundle/Resources/config/services.yml index f5de1fecd..b45b07457 100644 --- a/Bundle/WidgetMapBundle/Resources/config/services.yml +++ b/Bundle/WidgetMapBundle/Resources/config/services.yml @@ -1,17 +1,20 @@ services: victoire_widget_map.builder: class: Victoire\Bundle\WidgetMapBundle\Builder\WidgetMapBuilder + arguments: + - "@victoire_widget_map.contextual_view_warmer" victoire_widget_map.widget_data_warmer: class: Victoire\Bundle\WidgetMapBundle\Warmer\WidgetDataWarmer arguments: - - "@annotation_reader" - "@victoire_view_reference.repository" - - ["\Victoire\Bundle\MediaBundle\Entity\Media"] + - "@victoire_widget.widget_helper" + + victoire_widget_map.contextual_view_warmer: + class: Victoire\Bundle\WidgetMapBundle\Warmer\ContextualViewWarmer victoire_widget_map.manager: class: Victoire\Bundle\WidgetMapBundle\Manager\WidgetMapManager arguments: - "@doctrine.orm.entity_manager" - - "@victoire_widget_map.builder" - + - "@victoire_widget_map.builder" \ No newline at end of file diff --git a/Bundle/WidgetMapBundle/Warmer/AssociatedEntityToWarm.php b/Bundle/WidgetMapBundle/Warmer/AssociatedEntityToWarm.php index 53dc7dd22..6a36c287b 100644 --- a/Bundle/WidgetMapBundle/Warmer/AssociatedEntityToWarm.php +++ b/Bundle/WidgetMapBundle/Warmer/AssociatedEntityToWarm.php @@ -4,22 +4,53 @@ class AssociatedEntityToWarm { + const TYPE_MANY_TO_ONE = 'many_to_one'; + const TYPE_ONE_TO_MANY = 'one_to_many'; + + protected $type; protected $inheritorEntity; protected $inheritorPropertyName; protected $entityId; + protected $mappedBy; /** * Constructor. * + * @param null $type * @param null $inheritorEntity * @param null $inheritorPropertyName - * @param null $entityId for ManyToOne type + * @param null $entityId + * @param null $mappedBy for OneToMany type only */ - public function __construct($inheritorEntity = null, $inheritorPropertyName = null, $entityId = null) + public function __construct( + $type = null, + $inheritorEntity = null, + $inheritorPropertyName = null, + $entityId = null, + $mappedBy = null) { + $this->type = $type; $this->inheritorEntity = $inheritorEntity; $this->inheritorPropertyName = $inheritorPropertyName; $this->entityId = $entityId; + $this->mappedBy = $mappedBy; + } + + public function getType() + { + return $this->type; + } + + /** + * @param null $type + * + * @return $this + */ + public function setType($type) + { + $this->type = $type; + + return $this; } /** @@ -83,4 +114,21 @@ public function setEntityId($entityId) return $this; } + + public function getMappedBy() + { + return $this->mappedBy; + } + + /** + * @param null $mappedBy + * + * @return $this + */ + public function setMappedBy($mappedBy) + { + $this->mappedBy = $mappedBy; + + return $this; + } } diff --git a/Bundle/WidgetMapBundle/Warmer/ContextualViewWarmer.php b/Bundle/WidgetMapBundle/Warmer/ContextualViewWarmer.php new file mode 100644 index 000000000..742a51e54 --- /dev/null +++ b/Bundle/WidgetMapBundle/Warmer/ContextualViewWarmer.php @@ -0,0 +1,43 @@ +getWidgetMaps() as $_widgetMap) { + $_widgetMap->setContextualView($contextualView); + $widgetMaps[] = $_widgetMap; + } + + if ($template = $viewToWarm->getTemplate()) { + $templateWidgetMaps = $this->warm($template, $contextualView); + $widgetMaps = array_merge($widgetMaps, $templateWidgetMaps); + } + + return $widgetMaps; + } +} diff --git a/Bundle/WidgetMapBundle/Warmer/WidgetDataWarmer.php b/Bundle/WidgetMapBundle/Warmer/WidgetDataWarmer.php index bc3ce8ce2..f0a238e38 100644 --- a/Bundle/WidgetMapBundle/Warmer/WidgetDataWarmer.php +++ b/Bundle/WidgetMapBundle/Warmer/WidgetDataWarmer.php @@ -2,55 +2,55 @@ namespace Victoire\Bundle\WidgetMapBundle\Warmer; -use Doctrine\Common\Annotations\Reader; use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Mapping\ManyToOne; -use Doctrine\ORM\Mapping\OneToMany; -use Doctrine\ORM\PersistentCollection; use Symfony\Component\PropertyAccess\PropertyAccess; use Victoire\Bundle\BusinessPageBundle\Entity\BusinessTemplate; use Victoire\Bundle\CoreBundle\Entity\Link; use Victoire\Bundle\CoreBundle\Entity\View; +use Victoire\Bundle\CriteriaBundle\Entity\Criteria; use Victoire\Bundle\PageBundle\Entity\Page; use Victoire\Bundle\ViewReferenceBundle\Connector\ViewReferenceRepository; use Victoire\Bundle\ViewReferenceBundle\ViewReference\ViewReference; use Victoire\Bundle\WidgetBundle\Entity\Traits\LinkTrait; use Victoire\Bundle\WidgetBundle\Entity\Widget; -use Victoire\Widget\ListingBundle\Entity\WidgetListing; -use Victoire\Widget\ListingBundle\Entity\WidgetListingItem; -use Victoire\Widget\MenuBundle\Entity\WidgetMenu; +use Victoire\Bundle\WidgetBundle\Helper\WidgetHelper; +use Victoire\Bundle\WidgetBundle\Repository\WidgetRepository; /** - * Link Warmer. - * This class prepare all widgets with their links and medias for the current View to avoid queries during page rendering. + * WidgetDataWarmer. * - * ref: victoire_widget_map.widget_data_warmer + * This class prepare all widgets with their associated entities for the current View + * to reduce queries during page rendering. + * Only OneToMany and ManyToOne associations are handled because no OneToOne or ManyToMany + * associations have been used in Widgets. + * + * Ref: victoire_widget_map.widget_data_warmer */ class WidgetDataWarmer { - protected $reader; - protected $viewReferenceRepository; + /* @var $em EntityManager */ protected $em; + protected $viewReferenceRepository; + protected $widgetHelper; protected $accessor; - protected $manyToOneAssociations; /** * Constructor. * - * @param Reader $reader * @param ViewReferenceRepository $viewReferenceRepository - * @param array $manyToOneAssociations + * @param WidgetHelper $widgetHelper */ - public function __construct(Reader $reader, ViewReferenceRepository $viewReferenceRepository, array $manyToOneAssociations) - { - $this->reader = $reader; + public function __construct( + ViewReferenceRepository $viewReferenceRepository, + WidgetHelper $widgetHelper + ) { $this->viewReferenceRepository = $viewReferenceRepository; + $this->widgetHelper = $widgetHelper; $this->accessor = PropertyAccess::createPropertyAccessor(); - $this->manyToOneAssociations = $manyToOneAssociations; } /** - * Warm widgets, links and medias. + * Find all Widgets for current View, inject them in WidgetMap and warm associated entities. * * @param EntityManager $em * @param View $view @@ -59,87 +59,183 @@ public function warm(EntityManager $em, View $view) { $this->em = $em; + /* @var WidgetRepository $widgetRepo */ $widgetRepo = $this->em->getRepository('Victoire\Bundle\WidgetBundle\Entity\Widget'); $viewWidgets = $widgetRepo->findAllWidgetsForView($view); - $linkIds = $associatedEntities = []; - $this->extractAssociatedEntities($viewWidgets, $linkIds, $associatedEntities); - $this->setAssociatedEntities($associatedEntities); - $this->setPagesForLinks($linkIds); + + $this->injectWidgets($view, $viewWidgets); + + $this->extractAssociatedEntities($viewWidgets); } /** - * Pass throw all widgets and ManyToOne relations to extract all missing associations. + * Inject Widgets in View's builtWidgetMap. * - * @param Widget[]|WidgetListingItem[] $entities - * @param array $linkIds - * @param array $associatedEntities + * @param View $view + * @param $viewWidgets */ - private function extractAssociatedEntities(array $entities, &$linkIds, &$associatedEntities) + private function injectWidgets(View $view, $viewWidgets) { + $builtWidgetMap = $view->getBuiltWidgetMap(); + + foreach ($builtWidgetMap as $slot => $widgetMaps) { + foreach ($widgetMaps as $i => $widgetMap) { + foreach ($viewWidgets as $widget) { + if ($widget->getWidgetMap() == $widgetMap) { + $builtWidgetMap[$slot][$i]->addWidget($widget); + + //Override Collection default behaviour to avoid useless query + $builtWidgetMap[$slot][$i]->getWidgets()->setDirty(false); + $builtWidgetMap[$slot][$i]->getWidgets()->setInitialized(true); + continue 2; + } + } + } + } + + $view->setBuiltWidgetMap($builtWidgetMap); + } + + /** + * Pass through all widgets and associated entities to extract all missing associations, + * store it by repository to group queries by entity type. + * + * @param Widget[] $entities Widgets and associated entities + */ + private function extractAssociatedEntities(array $entities) + { + $linkIds = $associatedEntities = []; + foreach ($entities as $entity) { $reflect = new \ReflectionClass($entity); - $properties = $reflect->getProperties(); - //If entity has LinkTrait, store the entity link id - if ($this->hasLinkTrait($reflect) && ($entity instanceof Widget || $entity instanceof WidgetListingItem)) { - /* @var $entity LinkTrait */ - if ($entity->getLink()) { - $linkIds[] = $entity->getLink()->getId(); - } + //If Widget is already in cache, extract only its Criterias (used outside Widget rendering) + $widgetCached = ($entity instanceof Widget && $this->widgetHelper->isCacheEnabled($entity)); + + //If Widget has LinkTrait, store the entity link id + if (!$widgetCached && $this->hasLinkTrait($reflect) && $entity->getLink()) { + $linkIds[] = $entity->getLink()->getId(); } - foreach ($properties as $property) { - $annotations = $this->reader->getPropertyAnnotations($property); - foreach ($annotations as $key => $annotationObj) { + //Pass through all entity associations + $metaData = $this->em->getClassMetadata(get_class($entity)); + foreach ($metaData->getAssociationMappings() as $association) { + $targetClass = $association['targetEntity']; - //If entity has ManyToOne association, store them to construct a single query for each type - if ($annotationObj instanceof ManyToOne && in_array($annotationObj->targetEntity, $this->manyToOneAssociations)) { - if ($targetEntity = $this->accessor->getValue($entity, $property->getName())) { - $associatedEntities[$annotationObj->targetEntity][] = new AssociatedEntityToWarm( - $entity, - $property->getName(), - $targetEntity->getId() - ); - } + //If Widget has OneToOne or ManyToOne association, store target entity id to construct + //a single query for this entity type + if ($metaData->isSingleValuedAssociation($association['fieldName']) + && !$widgetCached + ) { + //If target Entity is not null, treat it + if ($targetEntity = $this->accessor->getValue($entity, $association['fieldName'])) { + $associatedEntities[$targetClass]['id'][] = new AssociatedEntityToWarm( + AssociatedEntityToWarm::TYPE_MANY_TO_ONE, + $entity, + $association['fieldName'], + $targetEntity->getId() + ); } + } + + //If Widget has OneToMany association, store owner entity id and mappedBy value + //to construct a single query for this entity type + elseif ($metaData->isCollectionValuedAssociation($association['fieldName'])) { + + //Even if Widget is cached, we need its Criterias used before cache call + if (!$widgetCached || $association['targetEntity'] == Criteria::class) { - //If current entity instanceof WidgetListing|WidgetListingItem|WidgetMenu and has children entities, pass throw childrens - if (($entity instanceof WidgetListing || $entity instanceof WidgetListingItem || $entity instanceof WidgetMenu) - && ($annotationObj instanceof OneToMany)) { + //If Collection is not null, treat it + if ($this->accessor->getValue($entity, $association['fieldName'])) { - /* @var PersistentCollection $collection */ - if ($collection = $this->accessor->getValue($entity, $property->getName())) { - $this->extractAssociatedEntities($collection->toArray(), $linkIds, $associatedEntities); + //Don't use Collection getter directly and override Collection + //default behaviour to avoid useless query + $getter = 'get'.ucwords($association['fieldName']); + $entity->$getter()->setDirty(false); + $entity->$getter()->setInitialized(true); + + $associatedEntities[$targetClass][$association['mappedBy']][] = new AssociatedEntityToWarm( + AssociatedEntityToWarm::TYPE_ONE_TO_MANY, + $entity, + $association['fieldName'], + $entity->getId() + ); } } } } } + + $newEntities = $this->setAssociatedEntities($associatedEntities); + $this->setPagesForLinks($linkIds); + + //Recursive call if previous has return new entities to warm + if ($newEntities) { + $this->extractAssociatedEntities($newEntities); + } } /** * Set all missing associated entities. * * @param array $repositories + * + * @throws \Throwable + * @throws \TypeError + * + * @return array */ private function setAssociatedEntities(array $repositories) { - foreach ($repositories as $repositoryName => $associatedEntitiesToWarm) { - $idsToSearch = $this->extractAssociatedEntitiesIds($associatedEntitiesToWarm); - $foundEntities = $this->em->getRepository($repositoryName)->findById(array_values($idsToSearch)); - - /* @var AssociatedEntityToWarm[] $associatedEntitiesToWarm */ - foreach ($associatedEntitiesToWarm as $associatedEntityToWarm) { - foreach ($foundEntities as $foundEntitie) { - if ($foundEntitie->getId() == $associatedEntityToWarm->getEntityId()) { - $inheritorEntity = $associatedEntityToWarm->getInheritorEntity(); - $inheritorPropertyName = $associatedEntityToWarm->getInheritorPropertyName(); - $this->accessor->setValue($inheritorEntity, $inheritorPropertyName, $foundEntitie); - break; + $newEntities = []; + + foreach ($repositories as $repositoryName => $findMethods) { + foreach ($findMethods as $findMethod => $associatedEntitiesToWarm) { + + //Extract ids to search + $idsToSearch = array_map(function ($associatedEntityToWarm) { + return $associatedEntityToWarm->getEntityId(); + }, $associatedEntitiesToWarm); + + //Find by id for ManyToOne associations based on target entity id + //Find by mappedBy value for OneToMany associations based on owner entity id + $foundEntities = $this->em->getRepository($repositoryName)->findBy([ + $findMethod => array_values($idsToSearch), + ]); + + /* @var AssociatedEntityToWarm[] $associatedEntitiesToWarm */ + foreach ($associatedEntitiesToWarm as $associatedEntityToWarm) { + foreach ($foundEntities as $foundEntity) { + if ($associatedEntityToWarm->getType() == AssociatedEntityToWarm::TYPE_MANY_TO_ONE + && $foundEntity->getId() == $associatedEntityToWarm->getEntityId() + ) { + $inheritorEntity = $associatedEntityToWarm->getInheritorEntity(); + $inheritorPropertyName = $associatedEntityToWarm->getInheritorPropertyName(); + $this->accessor->setValue($inheritorEntity, $inheritorPropertyName, $foundEntity); + continue; + } elseif ($associatedEntityToWarm->getType() == AssociatedEntityToWarm::TYPE_ONE_TO_MANY + && $this->accessor->getValue($foundEntity, $findMethod) == $associatedEntityToWarm->getInheritorEntity() + ) { + $inheritorEntity = $associatedEntityToWarm->getInheritorEntity(); + $inheritorPropertyName = $associatedEntityToWarm->getInheritorPropertyName(); + + //Don't use Collection getter directly and override Collection + //default behaviour to avoid useless query + $getter = 'get'.ucwords($inheritorPropertyName); + $inheritorEntity->$getter()->add($foundEntity); + $inheritorEntity->$getter()->setDirty(false); + $inheritorEntity->$getter()->setInitialized(true); + + //Store new entities to warm if necessary + $newEntities[] = $foundEntity; + continue; + } } } } } + + return $newEntities; } /** @@ -188,11 +284,9 @@ private function setPagesForLinks(array $linkIds) */ private function hasLinkTrait(\ReflectionClass $reflect) { - $linkTraitName = 'Victoire\Bundle\WidgetBundle\Entity\Traits\LinkTrait'; - $traits = $reflect->getTraits(); foreach ($traits as $trait) { - if ($trait->getName() == $linkTraitName) { + if ($trait->getName() == LinkTrait::class) { return true; } } @@ -205,21 +299,4 @@ private function hasLinkTrait(\ReflectionClass $reflect) return false; } - - /** - * Extract entities ids from an array of AssociatedEntityToWarm. - * - * @param AssociatedEntityToWarm[] $associatedEntitiesToWarm - * - * @return array - */ - private function extractAssociatedEntitiesIds(array $associatedEntitiesToWarm) - { - $extractedIds = []; - foreach ($associatedEntitiesToWarm as $associatedEntityToWarm) { - $extractedIds[] = $associatedEntityToWarm->getEntityId(); - } - - return $extractedIds; - } } diff --git a/Tests/Features/WidgetMap/widgetMapTemplate.feature b/Tests/Features/WidgetMap/widgetMapTemplate.feature index 441660958..f6b62a4e4 100644 --- a/Tests/Features/WidgetMap/widgetMapTemplate.feature +++ b/Tests/Features/WidgetMap/widgetMapTemplate.feature @@ -341,4 +341,4 @@ Scenario: I delete an overwrite widget on template When I am on the homepage Then I should see "Widget 1" Then I should see "Widget 2" - Then I should see "Widget 3 overwrite" + Then I should see "Widget 3 overwrite" \ No newline at end of file diff --git a/UPGRADE-v2.md b/UPGRADE-v2.md index adbccee19..4d76c4831 100644 --- a/UPGRADE-v2.md +++ b/UPGRADE-v2.md @@ -3,9 +3,15 @@ ##UPGRADE 2.0.0 The 2.0 version cleans the widget edit views. -To upgrate to this version, you have to refactor your widgets "edit" and "new" templates to use the "form_static" and "form_entity" blocks. +To upgrade to this version, you have to refactor your widgets "edit" and "new" templates to use the "form_static" and "form_entity" blocks. You don't have to redefine the "picker" div because it's been put outside the block in the parent template. ## UPGRADE 2.2.0 You need to execute the following SQL command to upgrade your database -UPDATE vic_link SET target = '_modal' WHERE target = 'ajax-modal' \ No newline at end of file +UPDATE vic_link SET target = '_modal' WHERE target = 'ajax-modal' + +## UPGRADE 2.2.16 +In order to reduce the number of Doctrine queries by using WidgetDataWarmer, you can execute the following SQL commands. +UPDATE vic_widget_map SET widget_id = NULL; +UPDATE vic_widget SET view_id = NULL; +It simply remove deprecated associations that are not required anymore. \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100644 index 000000000..5de5a2de9 --- /dev/null +++ b/install.sh @@ -0,0 +1,9 @@ +vicsf --env=test doc:sch:upd -f +vicsf --env=test ca:cl +vicsf --env=test ass:ins Tests/Functionnal/web/ +vicsf --env=test baz:js:dum Tests/Functionnal/web/js/ +vicsf --env=test fos:js:dum --target="Tests/Functionnal/web/js/fos_js_routes_test.js" +vicsf --env=test ass:dum +vicsf --env=test server:start 127.0.0.1:8080 -r vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_prod.php +curl http://selenium-release.storage.googleapis.com/2.53/selenium-server-standalone-2.53.1.jar > selenium-server-standalone-2.53.1.jar +nohup java -jar selenium-server-standalone-2.53.1.jar > /dev/null & \ No newline at end of file