diff --git a/Api/Data/NodeInterface.php b/Api/Data/NodeInterface.php index 8f10a24a..f3ca6860 100644 --- a/Api/Data/NodeInterface.php +++ b/Api/Data/NodeInterface.php @@ -26,6 +26,7 @@ interface NodeInterface const ADDITIONAL_DATA = 'additional_data'; const SELECTED_ITEM_ID = 'selected_item_id'; const CUSTOMER_GROUPS = 'customer_groups'; + const HIDE_IF_EMPTY = 'hide_if_empty'; /** * Get node id @@ -316,4 +317,16 @@ public function setCustomerGroups($customerGroups); * @return bool */ public function isVisible($customerGroupId); + + + /** + * @return int + */ + public function getHideIfEmpty(); + + /** + * @param int $hideIfEmpty + * @return $this + */ + public function setHideIfEmpty($hideIfEmpty); } diff --git a/Block/Menu.php b/Block/Menu.php index dd076269..06a72c5d 100644 --- a/Block/Menu.php +++ b/Block/Menu.php @@ -24,6 +24,7 @@ class Menu extends Template implements DataObject\IdentityInterface { const XML_SNOWMENU_GENERAL_CUSTOMER_GROUPS = 'snowmenu/general/customer_groups'; + const XML_SNOWMENU_GENERAL_CACHE_TAGS = 'snowmenu/general/cache_tags'; /** * @var MenuRepositoryInterface @@ -84,6 +85,11 @@ class Menu extends Template implements DataObject\IdentityInterface */ private $httpContext; + /** + * @var array + */ + private $nodeTypeCaches = []; + /** * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -97,6 +103,7 @@ public function __construct( ImageFile $imageFile, Escaper $escaper, Context $httpContext, + array $nodeTypeCaches = [], array $data = [] ) { parent::__construct($context, $data); @@ -110,6 +117,7 @@ public function __construct( $this->setTemplate($this->getMenuTemplate($this->_template)); $this->submenuTemplate = $this->getSubmenuTemplate(); $this->httpContext = $httpContext; + $this->nodeTypeCaches = $nodeTypeCaches; } /** @@ -119,11 +127,22 @@ public function __construct( */ public function getIdentities() { - return [ + $tags = [ \Snowdog\Menu\Model\Menu::CACHE_TAG . '_' . $this->loadMenu()->getId(), Block::CACHE_TAG, \Snowdog\Menu\Model\Menu::CACHE_TAG ]; + if (!$this->canGatherEntityCacheTags()) { + return $tags; + } + $otherCacheTagsArrays = []; + foreach ($this->nodeTypeCaches as $provider) { + $entityCacheTags = $this->nodeTypeProvider->getProvider($provider)->getEntityCacheTags(); + if (!empty($entityCacheTags)) { + $otherCacheTagsArrays[] = $entityCacheTags; + } + } + return array_merge($tags, ...$otherCacheTagsArrays); } protected function getCacheLifetime() @@ -440,6 +459,9 @@ private function getSubmenuBlock($nodes, $parentNode, $level = 0) return $block; } + /** + * @SuppressWarnings(PHPMD.NPathComplexity) + */ private function fetchData() { $nodes = $this->nodeRepository->getByMenu($this->loadMenu()->getId()); @@ -464,16 +486,29 @@ private function fetchData() $result[$level][$parent] = []; } $result[$level][$parent][] = $node; + $idx = array_key_last($result[$level][$parent]); $type = $node->getType(); if (!isset($types[$type])) { $types[$type] = []; } - $types[$type][] = $node; + $types[$type][] = [ + 'node' => $node, + 'path' => [$level, $parent, $idx] + ]; } $this->nodes = $result; foreach ($types as $type => $nodes) { - $this->nodeTypeProvider->prepareData($type, $nodes); + $this->nodeTypeProvider->prepareData($type, array_column($nodes, 'node')); + } + + foreach ($types['category'] ?? [] as $nodes) { + $categoryProvider = $this->nodeTypeProvider->getProvider('category'); + $productCount = $categoryProvider->getCategoryProductCount($nodes['node']->getNodeId()); + if (empty($productCount) && $nodes['node']->getHideIfEmpty()) { + [$level, $parent, $idx] = $nodes['path']; + unset($this->nodes[$level][$parent][$idx]); + } } } @@ -509,6 +544,15 @@ private function getSubmenuTemplate() return $this->getMenuTemplate($baseSubmenuTemplate); } + private function canGatherEntityCacheTags() + { + if (!$this->_scopeConfig->isSetFlag(self::XML_SNOWMENU_GENERAL_CACHE_TAGS)) { + return false; + } + + return !empty($this->nodeTypeCaches); + } + public function getCustomerGroupId() { return $this->httpContext->getValue(\Magento\Customer\Model\Context::CONTEXT_GROUP); diff --git a/Block/NodeType/Category.php b/Block/NodeType/Category.php index 990c5aab..5722ecb7 100644 --- a/Block/NodeType/Category.php +++ b/Block/NodeType/Category.php @@ -43,6 +43,15 @@ class Category extends AbstractNode * @var array */ private $categories; + /** + * @var array + */ + private $cacheTags; + + /** + * @var array + */ + private $categoryProductCounts; /** * Category constructor. @@ -103,7 +112,14 @@ public function fetchData(array $nodes) { $storeId = $this->_storeManager->getStore()->getId(); - list($this->nodes, $this->categoryUrls, $this->categories) = $this->_categoryModel->fetchData($nodes, $storeId); + [ + $this->nodes, + $this->categoryUrls, + $this->categories, + $this->categoryProductCounts, + $this->cacheTags + ] = $this->_categoryModel->fetchData($nodes, $storeId); + } /** @@ -151,6 +167,22 @@ public function getCategoryUrl($nodeId, $storeId = null) return false; } + public function getCategoryProductCount($nodeId) + { + if (!isset($this->nodes[$nodeId])) { + throw new \InvalidArgumentException('Invalid node identifier specified'); + } + + $node = $this->nodes[$nodeId]; + $categoryId = (int) $node->getContent(); + + if (isset($this->categoryProductCounts[$categoryId])) { + return $this->categoryProductCounts[$categoryId]; + } + + return 0; + } + /** * @param int $nodeId * @@ -199,4 +231,9 @@ public function getLabel() { return __("Category"); } + + public function getEntityCacheTags() + { + return $this->cacheTags; + } } diff --git a/Model/GraphQl/Resolver/DataProvider/Node.php b/Model/GraphQl/Resolver/DataProvider/Node.php index 6c52e85e..b12c4466 100644 --- a/Model/GraphQl/Resolver/DataProvider/Node.php +++ b/Model/GraphQl/Resolver/DataProvider/Node.php @@ -113,7 +113,8 @@ private function convertData(NodeInterface $node): array NodeInterface::UPDATE_TIME => $node->getUpdateTime(), NodeInterface::ADDITIONAL_DATA => $node->getAdditionalData(), NodeInterface::SELECTED_ITEM_ID => $node->getSelectedItemId(), - NodeInterface::CUSTOMER_GROUPS => $node->getCustomerGroups() + NodeInterface::CUSTOMER_GROUPS => $node->getCustomerGroups(), + NodeInterface::HIDE_IF_EMPTY => $node->getHideIfEmpty(), ]; } diff --git a/Model/Menu/Node.php b/Model/Menu/Node.php index 4291d991..c9401adc 100644 --- a/Model/Menu/Node.php +++ b/Model/Menu/Node.php @@ -10,6 +10,9 @@ use Magento\Framework\Serialize\SerializerInterface; use Snowdog\Menu\Api\Data\NodeInterface; +/** + * @SuppressWarnings(PHPMD.ExcessivePublicCount) + */ class Node extends AbstractModel implements NodeInterface, IdentityInterface { const CACHE_TAG = 'snowdog_menu_node'; @@ -391,4 +394,14 @@ public function isVisible($customerGroupId) return false; } + + public function getHideIfEmpty() + { + return (int) $this->_getData(NodeInterface::HIDE_IF_EMPTY); + } + + public function setHideIfEmpty($hideIfEmpty) + { + return $this->setData(NodeInterface::HIDE_IF_EMPTY, (int) $hideIfEmpty); + } } diff --git a/Model/NodeType/Category.php b/Model/NodeType/Category.php index 32b10a44..e80ab876 100644 --- a/Model/NodeType/Category.php +++ b/Model/NodeType/Category.php @@ -122,10 +122,12 @@ public function fetchData(array $nodes, $storeId) $categoryUrls = $this->getResource()->fetchData($storeId, $categoryIds); $categories = $this->getCategories($storeId, $categoryIds); + $categoryProductCounts = $this->getResource()->getCategoriesProductCount($categoryIds); + $cacheTags = preg_filter('/^/', 'cat_c_p' . '_', $categoryIds); $this->profiler->stop(__METHOD__); - return [$localNodes, $categoryUrls, $categories]; + return [$localNodes, $categoryUrls, $categories, $categoryProductCounts, $cacheTags]; } /** @@ -136,6 +138,7 @@ public function fetchData(array $nodes, $storeId) public function getCategories($store, array $categoryIds) { $return = []; + /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categories */ $categories = $this->categoryCollection->create() ->addAttributeToSelect('*') ->setStoreId($store) @@ -144,6 +147,7 @@ public function getCategories($store, array $categoryIds) ['in' => $categoryIds] ); + /** @var \Magento\Catalog\Api\Data\CategoryInterface $category */ foreach ($categories as $category) { $return[$category->getId()] = $category; } diff --git a/Model/ResourceModel/NodeType/Category.php b/Model/ResourceModel/NodeType/Category.php index 831a0ad4..e269401c 100644 --- a/Model/ResourceModel/NodeType/Category.php +++ b/Model/ResourceModel/NodeType/Category.php @@ -15,6 +15,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\Store; +use Zend_Db_Expr; class Category extends AbstractNode { @@ -101,4 +102,22 @@ public function fetchData($storeId = Store::DEFAULT_STORE_ID, $categoryIds = []) return $connection->fetchPairs($select); } + + /** + * Get products count in categories + * + * @see \Magento\Catalog\Model\ResourceModel\Category::getProductCount + */ + public function getCategoriesProductCount($categoryIds = []) + { + $productTable = $this->getConnection()->getTableName('catalog_category_product'); + + $select = $this->getConnection() + ->select() + ->from($productTable, ['category_id', new Zend_Db_Expr('COUNT(product_id)')]) + ->where('category_id IN (?)', $categoryIds) + ->group('category_id'); + + return $this->getConnection()->fetchPairs($select); + } } diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 0cd14336..543dfb41 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -15,6 +15,11 @@ Controls serving different menus to different customer groups Magento\Config\Model\Config\Source\Yesno + + + Controls menu caching mechanisms. Enabling this will allow for gathering cache tags from menu nodes, which allows handling menus that are heavily dependent on e.g. category contents - see https://github.com/SnowdogApps/magento2-menu/discussions/317 + Magento\Config\Model\Config\Source\Yesno + diff --git a/etc/db_schema.xml b/etc/db_schema.xml index 21d02c1d..dd45623c 100644 --- a/etc/db_schema.xml +++ b/etc/db_schema.xml @@ -33,6 +33,7 @@ + diff --git a/etc/db_schema_whitelist.json b/etc/db_schema_whitelist.json index 94d4d708..1c0b24f3 100644 --- a/etc/db_schema_whitelist.json +++ b/etc/db_schema_whitelist.json @@ -35,7 +35,8 @@ "selected_item_id": true, "image_width": true, "image_heigth": true, - "customer_groups": true + "customer_groups": true, + "hide_if_empty": true }, "constraint": { "PRIMARY": true, diff --git a/etc/di.xml b/etc/di.xml index bf2f437f..c46b4223 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -74,4 +74,12 @@ Snowdog\Menu\Model\ImportExport\Processor\Import\Node\Validator\Proxy + + + + + category + + + diff --git a/view/adminhtml/templates/menu/nodes.phtml b/view/adminhtml/templates/menu/nodes.phtml index 2d1b2723..7e9f5b09 100644 --- a/view/adminhtml/templates/menu/nodes.phtml +++ b/view/adminhtml/templates/menu/nodes.phtml @@ -76,7 +76,9 @@ $vueComponents = $block->getVueComponents(); "imageHeight" : "", "selectedItemId" : "", "customerGroups" : "", - "customerGroupsDescription" : "" + "customerGroupsDescription" : "", + "hideIfEmpty" : "", + "hideIfEmptyDescription" : "" } } } diff --git a/view/adminhtml/web/vue/app.vue b/view/adminhtml/web/vue/app.vue index 0efeaa4c..2a7216fa 100644 --- a/view/adminhtml/web/vue/app.vue +++ b/view/adminhtml/web/vue/app.vue @@ -138,7 +138,8 @@ submenu_template: null, columns: [], is_active: 0, - customer_groups: [] + customer_groups: [], + hide_if_empty: 0 }); }, setUniqueIds(node) {