Skip to content

Commit 8fa5564

Browse files
MC-22738: Layered Navigation with different product attributes on Category page
1 parent 6da226d commit 8fa5564

15 files changed

+1106
-2
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
use Magento\Catalog\Api\Data\ProductInterfaceFactory;
9+
use Magento\Catalog\Api\ProductRepositoryInterface;
10+
use Magento\Catalog\Api\Data\CategoryInterfaceFactory;
11+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
12+
use Magento\Catalog\Model\Product\Type;
13+
use Magento\Catalog\Model\Product\Visibility;
14+
use Magento\Store\Model\Store;
15+
use Magento\TestFramework\Helper\Bootstrap;
16+
17+
$objectManager = Bootstrap::getObjectManager();
18+
$categoryFactory = Bootstrap::getObjectManager()->get(CategoryInterfaceFactory::class);
19+
$productFactory = $objectManager->get(ProductInterfaceFactory::class);
20+
$productRepository = $objectManager->get(ProductRepositoryInterface::class);
21+
22+
$category = $categoryFactory->create();
23+
$category->isObjectNew(true);
24+
$category->setName('Category 999')
25+
->setParentId(2)->setPath('1/2')
26+
->setLevel(2)
27+
->setAvailableSortBy('name')
28+
->setDefaultSortBy('name')
29+
->setIsActive(true)
30+
->setPosition(1)
31+
->setStoreId(Store::DEFAULT_STORE_ID)
32+
->setAvailableSortBy(['position'])
33+
->save();
34+
35+
$product = $productFactory->create();
36+
$product->setTypeId(Type::TYPE_SIMPLE)
37+
->setAttributeSetId($product->getDefaultAttributeSetId())
38+
->setStoreId(Store::DEFAULT_STORE_ID)
39+
->setWebsiteIds([1])
40+
->setName('Simple Product With Price 10')
41+
->setSku('simple1000')
42+
->setPrice(10)
43+
->setWeight(1)
44+
->setStockData(['use_config_manage_stock' => 0])
45+
->setCategoryIds([$category->getId()])
46+
->setVisibility(Visibility::VISIBILITY_BOTH)
47+
->setStatus(Status::STATUS_ENABLED);
48+
$productRepository->save($product);
49+
50+
$product = $productFactory->create();
51+
$product->setTypeId(Type::TYPE_SIMPLE)
52+
->setAttributeSetId($product->getDefaultAttributeSetId())
53+
->setStoreId(Store::DEFAULT_STORE_ID)
54+
->setWebsiteIds([1])
55+
->setName('Simple Product With Price 20')
56+
->setSku('simple1001')
57+
->setPrice(20)
58+
->setWeight(1)
59+
->setStockData(['use_config_manage_stock' => 0])
60+
->setCategoryIds([$category->getId()])
61+
->setVisibility(Visibility::VISIBILITY_BOTH)
62+
->setStatus(Status::STATUS_ENABLED);
63+
$productRepository->save($product);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
use Magento\Catalog\Api\CategoryRepositoryInterface;
9+
use Magento\Catalog\Api\Data\CategoryInterface;
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
12+
use Magento\Framework\Exception\NoSuchEntityException;
13+
use Magento\Framework\Registry;
14+
use Magento\TestFramework\Helper\Bootstrap;
15+
16+
$objectManager = Bootstrap::getObjectManager();
17+
$registry = $objectManager->get(Registry::class);
18+
$registry->unregister('isSecureArea');
19+
$registry->register('isSecureArea', true);
20+
$productRepository = $objectManager->get(ProductRepositoryInterface::class);
21+
$categoryCollectionFactory = $objectManager->get(CollectionFactory::class);
22+
$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class);
23+
try {
24+
$productRepository->deleteById('simple1000');
25+
} catch (NoSuchEntityException $e) {
26+
}
27+
28+
try {
29+
$productRepository->deleteById('simple1001');
30+
} catch (NoSuchEntityException $e) {
31+
}
32+
33+
try {
34+
$categoryCollection = $categoryCollectionFactory->create();
35+
$category = $categoryCollection
36+
->addAttributeToFilter(CategoryInterface::KEY_NAME, 'Category 999')
37+
->setPageSize(1)
38+
->getFirstItem();
39+
$categoryRepository->delete($category);
40+
} catch (NoSuchEntityException $e) {
41+
}
42+
$registry->unregister('isSecureArea');
43+
$registry->register('isSecureArea', false);

dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@
33
* Copyright © Magento, Inc. All rights reserved.
44
* See COPYING.txt for license details.
55
*/
6+
declare(strict_types=1);
67

78
use Magento\Catalog\Setup\CategorySetup;
89
use Magento\Eav\Api\AttributeRepositoryInterface;
910
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
11+
use Magento\Eav\Model\Entity\Attribute\Source\Boolean;
1012
use Magento\TestFramework\Helper\Bootstrap;
1113

1214
$objectManager = Bootstrap::getObjectManager();
1315
/** @var AttributeRepositoryInterface $attributeRepository */
1416
$attributeRepository = $objectManager->get(AttributeRepositoryInterface::class);
1517
/** @var Attribute $attribute */
1618
$attribute = $objectManager->create(Attribute::class);
17-
/** @var $installer \Magento\Catalog\Setup\CategorySetup */
19+
/** @var $installer CategorySetup */
1820
$installer = $objectManager->create(CategorySetup::class);
1921

2022
$attribute->setData(
@@ -37,7 +39,8 @@
3739
'used_in_product_listing' => 1,
3840
'used_for_sort_by' => 0,
3941
'frontend_label' => ['Boolean Attribute'],
40-
'backend_type' => 'int'
42+
'backend_type' => 'int',
43+
'source_model' => Boolean::class
4144
]
4245
);
4346

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\LayeredNavigation\Block\Navigation;
9+
10+
use Magento\Catalog\Api\Data\CategoryInterface;
11+
use Magento\Catalog\Model\ResourceModel\Category as CategoryResource;
12+
use Magento\Catalog\Model\ResourceModel\Category\Collection;
13+
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
14+
use Magento\Framework\ObjectManagerInterface;
15+
use Magento\Framework\View\LayoutInterface;
16+
use Magento\LayeredNavigation\Block\Navigation;
17+
use Magento\Store\Model\Store;
18+
use Magento\TestFramework\Helper\Bootstrap;
19+
use PHPUnit\Framework\TestCase;
20+
21+
/**
22+
* Base class for filters block tests on category page.
23+
*/
24+
abstract class AbstractCategoryTest extends TestCase
25+
{
26+
/**
27+
* @var ObjectManagerInterface
28+
*/
29+
protected $objectManager;
30+
31+
/**
32+
* @var CollectionFactory
33+
*/
34+
protected $categoryCollectionFactory;
35+
36+
/**
37+
* @var CategoryResource
38+
*/
39+
protected $categoryResource;
40+
41+
/**
42+
* @var Navigation
43+
*/
44+
protected $navigationBlock;
45+
46+
/**
47+
* @var LayoutInterface
48+
*/
49+
protected $layout;
50+
51+
/**
52+
* @inheritdoc
53+
*/
54+
protected function setUp()
55+
{
56+
$this->objectManager = Bootstrap::getObjectManager();
57+
$this->categoryCollectionFactory = $this->objectManager->create(CollectionFactory::class);
58+
$this->categoryResource = $this->objectManager->get(CategoryResource::class);
59+
$this->layout = $this->objectManager->create(LayoutInterface::class);
60+
$this->navigationBlock = $this->objectManager->create(Category::class);
61+
parent::setUp();
62+
}
63+
64+
/**
65+
* Inits navigation block.
66+
*
67+
* @param string $categoryName
68+
* @param int $storeId
69+
* @return void
70+
*/
71+
protected function prepareNavigationBlock(
72+
string $categoryName,
73+
int $storeId = Store::DEFAULT_STORE_ID
74+
): void {
75+
$category = $this->loadCategory($categoryName, $storeId);
76+
$this->navigationBlock->getLayer()->setCurrentCategory($category);
77+
$this->navigationBlock->setLayout($this->layout);
78+
}
79+
80+
/**
81+
* Loads category by id.
82+
*
83+
* @param string $categoryName
84+
* @param int $storeId
85+
* @return CategoryInterface
86+
*/
87+
protected function loadCategory(string $categoryName, int $storeId): CategoryInterface
88+
{
89+
/** @var Collection $categoryCollection */
90+
$categoryCollection = $this->categoryCollectionFactory->create();
91+
/** @var CategoryInterface $category */
92+
$category = $categoryCollection->setStoreId($storeId)
93+
->addAttributeToSelect('display_mode', 'left')
94+
->addAttributeToFilter(CategoryInterface::KEY_NAME, $categoryName)
95+
->setPageSize(1)
96+
->getFirstItem();
97+
$category->setStoreId($storeId);
98+
99+
return $category;
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\LayeredNavigation\Block\Navigation\Category;
9+
10+
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
11+
use Magento\Catalog\Api\ProductRepositoryInterface;
12+
use Magento\Catalog\Model\Layer\Filter\AbstractFilter;
13+
use Magento\Catalog\Model\Layer\Filter\Item;
14+
use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor;
15+
use Magento\Framework\Search\Request\Builder;
16+
use Magento\Framework\Search\Request\Config;
17+
use Magento\LayeredNavigation\Block\Navigation\AbstractCategoryTest;
18+
use Magento\Search\Model\Search;
19+
use Magento\Store\Model\Store;
20+
21+
/**
22+
* Base class for custom filters in navigation block on category page.
23+
*/
24+
abstract class AbstractFiltersTest extends AbstractCategoryTest
25+
{
26+
/**
27+
* @var ProductAttributeRepositoryInterface
28+
*/
29+
protected $attributeRepository;
30+
31+
/**
32+
* @var ProductRepositoryInterface
33+
*/
34+
protected $productRepository;
35+
36+
/**
37+
* @inheritdoc
38+
*/
39+
protected function setUp()
40+
{
41+
parent::setUp();
42+
$this->attributeRepository = $this->objectManager->create(ProductAttributeRepositoryInterface::class);
43+
$this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class);
44+
}
45+
46+
/**
47+
* @inheritdoc
48+
*/
49+
protected function prepareNavigationBlock(string $categoryName, int $storeId = Store::DEFAULT_STORE_ID): void
50+
{
51+
$this->objectManager->removeSharedInstance(Config::class);
52+
$this->objectManager->removeSharedInstance(Builder::class);
53+
$this->objectManager->removeSharedInstance(Search::class);
54+
$this->objectManager->create(Processor::class)->reindexAll();
55+
parent::prepareNavigationBlock($categoryName, $storeId);
56+
}
57+
58+
/**
59+
* Returns filter with specified attribute.
60+
*
61+
* @param array $filters
62+
* @param string $code
63+
* @return AbstractFilter|null
64+
*/
65+
protected function getFilterByCode(array $filters, string $code): ?AbstractFilter
66+
{
67+
$filter = array_filter(
68+
$filters,
69+
function (AbstractFilter $filter) use ($code) {
70+
return $filter->getData('attribute_model')
71+
&& $filter->getData('attribute_model')->getAttributeCode() == $code;
72+
}
73+
);
74+
75+
return array_shift($filter);
76+
}
77+
78+
/**
79+
* Updates attribute and products data.
80+
*
81+
* @param string $attributeCode
82+
* @param int $filterable
83+
* @param array $products
84+
* @return void
85+
*/
86+
protected function updateAttributeAndProducts(
87+
string $attributeCode,
88+
int $filterable,
89+
array $products
90+
): void {
91+
$attribute = $this->attributeRepository->get($attributeCode);
92+
$attribute->setData('is_filterable', $filterable);
93+
$this->attributeRepository->save($attribute);
94+
95+
foreach ($products as $productSku => $stringValue) {
96+
$product = $this->productRepository->get($productSku, false, Store::DEFAULT_STORE_ID, true);
97+
$product->addData(
98+
[$attribute->getAttributeCode() => $attribute->getSource()->getOptionId($stringValue)]
99+
);
100+
$this->productRepository->save($product);
101+
}
102+
}
103+
104+
/**
105+
* Returns filter items as array.
106+
*
107+
* @param AbstractFilter $filter
108+
* @return array
109+
*/
110+
protected function prepareFilterItems(AbstractFilter $filter): array
111+
{
112+
$items = [];
113+
/** @var Item $item */
114+
foreach ($filter->getItems() as $item) {
115+
$item = [
116+
'label' => $item->getData('label'),
117+
'count' => $item->getData('count'),
118+
];
119+
$items[] = $item;
120+
}
121+
122+
return $items;
123+
}
124+
}

0 commit comments

Comments
 (0)