diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Longtext.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Longtext.php index e26e1702642285b07e44b08c2177dc1c12cd840c..626c899428ce7b4eac47a221840101adc8aa7319 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Longtext.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Longtext.php @@ -30,7 +30,7 @@ class Longtext extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abstra $truncateLength = $this->getColumn()->getTruncate(); } $text = $this->filterManager->truncate(parent::_getValue($row), ['length' => $truncateLength]); - if ($this->getColumn()->getEscape()) { + if (!$this->getColumn()->hasEscape() || $this->getColumn()->getEscape()) { $text = $this->escapeHtml($text); } if ($this->getColumn()->getNl2br()) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php index 0d0037acb67ee870de9c2c3380ce7166e88f9383..d94c3122b89eb362b1eb8ded2867cb3e2772d882 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php @@ -113,27 +113,20 @@ class Attribute extends \Magento\Eav\Model\ResourceModel\Entity\Attribute /** * Delete entity * - * @param \Magento\Framework\Model\AbstractModel $object + * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $object * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ - public function deleteEntity(\Magento\Framework\Model\AbstractModel $object) + public function deleteEntity(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $object) { if (!$object->getEntityAttributeId()) { return $this; } - $select = $this->getConnection()->select()->from( - $this->getTable('eav_entity_attribute') - )->where( - 'entity_attribute_id = ?', - (int)$object->getEntityAttributeId() - ); - $result = $this->getConnection()->fetchRow($select); - + $result = $this->getEntityAttribute($object->getEntityAttributeId()); if ($result) { $attribute = $this->_eavConfig->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + $object->getEntityTypeId(), $result['attribute_id'] ); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php index 9d4cb16f18ceae3149e1f3cf69bd6d4e8595f64a..02abcbd944e6245a080934abff9aad569c1ec968 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php @@ -5,13 +5,14 @@ */ namespace Magento\Catalog\Model\ResourceModel\Category; +use Magento\Framework\Data\Tree\Dbp; use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Framework\Model\Entity\MetadataPool; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Tree extends \Magento\Framework\Data\Tree\Dbp +class Tree extends Dbp { const ID_FIELD = 'id'; @@ -124,10 +125,10 @@ class Tree extends \Magento\Framework\Data\Tree\Dbp $resource->getConnection('catalog'), $resource->getTableName('catalog_category_entity'), [ - \Magento\Framework\Data\Tree\Dbp::ID_FIELD => 'entity_id', - \Magento\Framework\Data\Tree\Dbp::PATH_FIELD => 'path', - \Magento\Framework\Data\Tree\Dbp::ORDER_FIELD => 'position', - \Magento\Framework\Data\Tree\Dbp::LEVEL_FIELD => 'level' + Dbp::ID_FIELD => 'entity_id', + Dbp::PATH_FIELD => 'path', + Dbp::ORDER_FIELD => 'position', + Dbp::LEVEL_FIELD => 'level' ] ); $this->_eventManager = $eventManager; diff --git a/app/code/Magento/Catalog/Observer/AddCatalogToTopmenuItemsObserver.php b/app/code/Magento/Catalog/Observer/AddCatalogToTopmenuItemsObserver.php index a7e78014789d5b398b6bfc9095461724aa2d598f..8dab4df1c131c162aa953d67de3453e1f24b313a 100644 --- a/app/code/Magento/Catalog/Observer/AddCatalogToTopmenuItemsObserver.php +++ b/app/code/Magento/Catalog/Observer/AddCatalogToTopmenuItemsObserver.php @@ -5,8 +5,14 @@ */ namespace Magento\Catalog\Observer; +use Magento\Catalog\Model\Category; +use Magento\Framework\Data\Collection; +use Magento\Framework\Data\Tree\Node; use Magento\Framework\Event\ObserverInterface; +/** + * Observer that add Categories Tree to Topmenu + */ class AddCatalogToTopmenuItemsObserver implements ObserverInterface { /** @@ -17,28 +23,38 @@ class AddCatalogToTopmenuItemsObserver implements ObserverInterface protected $catalogCategory; /** - * @var \Magento\Catalog\Model\Indexer\Category\Flat\State + * @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory + */ + private $collectionFactory; + + /** + * @var \Magento\Store\Model\StoreManagerInterface */ - protected $categoryFlatState; + private $storeManager; /** - * @var MenuCategoryData + * @var \Magento\Catalog\Model\Layer\Resolver */ - protected $menuCategoryData; + private $layerResolver; /** * @param \Magento\Catalog\Helper\Category $catalogCategory * @param \Magento\Catalog\Model\Indexer\Category\Flat\State $categoryFlatState - * @param \Magento\Catalog\Observer\MenuCategoryData $menuCategoryData + * @param \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param \Magento\Catalog\Helper\Category $catalogCategory + * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver */ public function __construct( \Magento\Catalog\Helper\Category $catalogCategory, - \Magento\Catalog\Model\Indexer\Category\Flat\State $categoryFlatState, - \Magento\Catalog\Observer\MenuCategoryData $menuCategoryData + \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\Catalog\Model\Layer\Resolver $layerResolver ) { $this->catalogCategory = $catalogCategory; - $this->categoryFlatState = $categoryFlatState; - $this->menuCategoryData = $menuCategoryData; + $this->collectionFactory = $categoryCollectionFactory; + $this->storeManager = $storeManager; + $this->layerResolver = $layerResolver; } /** @@ -50,38 +66,96 @@ class AddCatalogToTopmenuItemsObserver implements ObserverInterface public function execute(\Magento\Framework\Event\Observer $observer) { $block = $observer->getEvent()->getBlock(); - $block->addIdentity(\Magento\Catalog\Model\Category::CACHE_TAG); - $this->_addCategoriesToMenu($this->catalogCategory->getStoreCategories(), $observer->getMenu(), $block); + $menuRootNode = $observer->getEvent()->getMenu(); + $block->addIdentity(Category::CACHE_TAG); + + $rootId = $this->storeManager->getStore()->getRootCategoryId(); + $storeId = $this->storeManager->getStore()->getId(); + + /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ + $collection = $this->getCategoryTree($storeId, $rootId); + + $currentCategory = $this->getCurrentCategory(); + + $mapping = [$rootId => $menuRootNode]; // use nodes stack to avoid recursion + foreach ($collection as $category) { + if (!isset($mapping[$category->getParentId()])) { + continue; + } + /** @var Node $parentCategoryNode */ + $parentCategoryNode = $mapping[$category->getParentId()]; + + $categoryNode = new Node( + $this->getCategoryAsArray($category, $currentCategory), + 'id', + $parentCategoryNode->getTree(), + $parentCategoryNode + ); + $parentCategoryNode->addChild($categoryNode); + + $mapping[$category->getId()] = $categoryNode; //add node in stack + + $block->addIdentity(Category::CACHE_TAG . '_' . $category->getId()); + } } /** - * Recursively adds categories to top menu + * Get current Category from catalog layer * - * @param \Magento\Framework\Data\Tree\Node\Collection|array $categories - * @param \Magento\Framework\Data\Tree\Node $parentCategoryNode - * @param \Magento\Theme\Block\Html\Topmenu $block - * @return void + * @return \Magento\Catalog\Model\Category */ - protected function _addCategoriesToMenu($categories, $parentCategoryNode, $block) + private function getCurrentCategory() { - foreach ($categories as $category) { - if (!$category->getIsActive()) { - continue; - } - $block->addIdentity(\Magento\Catalog\Model\Category::CACHE_TAG . '_' . $category->getId()); + $catalogLayer = $this->layerResolver->get(); - $tree = $parentCategoryNode->getTree(); - $categoryData = $this->menuCategoryData->getMenuCategoryData($category); - $categoryNode = new \Magento\Framework\Data\Tree\Node($categoryData, 'id', $tree, $parentCategoryNode); - $parentCategoryNode->addChild($categoryNode); + if (!$catalogLayer) { + return null; + } - if ($this->categoryFlatState->isFlatEnabled() && $category->getUseFlatResource()) { - $subcategories = (array)$category->getChildrenNodes(); - } else { - $subcategories = $category->getChildren(); - } + return $catalogLayer->getCurrentCategory(); + } - $this->_addCategoriesToMenu($subcategories, $categoryNode, $block); - } + /** + * Convert category to array + * + * @param \Magento\Catalog\Model\Category $category + * @param \Magento\Catalog\Model\Category $currentCategory + * @return array + */ + private function getCategoryAsArray($category, $currentCategory) + { + return [ + 'name' => $category->getName(), + 'id' => 'category-node-' . $category->getId(), + 'url' => $this->catalogCategory->getCategoryUrl($category), + 'has_active' => in_array((string)$category->getId(), explode('/', $currentCategory->getPath()), true), + 'is_active' => $category->getId() == $currentCategory->getId() + ]; + } + + /** + * Get Category Tree + * + * @param int $storeId + * @param int $rootId + * @return \Magento\Catalog\Model\ResourceModel\Category\Collection + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected function getCategoryTree($storeId, $rootId) + { + /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $collection */ + $collection = $this->collectionFactory->create(); + $collection->setStoreId($storeId); + $collection->addAttributeToSelect('name'); + $collection->addFieldToFilter('path', ['like' => '1/' . $rootId . '/%']); //load only from store root + $collection->addAttributeToFilter('include_in_menu', 1); + $collection->addAttributeToFilter('is_active', 1); + $collection->addUrlRewriteToResult(); + $collection->addOrder('level', Collection::SORT_ORDER_ASC); + $collection->addOrder('position', Collection::SORT_ORDER_ASC); + $collection->addOrder('parent_id', Collection::SORT_ORDER_ASC); + $collection->addOrder('entity_id', Collection::SORT_ORDER_ASC); + + return $collection; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Observer/AddCatalogToTopmenuItemsObserverTest.php b/app/code/Magento/Catalog/Test/Unit/Observer/AddCatalogToTopmenuItemsObserverTest.php index f8a9b12083b16710f78a60b616124b8871b94180..e0eb21a8bba306a258a26440a5cf957438427c5e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Observer/AddCatalogToTopmenuItemsObserverTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Observer/AddCatalogToTopmenuItemsObserverTest.php @@ -52,14 +52,6 @@ class AddCatalogToTopmenuItemsObserverTest extends \PHPUnit_Framework_TestCase false ); - $this->_categoryFlatState = $this->getMock( - '\Magento\Catalog\Model\Indexer\Category\Flat\State', - ['isFlatEnabled'], - [], - '', - false - ); - $this->menuCategoryData = $this->getMock( 'Magento\Catalog\Observer\MenuCategoryData', ['getMenuCategoryData'], @@ -67,12 +59,44 @@ class AddCatalogToTopmenuItemsObserverTest extends \PHPUnit_Framework_TestCase '', false ); + + $this->store = $this->getMockBuilder('Magento\Store\Model\Store') + ->setMethods(['getRootCategoryId', 'getFilters', '__wakeup']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->storeManager = $this->getMockBuilder('Magento\Store\Model\StoreManagerInterface') + ->setMethods(['getStore']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->storeManager->expects($this->any())->method('getStore') + ->will($this->returnValue($this->store)); + + $this->store->expects($this->any())->method('getRootCategoryId') + ->will($this->returnValue(1)); + + $collectionFactory = $this->getMockBuilder('\Magento\Catalog\Model\ResourceModel\Category\CollectionFactory') + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + + $collection = $this->getMockBuilder('\Magento\Catalog\Model\ResourceModel\Category\Collection') + ->disableOriginalConstructor() + ->getMock(); + + $collectionFactory->expects($this->once())->method('create') + ->willReturn($collection); + + $collection->expects($this->once())->method('getIterator') + ->willReturn(new \ArrayIterator([])); + $this->_observer = (new ObjectManager($this))->getObject( 'Magento\Catalog\Observer\AddCatalogToTopmenuItemsObserver', [ 'catalogCategory' => $this->_catalogCategory, 'menuCategoryData' => $this->menuCategoryData, - 'categoryFlatState' => $this->_categoryFlatState, + 'storeManager' => $this->storeManager, + 'categoryCollectionFactory' => $collectionFactory, ] ); } @@ -97,9 +121,7 @@ class AddCatalogToTopmenuItemsObserverTest extends \PHPUnit_Framework_TestCase '', false ); - $this->_childrenCategory->expects($this->once()) - ->method('getIsActive') - ->will($this->returnValue(false)); + $this->_category = $this->getMock( '\Magento\Catalog\Model\Category', @@ -108,69 +130,27 @@ class AddCatalogToTopmenuItemsObserverTest extends \PHPUnit_Framework_TestCase '', false ); - $this->_category->expects($this->once()) - ->method('getIsActive') - ->will($this->returnValue(true)); - - $this->_catalogCategory->expects($this->once()) - ->method('getStoreCategories') - ->will($this->returnValue([$this->_category])); - $this->menuCategoryData->expects($this->once()) - ->method('getMenuCategoryData') - ->with($this->_category); $blockMock = $this->_getCleanMock('\Magento\Theme\Block\Html\Topmenu'); - $treeMock = $this->_getCleanMock('\Magento\Framework\Data\Tree'); - - $menuMock = $this->getMock('\Magento\Framework\Data\Tree\Node', ['getTree', 'addChild'], [], '', false); - $menuMock->expects($this->once()) - ->method('getTree') - ->will($this->returnValue($treeMock)); - $eventMock = $this->getMock('\Magento\Framework\Event', ['getBlock'], [], '', false); $eventMock->expects($this->once()) ->method('getBlock') ->will($this->returnValue($blockMock)); $observerMock = $this->getMock('\Magento\Framework\Event\Observer', ['getEvent', 'getMenu'], [], '', false); - $observerMock->expects($this->once()) + $observerMock->expects($this->any()) ->method('getEvent') ->will($this->returnValue($eventMock)); - $observerMock->expects($this->once()) - ->method('getMenu') - ->will($this->returnValue($menuMock)); return $observerMock; } - public function testAddCatalogToTopMenuItemsWithoutFlat() + public function testAddCatalogToTopMenuItems() { $observer = $this->_preparationData(); - - $this->_category->expects($this->once()) - ->method('getChildren') - ->will($this->returnValue([$this->_childrenCategory])); - $this->_observer->execute($observer); } - public function testAddCatalogToTopMenuItemsWithFlat() - { - $observer = $this->_preparationData(); - - $this->_category->expects($this->once()) - ->method('getChildrenNodes') - ->will($this->returnValue([$this->_childrenCategory])); - - $this->_category->expects($this->once()) - ->method('getUseFlatResource') - ->will($this->returnValue(true)); - $this->_categoryFlatState->expects($this->once()) - ->method('isFlatEnabled') - ->will($this->returnValue(true)); - - $this->_observer->execute($observer); - } } diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/custom-options.js b/app/code/Magento/Catalog/view/adminhtml/web/js/custom-options.js index 28f0ec5842a38dd7e63a3918f6e48fc04b7568ec..f1e3e33a99af289ce72a55e41238776843989dfc 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/custom-options.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/custom-options.js @@ -339,7 +339,7 @@ define([ checkbox: 'input[id$=_price_use_default]', label: 'span' }); - //@TODO not work set default value for second field + //not work set default value for second field priceType.useDefault({ field: '.field', useDefault: 'label[for$=_price]', diff --git a/app/code/Magento/Catalog/view/frontend/templates/js/components.phtml b/app/code/Magento/Catalog/view/base/templates/js/components.phtml similarity index 100% rename from app/code/Magento/Catalog/view/frontend/templates/js/components.phtml rename to app/code/Magento/Catalog/view/base/templates/js/components.phtml diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Set.php b/app/code/Magento/Eav/Model/Entity/Attribute/Set.php index d35fafee492d8408e0c9e7d00b41afa823396566..d63f6a63f455a0ac7caa488485a9a4954ccbb6dd 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Set.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Set.php @@ -173,6 +173,7 @@ class Set extends \Magento\Framework\Model\AbstractExtensibleModel implements * * @param array $data * @return $this + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -220,10 +221,17 @@ class Set extends \Magento\Framework\Model\AbstractExtensibleModel implements if ($data['not_attributes']) { $modelAttributeArray = []; - foreach ($data['not_attributes'] as $attributeId) { - $modelAttribute = $this->_attributeFactory->create(); - - $modelAttribute->setEntityAttributeId($attributeId); + $data['not_attributes'] = array_filter($data['not_attributes']); + foreach ($data['not_attributes'] as $entityAttributeId) { + $entityAttribute = $this->_resourceAttribute->getEntityAttribute($entityAttributeId); + if (!$entityAttribute) { + throw new LocalizedException(__('Entity attribute with id "%1" not found', $entityAttributeId)); + } + $modelAttribute = $this->_eavConfig->getAttribute( + $this->getEntityTypeId(), + $entityAttribute['attribute_id'] + ); + $modelAttribute->setEntityAttributeId($entityAttributeId); $modelAttributeArray[] = $modelAttribute; } $this->setRemoveAttributes($modelAttributeArray); diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 46d4c57cf0a7a1c3602af0de224f638be4a0df11..47fe17eed63c0db64e048f35505d24832e75c287 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -133,10 +133,10 @@ class Attribute extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb /** * Delete entity * - * @param AbstractModel $object + * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $object * @return $this */ - public function deleteEntity(AbstractModel $object) + public function deleteEntity(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $object) { if (!$object->getEntityAttributeId()) { return $this; @@ -485,6 +485,23 @@ class Attribute extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb return $connection->fetchOne($select, $bind); } + /** + * Get entity attribute + * + * @param int|string $entityAttributeId + * @return array + */ + public function getEntityAttribute($entityAttributeId) + { + $select = $this->getConnection()->select()->from( + $this->getTable('eav_entity_attribute') + )->where( + 'entity_attribute_id = ?', + (int)$entityAttributeId + ); + return $this->getConnection()->fetchRow($select); + } + /** * Retrieve attribute codes by front-end type * diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index 1bd5d307d419866b0c32e37a9eda6b562ba84323..809829ce6df0bc8e659988844ff6f853910b3862 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -89,4 +89,8 @@ <argument name="contextHandler" xsi:type="object">Magento\Eav\Model\ResourceModel\ContextHandler</argument> </arguments> </type> + <type name="Magento\Eav\Model\Entity\AbstractEntity"> + <plugin name="clean_cache" type="Magento\Framework\App\Cache\FlushCacheByTags" /> + </type> </config> + diff --git a/app/code/Magento/Theme/Block/Html/Topmenu.php b/app/code/Magento/Theme/Block/Html/Topmenu.php index bacb704627493e1872c570539a8898bf679492b2..8b86ee29a86d29e78786291c29dcc2ba733ec3d3 100644 --- a/app/code/Magento/Theme/Block/Html/Topmenu.php +++ b/app/code/Magento/Theme/Block/Html/Topmenu.php @@ -49,6 +49,7 @@ class Topmenu extends Template implements IdentityInterface TreeFactory $treeFactory, array $data = [] ) { + $this->setCacheLifetime(30 * 60); parent::__construct($context, $data); $this->_menu = $nodeFactory->create( [ @@ -85,7 +86,6 @@ class Topmenu extends Template implements IdentityInterface ['menu' => $this->_menu, 'transportObject' => $transportObject] ); $html = $transportObject->getHtml(); - return $html; } @@ -333,4 +333,26 @@ class Topmenu extends Template implements IdentityInterface { return $this->identities; } + + /** + * Get cache key informative items + * + * @return array + */ + public function getCacheKeyInfo() + { + $keyInfo = parent::getCacheKeyInfo(); + $keyInfo[] = $this->getUrl('*/*/*', ['_current' => true, '_query' => '']); + return $keyInfo; + } + + /** + * Get tags array for saving cache + * + * @return array + */ + protected function getCacheTags() + { + return array_merge(parent::getCacheTags(), $this->getIdentities()); + } } diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php index d8b18edafa1232efddaccad35d35947af39cf8ed..b8260963ecbd7cc79d910ded6a0e07996c91acf8 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/TopmenuTest.php @@ -14,6 +14,11 @@ use Magento\Framework\Data\Tree\NodeFactory; class TopmenuTest extends \PHPUnit_Framework_TestCase { + /** + * @var \Magento\Framework\UrlInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $urlBuilder; + /** * @var Registry|\PHPUnit_Framework_MockObject_MockObject */ @@ -51,15 +56,71 @@ HTML; <li class="level0 nav-1 first active"><a href="http://magento2/category-0.html" ><span></span></a></li><li class="level0 nav-2"><a href="http://magento2/category-1.html" ><span></span></a></li><li class="level0 nav-3"><a href="http://magento2/category-2.html" ><span></span></a></li><li class="level0 nav-4"><a href="http://magento2/category-3.html" ><span></span></a></li><li class="level0 nav-5"><a href="http://magento2/category-4.html" ><span></span></a></li><li class="level0 nav-6"><a href="http://magento2/category-5.html" ><span></span></a></li><li class="level0 nav-7"><a href="http://magento2/category-6.html" ><span></span></a></li><li class="level0 nav-8"><a href="http://magento2/category-7.html" ><span></span></a></li><li class="level0 nav-9"><a href="http://magento2/category-8.html" ><span></span></a></li><li class="level0 nav-10 last"><a href="http://magento2/category-9.html" ><span></span></a></li> HTML; + /** + * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $storeManager; + // @codingStandardsIgnoreEnd public function setUp() { - $isCurrentItem = $this->getName() == 'testGetHtmlWithSelectedCategory' ? true : false; + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->getMockForAbstractClass(); + + $this->urlBuilder = $this->getMockBuilder(\Magento\Framework\UrlInterface::class)->getMockForAbstractClass(); + $this->context = $objectManager->getObject( + 'Magento\Framework\View\Element\Template\Context', + ['urlBuilder' => $this->urlBuilder, 'storeManager' => $this->storeManager] + ); - $this->context = $objectManager->getObject('Magento\Framework\View\Element\Template\Context'); + } + protected function getTopmenu() + { + return new Topmenu($this->context, $this->nodeFactory, $this->treeFactory); + } + + public function testGetHtmlWithoutSelectedCategory() + { + $this->buildTree(false); + $this->assertEquals($this->htmlWithoutCategory, $this->getTopmenu()->getHtml()); + } + + public function testGetHtmlWithSelectedCategory() + { + $this->buildTree(true); + $this->assertEquals($this->htmlWithCategory, $this->getTopmenu()->getHtml()); + } + + public function testGetCacheKeyInfo() + { + $nodeFactory = $this->getMock('Magento\Framework\Data\Tree\NodeFactory', [], [], '', false); + $treeFactory = $this->getMock('Magento\Framework\Data\TreeFactory', [], [], '', false); + + $topmenu = new Topmenu($this->context, $nodeFactory, $treeFactory); + $this->urlBuilder->expects($this->once())->method('getUrl')->with('*/*/*')->willReturn('123'); + $store = $this->getMockBuilder('Magento\Store\Model\Store') + ->disableOriginalConstructor() + ->setMethods(['getCode']) + ->getMock(); + $store->expects($this->once())->method('getCode')->willReturn('321'); + $this->storeManager->expects($this->once())->method('getStore')->willReturn($store); + + $this->assertEquals( + ['BLOCK_TPL', '321', null, 'template' => null, '123'], + $topmenu->getCacheKeyInfo() + ); + } + + /** + * @param $isCurrentItem + * @return void + */ + private function buildTree($isCurrentItem) + { $this->nodeFactory = $this->getMock('Magento\Framework\Data\Tree\NodeFactory', [], [], '', false); $this->treeFactory = $this->getMock('Magento\Framework\Data\TreeFactory', [], [], '', false); @@ -76,14 +137,8 @@ HTML; for ($i = 0; $i < 10; $i++) { $id = "category-node-$i"; $categoryNode = $this->getMock('Magento\Framework\Data\Tree\Node', ['getId', 'hasChildren'], [], '', false); - $categoryNode - ->expects($this->once()) - ->method('getId') - ->willReturn($id); - $categoryNode - ->expects($this->atLeastOnce()) - ->method('hasChildren') - ->willReturn(false); + $categoryNode->expects($this->once())->method('getId')->willReturn($id); + $categoryNode->expects($this->atLeastOnce())->method('hasChildren')->willReturn(false); $categoryNode->setData( [ 'name' => "Category $i", @@ -97,45 +152,14 @@ HTML; $children->add($categoryNode); } - $children - ->expects($this->once()) - ->method('count') - ->willReturn(10); + $children->expects($this->once())->method('count')->willReturn(10); $node = $this->getMock('Magento\Framework\Data\Tree\Node', ['getChildren'], [], '', false); - $node - ->expects($this->once()) - ->method('getChildren') - ->willReturn($children); - $node - ->expects($this->any()) - ->method('__call') - ->with('getLevel', []) - ->willReturn(null); - - $this->nodeFactory - ->expects($this->once()) - ->method('create') - ->willReturn($node); - - $this->treeFactory - ->expects($this->once()) - ->method('create') - ->willReturn($tree); - } - - protected function getTopmenu() - { - return new Topmenu($this->context, $this->nodeFactory, $this->treeFactory); - } + $node->expects($this->once())->method('getChildren')->willReturn($children); + $node->expects($this->any())->method('__call')->with('getLevel', [])->willReturn(null); - public function testGetHtmlWithoutSelectedCategory() - { - $this->assertEquals($this->htmlWithoutCategory, $this->getTopmenu()->getHtml()); - } + $this->nodeFactory->expects($this->once())->method('create')->willReturn($node); - public function testGetHtmlWithSelectedCategory() - { - $this->assertEquals($this->htmlWithCategory, $this->getTopmenu()->getHtml()); + $this->treeFactory->expects($this->once())->method('create')->willReturn($tree); } } diff --git a/app/code/Magento/Theme/view/frontend/templates/html/topmenu.phtml b/app/code/Magento/Theme/view/frontend/templates/html/topmenu.phtml index 6f8c7965c4412df8b866505bb01294536e429f40..15067b664c252cbfb43d43de192296a2eb70f20b 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/topmenu.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/topmenu.phtml @@ -11,7 +11,7 @@ /** * Top menu for store * - * @see \Magento\Theme\Block\Html\Topmenu + * @var $block \Magento\Theme\Block\Html\Topmenu */ ?> <?php $columnsLimit = $block->getColumnsLimit() ?: 0; ?> @@ -20,5 +20,6 @@ <nav class="navigation" data-action="navigation"> <ul data-mage-init='{"menu":{"responsive":true, "expanded":true, "position":{"my":"left top","at":"left bottom"}}}'> <?php /* @escapeNotVerified */ echo $_menu; ?> + <?php /* @escapeNotVerified */ echo $block->getChildHtml(); ?> </ul> </nav> diff --git a/app/etc/di.xml b/app/etc/di.xml index 8bdf6e4d498b8d949117eef496c99d96ceb9adc3..3aa20b5c0331a4f0f658c4480e57a724709c289c 100755 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1174,4 +1174,14 @@ </argument> </arguments> </type> + <type name="Magento\Framework\App\Cache\FlushCacheByTags"> + <arguments> + <argument name="cacheList" xsi:type="array"> + <item name="block_html" xsi:type="const">Magento\Framework\App\Cache\Type\Block::TYPE_IDENTIFIER</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\Model\ResourceModel\Db\AbstractDb"> + <plugin name="clean_cache" type="Magento\Framework\App\Cache\FlushCacheByTags" /> + </type> </config> diff --git a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php new file mode 100644 index 0000000000000000000000000000000000000000..468000e7de4125e83a9de8c17a3dae57dbcdbbca --- /dev/null +++ b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php @@ -0,0 +1,110 @@ +<?php +/** + * + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\App\Cache; + +/** + * Automatic cache cleaner plugin + */ +class FlushCacheByTags +{ + /** + * @var Type\FrontendPool + */ + private $cachePool; + + /** + * @var array + */ + private $cacheList; + + /** + * @var StateInterface + */ + private $cacheState; + + /** + * FlushCacheByTags constructor. + * + * @param Type\FrontendPool $cachePool + * @param StateInterface $cacheState + * @param array $cacheList + */ + public function __construct( + \Magento\Framework\App\Cache\Type\FrontendPool $cachePool, + \Magento\Framework\App\Cache\StateInterface $cacheState, + array $cacheList + ) { + $this->cachePool = $cachePool; + $this->cacheState = $cacheState; + $this->cacheList = $cacheList; + } + + /** + * Clean cache on save object + * + * @param \Magento\Framework\Model\ResourceModel\AbstractResource $subject + * @param \Closure $proceed + * @param \Magento\Framework\Model\AbstractModel $object + * @return \Magento\Framework\Model\ResourceModel\AbstractResource + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundSave( + \Magento\Framework\Model\ResourceModel\AbstractResource $subject, + \Closure $proceed, + \Magento\Framework\Model\AbstractModel $object + ) { + $result = $proceed($object); + if ($object instanceof \Magento\Framework\DataObject\IdentityInterface) { + $this->cleanCacheByTags($object->getIdentities()); + } + return $result; + } + + /** + * Clean cache on delete object + * + * @param \Magento\Framework\Model\ResourceModel\AbstractResource $subject + * @param \Closure $proceed + * @param \Magento\Framework\Model\AbstractModel $object + * @return \Magento\Framework\Model\ResourceModel\AbstractResource + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundDelete( + \Magento\Framework\Model\ResourceModel\AbstractResource $subject, + \Closure $proceed, + \Magento\Framework\Model\AbstractModel $object + ) { + $tags = []; + if ($object instanceof \Magento\Framework\DataObject\IdentityInterface) { + $tags = $object->getIdentities(); + } + $result = $proceed($object); + $this->cleanCacheByTags($tags); + return $result; + } + + /** + * Clean cache by tags + * + * @param string[] $tags + * @return void + */ + private function cleanCacheByTags($tags) + { + if (empty($tags)) { + return; + } + foreach ($this->cacheList as $cacheType) { + if ($this->cacheState->isEnabled($cacheType)) { + $this->cachePool->get($cacheType)->clean( + \Zend_Cache::CLEANING_MODE_MATCHING_TAG, + array_unique($tags) + ); + } + } + } +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..902df102283cb0900f24deef645c08ec4c9e2abf --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Created by PhpStorm. + * User: akasian + * Date: 1/25/16 + * Time: 10:26 AM + */ + +namespace Magento\Framework\App\Test\Unit\Cache; + +class FlushCacheByTagsTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\App\Cache\StateInterface + */ + private $cacheState; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Framework\App\Cache\Type\FrontendPool + */ + private $frontendPool; + + /** + * @var \Magento\Framework\App\Cache\FlushCacheByTags + */ + private $plugin; + + protected function setUp() + { + $this->cacheState = $this->getMockForAbstractClass(\Magento\Framework\App\Cache\StateInterface::class); + $this->frontendPool = $this->getMock(\Magento\Framework\App\Cache\Type\FrontendPool::class, [], [], '', false); + $this->plugin = new \Magento\Framework\App\Cache\FlushCacheByTags( + $this->frontendPool, + $this->cacheState, + ['test'] + ); + + } + + public function testAroundSave() + { + $resource = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\AbstractResource::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $model = $this->getMockBuilder(\Magento\Framework\Model\AbstractModel::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $result = $this->plugin->aroundSave( + $resource, + function () use ($resource) { + return $resource; + }, + $model + ); + $this->assertSame($resource, $result); + } + + public function testAroundDelete() + { + $resource = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\AbstractResource::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $model = $this->getMockBuilder(\Magento\Framework\Model\AbstractModel::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $result = $this->plugin->aroundDelete( + $resource, + function () use ($resource) { + return $resource; + }, + $model + ); + $this->assertSame($resource, $result); + } + + public function testAroundSaveWithInterface() + { + $resource = $this->getMockBuilder(\Magento\Framework\Model\ResourceModel\AbstractResource::class) + + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $model = $this->getMockBuilder(\Magento\Framework\Model\AbstractModel::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $result = $this->plugin->aroundSave( + $resource, + function () use ($resource) { + return $resource; + }, + $model + ); + $this->assertSame($resource, $result); + } +} diff --git a/lib/internal/Magento/Framework/Url/QueryParamsResolver.php b/lib/internal/Magento/Framework/Url/QueryParamsResolver.php index 075ecfe0d4ee9bded7c543d1ce3a147d76752578..c74cbbfab9e82b010d13c3dc8819e1f79a116fd0 100644 --- a/lib/internal/Magento/Framework/Url/QueryParamsResolver.php +++ b/lib/internal/Magento/Framework/Url/QueryParamsResolver.php @@ -29,7 +29,7 @@ class QueryParamsResolver extends \Magento\Framework\DataObject implements Query */ public function setQuery($data) { - if ($this->_getData('query') != $data) { + if ($this->_getData('query') !== $data) { $this->unsetData('query_params'); $this->setData('query', $data); } diff --git a/setup/src/Magento/Setup/Test/Unit/Fixtures/StoresFixtureTest.php b/setup/src/Magento/Setup/Test/Unit/Fixtures/StoresFixtureTest.php index 07cd715489eab46621edfc68ae1a332c5678d5d3..bd8d4022d063794ba5d8a0f530ab93cf55311da1 100644 --- a/setup/src/Magento/Setup/Test/Unit/Fixtures/StoresFixtureTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Fixtures/StoresFixtureTest.php @@ -75,7 +75,6 @@ class StoresFixtureTest extends \PHPUnit_Framework_TestCase $categoryMock = $this->getMock( 'Magento\Catalog\Model\Category', [ - 'setId', 'setName', 'setPath', 'setLevel', @@ -83,15 +82,13 @@ class StoresFixtureTest extends \PHPUnit_Framework_TestCase 'setDefaultSortBy', 'setIsActive', 'getId', - 'save' + 'save', + 'load' ], [], '', false ); - $categoryMock->expects($this->once()) - ->method('setId') - ->willReturnSelf(); $categoryMock->expects($this->once()) ->method('setName') ->willReturnSelf();