Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SMM-5 Dynamic menu improvements #355

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
13 changes: 13 additions & 0 deletions Api/Data/NodeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions Block/Adminhtml/Edit/Tab/Nodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ private function renderNodeList($level, $parent, $data)
foreach ($nodes as $node) {
$menu[] = [
'is_active' => $node->getIsActive(),
'hide_if_empty' => $node->getHideIfEmpty(),
'is_stored' => true,
'type' => $node->getType(),
'content' => $node->getContent(),
Expand Down
50 changes: 47 additions & 3 deletions Block/Menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,6 +85,11 @@ class Menu extends Template implements DataObject\IdentityInterface
*/
private $httpContext;

/**
* @var array
*/
private $nodeTypeCaches = [];

/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
Expand All @@ -97,6 +103,7 @@ public function __construct(
ImageFile $imageFile,
Escaper $escaper,
Context $httpContext,
array $nodeTypeCaches = [],
array $data = []
) {
parent::__construct($context, $data);
Expand All @@ -110,6 +117,7 @@ public function __construct(
$this->setTemplate($this->getMenuTemplate($this->_template));
$this->submenuTemplate = $this->getSubmenuTemplate();
$this->httpContext = $httpContext;
$this->nodeTypeCaches = $nodeTypeCaches;
}

/**
Expand All @@ -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()
Expand Down Expand Up @@ -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());
Expand All @@ -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]);
}
}
}

Expand Down Expand Up @@ -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);
Expand Down
39 changes: 38 additions & 1 deletion Block/NodeType/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ class Category extends AbstractNode
* @var array
*/
private $categories;
/**
* @var array
*/
private $cacheTags;

/**
* @var array
*/
private $categoryProductCounts;

/**
* Category constructor.
Expand Down Expand Up @@ -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);

}

/**
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -199,4 +231,9 @@ public function getLabel()
{
return __("Category");
}

public function getEntityCacheTags()
{
return $this->cacheTags;
}
}
3 changes: 2 additions & 1 deletion Model/GraphQl/Resolver/DataProvider/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
];
}

Expand Down
13 changes: 13 additions & 0 deletions Model/Menu/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
}
6 changes: 5 additions & 1 deletion Model/NodeType/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

/**
Expand All @@ -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)
Expand All @@ -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;
}
Expand Down
19 changes: 19 additions & 0 deletions Model/ResourceModel/NodeType/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may provide misleading data as catalog_category_product table is source of truth for all product category associations including disabled and not visible products. (In Example will indicate category full of disabled products as non-zero products, even thou navigating to that category will yield zero products).

There is table catalog_category_product_index_store<store_id> that contains visibility - can You check if it is depopluated when changing product status? As it may be better source of data if it is indeed not having entries for disabled products.


$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);
}
}
1 change: 1 addition & 0 deletions Service/Menu/SaveRequestProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ private function processNodeObject(
$nodeObject->setMenuId($menu->getMenuId());
$nodeObject->setTitle($nodeData['title']);
$nodeObject->setIsActive($nodeData['is_active'] ?? '0');
$nodeObject->setHideIfEmpty($nodeData['hide_if_empty'] ?? '0');
$nodeObject->setLevel((string) $level);
$nodeObject->setPosition((string) $position);

Expand Down
5 changes: 5 additions & 0 deletions etc/adminhtml/system.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
<comment>Controls serving different menus to different customer groups</comment>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="cache_tags" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Gather node entities cache tags</label>
<comment>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</comment>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
</group>
</section>
</system>
Expand Down
1 change: 1 addition & 0 deletions etc/db_schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<column xsi:type="smallint" name="selected_item_id" padding="5" unsigned="true" nullable="true" identity="false" comment="Selected Item Id"/>
<column xsi:type="int" name="image_width" padding="11" unsigned="false" nullable="true" identity="false" comment="Image Width"/>
<column xsi:type="int" name="image_heigth" padding="11" unsigned="false" nullable="true" identity="false" comment="Image Height"/>
<column xsi:type="smallint" name="hide_if_empty" padding="6" unsigned="false" nullable="false" identity="false" default="0" comment="Hide If Empty"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="node_id"/>
</constraint>
Expand Down
3 changes: 2 additions & 1 deletion etc/db_schema_whitelist.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions etc/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,12 @@
<argument name="validator" xsi:type="object">Snowdog\Menu\Model\ImportExport\Processor\Import\Node\Validator\Proxy</argument>
</arguments>
</type>

<type name="Snowdog\Menu\Block\Menu">
<arguments>
<argument name="nodeTypeCaches" xsi:type="array">
<item name="category" xsi:type="string">category</item>
</argument>
</arguments>
</type>
</config>
8 changes: 5 additions & 3 deletions view/adminhtml/templates/menu/nodes.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,13 @@ $vueComponents = $block->getVueComponents();
"product" : "<?= __('Product') ?>",
"productId" : "<?= __('Product ID') ?>",
"imageAltText" : "<?= __('Image Alt text') ?>",
"imageWidth" : "<?= __('Image Width') ?>",
"imageHeight" : "<?= __('Image Height') ?>",
"imageWidth" : "<?= __('Image Width') ?>",
"imageHeight" : "<?= __('Image Height') ?>",
"selectedItemId" : "<?= __('Selected Item Id') ?>",
"customerGroups" : "<?= __('Customer Groups') ?>",
"customerGroupsDescription" : "<?= __('If no customer group is selected, the node will be visible to all customers.') ?>"
"customerGroupsDescription" : "<?= __('If no customer group is selected, the node will be visible to all customers.') ?>",
"hideIfEmpty" : "<?= __("Hide node if empty") ?>",
"hideIfEmptyDescription" : "<?= __("(e.g. no product in categories)") ?>"
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions view/adminhtml/web/css/source/blocks/_panel.less
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,13 @@
}

// overwrite default Magento admin styles
.admin__fieldset > .admin__field.admin__scope-old > .admin__field-control input[type="checkbox"] {
margin: 0;
.admin__fieldset > .admin__field.admin__scope-old > .admin__field-control {
input[type="checkbox"] {
margin: 0;
}

.admin__field-control__description {
display: flex;
}
}
}
Loading
Loading