Skip to content

Commit 9177877

Browse files
committed
Merge remote-tracking branch 'upstream/2.4-develop' into B2B-1876
2 parents 74a1065 + 6df210e commit 9177877

File tree

47 files changed

+1923
-112
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1923
-112
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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\Bundle\Model\Product;
9+
10+
use Magento\Framework\EntityManager\MetadataPool;
11+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
12+
use Magento\Bundle\Model\ResourceModel\Selection as BundleSelection;
13+
use Magento\Store\Model\StoreManagerInterface;
14+
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
15+
use Magento\Catalog\Model\Product;
16+
use Magento\Catalog\Api\Data\ProductInterface;
17+
18+
/**
19+
* Class to return ids of options and child products when all products in required option are disabled in bundle product
20+
*/
21+
class SelectionProductsDisabledRequired
22+
{
23+
/**
24+
* @var BundleSelection
25+
*/
26+
private $bundleSelection;
27+
28+
/**
29+
* @var StoreManagerInterface
30+
*/
31+
private $storeManager;
32+
33+
/**
34+
* @var Status
35+
*/
36+
private $catalogProductStatus;
37+
38+
/**
39+
* @var ProductCollectionFactory
40+
*/
41+
private $productCollectionFactory;
42+
43+
/**
44+
* @var MetadataPool
45+
*/
46+
private $metadataPool;
47+
48+
/**
49+
* @var string
50+
*/
51+
private $hasStockStatusFilter = 'has_stock_status_filter';
52+
53+
/**
54+
* @var array
55+
*/
56+
private $productsDisabledRequired = [];
57+
58+
/**
59+
* @param BundleSelection $bundleSelection
60+
* @param StoreManagerInterface $storeManager
61+
* @param Status $catalogProductStatus
62+
* @param ProductCollectionFactory $productCollectionFactory
63+
* @param MetadataPool $metadataPool
64+
*/
65+
public function __construct(
66+
BundleSelection $bundleSelection,
67+
StoreManagerInterface $storeManager,
68+
Status $catalogProductStatus,
69+
ProductCollectionFactory $productCollectionFactory,
70+
MetadataPool $metadataPool
71+
) {
72+
$this->bundleSelection = $bundleSelection;
73+
$this->storeManager = $storeManager;
74+
$this->catalogProductStatus = $catalogProductStatus;
75+
$this->productCollectionFactory = $productCollectionFactory;
76+
$this->metadataPool = $metadataPool;
77+
}
78+
79+
/**
80+
* Return ids of options and child products when all products in required option are disabled in bundle product
81+
*
82+
* @param int $bundleId
83+
* @param int|null $websiteId
84+
* @return array
85+
*/
86+
public function getChildProductIds(int $bundleId, ?int $websiteId = null): array
87+
{
88+
if (!$websiteId) {
89+
$websiteId = (int)$this->storeManager->getStore()->getWebsiteId();
90+
}
91+
$cacheKey = $this->getCacheKey($bundleId, $websiteId);
92+
if (isset($this->productsDisabledRequired[$cacheKey])) {
93+
return $this->productsDisabledRequired[$cacheKey];
94+
}
95+
$selectionProductIds = $this->bundleSelection->getChildrenIds($bundleId);
96+
/** for cases when no required products found */
97+
if (count($selectionProductIds) === 1 && isset($selectionProductIds[0])) {
98+
$this->productsDisabledRequired[$cacheKey] = [];
99+
return $this->productsDisabledRequired[$cacheKey];
100+
}
101+
$products = $this->getProducts($selectionProductIds, $websiteId);
102+
if (!$products) {
103+
$this->productsDisabledRequired[$cacheKey] = [];
104+
return $this->productsDisabledRequired[$cacheKey];
105+
}
106+
foreach ($selectionProductIds as $optionId => $optionProductIds) {
107+
foreach ($optionProductIds as $productId) {
108+
if (isset($products[$productId])) {
109+
/** @var Product $product */
110+
$product = $products[$productId];
111+
if (in_array($product->getStatus(), $this->catalogProductStatus->getVisibleStatusIds())) {
112+
unset($selectionProductIds[$optionId]);
113+
}
114+
}
115+
}
116+
}
117+
$this->productsDisabledRequired[$cacheKey] = $selectionProductIds;
118+
return $this->productsDisabledRequired[$cacheKey];
119+
}
120+
121+
/**
122+
* Get products objects
123+
*
124+
* @param array $selectionProductIds
125+
* @param int $websiteId
126+
* @return ProductInterface[]
127+
*/
128+
private function getProducts(array $selectionProductIds, int $websiteId): array
129+
{
130+
$productIds = [];
131+
$defaultStoreId = $this->storeManager->getWebsite($websiteId)->getDefaultStore()->getId();
132+
foreach ($selectionProductIds as $optionProductIds) {
133+
$productIds = array_merge($productIds, $optionProductIds);
134+
}
135+
$productCollection = $this->productCollectionFactory->create();
136+
$productCollection->joinAttribute(
137+
ProductInterface::STATUS,
138+
Product::ENTITY . '/' . ProductInterface::STATUS,
139+
$this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(),
140+
null,
141+
'inner',
142+
$defaultStoreId
143+
);
144+
$productCollection->addIdFilter($productIds);
145+
$productCollection->addStoreFilter($defaultStoreId);
146+
$productCollection->setFlag($this->hasStockStatusFilter, true);
147+
return $productCollection->getItems();
148+
}
149+
150+
/**
151+
* Get cache key
152+
*
153+
* @param int $bundleId
154+
* @param int $websiteId
155+
* @return string
156+
*/
157+
private function getCacheKey(int $bundleId, int $websiteId): string
158+
{
159+
return $bundleId . '-' . $websiteId;
160+
}
161+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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\Bundle\Model\ResourceModel\Indexer\Price;
9+
10+
use Magento\Bundle\Model\Product\SelectionProductsDisabledRequired;
11+
use Magento\Catalog\Model\Product\Type;
12+
use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure;
13+
use Magento\Framework\App\ResourceConnection;
14+
use Magento\Catalog\Model\Config;
15+
use Magento\Framework\EntityManager\MetadataPool;
16+
use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceModifierInterface;
17+
use Magento\Bundle\Model\ResourceModel\Selection as BundleSelection;
18+
19+
/**
20+
* Remove bundle product from price index when all products in required option are disabled
21+
*/
22+
class DisabledProductOptionPriceModifier implements PriceModifierInterface
23+
{
24+
/**
25+
* @var ResourceConnection
26+
*/
27+
private $resourceConnection;
28+
29+
/**
30+
* @var SelectionProductsDisabledRequired
31+
*/
32+
private $selectionProductsDisabledRequired;
33+
34+
/**
35+
* @var array
36+
*/
37+
private $isBundle = [];
38+
39+
/**
40+
* @var array
41+
*/
42+
private $websiteIdsOfProduct = [];
43+
44+
/**
45+
* @param ResourceConnection $resourceConnection
46+
* @param Config $config
47+
* @param MetadataPool $metadataPool
48+
* @param BundleSelection $bundleSelection
49+
* @param SelectionProductsDisabledRequired $selectionProductsDisabledRequired
50+
*/
51+
public function __construct(
52+
ResourceConnection $resourceConnection,
53+
Config $config,
54+
MetadataPool $metadataPool,
55+
BundleSelection $bundleSelection,
56+
SelectionProductsDisabledRequired $selectionProductsDisabledRequired
57+
) {
58+
$this->resourceConnection = $resourceConnection;
59+
$this->config = $config;
60+
$this->metadataPool = $metadataPool;
61+
$this->bundleSelection = $bundleSelection;
62+
$this->selectionProductsDisabledRequired = $selectionProductsDisabledRequired;
63+
}
64+
65+
/**
66+
* Remove bundle product from price index when all products in required option are disabled
67+
*
68+
* @param IndexTableStructure $priceTable
69+
* @param array $entityIds
70+
* @return void
71+
* @throws \Magento\Framework\Exception\LocalizedException
72+
*/
73+
public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = []) : void
74+
{
75+
foreach ($entityIds as $entityId) {
76+
$entityId = (int) $entityId;
77+
if (!$this->isBundle($entityId)) {
78+
continue;
79+
}
80+
foreach ($this->getWebsiteIdsOfProduct($entityId) as $websiteId) {
81+
$productIdsDisabledRequired = $this->selectionProductsDisabledRequired
82+
->getChildProductIds($entityId, (int)$websiteId);
83+
if ($productIdsDisabledRequired) {
84+
$connection = $this->resourceConnection->getConnection('indexer');
85+
$select = $connection->select();
86+
$select->from(['price_index' => $priceTable->getTableName()], []);
87+
$priceEntityField = $priceTable->getEntityField();
88+
$select->where('price_index.website_id = ?', $websiteId);
89+
$select->where("price_index.{$priceEntityField} = ?", $entityId);
90+
$query = $select->deleteFromSelect('price_index');
91+
$connection->query($query);
92+
}
93+
}
94+
}
95+
}
96+
97+
/**
98+
* Get all website ids of product
99+
*
100+
* @param int $entityId
101+
* @return array
102+
*/
103+
private function getWebsiteIdsOfProduct(int $entityId): array
104+
{
105+
if (isset($this->websiteIdsOfProduct[$entityId])) {
106+
return $this->websiteIdsOfProduct[$entityId];
107+
}
108+
$connection = $this->resourceConnection->getConnection('indexer');
109+
$select = $connection->select();
110+
$select->from(
111+
['product_in_websites' => $this->resourceConnection->getTableName('catalog_product_website')],
112+
['website_id']
113+
)->where('product_in_websites.product_id = ?', $entityId);
114+
foreach ($connection->fetchCol($select) as $websiteId) {
115+
$this->websiteIdsOfProduct[$entityId][] = (int)$websiteId;
116+
}
117+
return $this->websiteIdsOfProduct[$entityId];
118+
}
119+
120+
/**
121+
* Is product bundle
122+
*
123+
* @param int $entityId
124+
* @return bool
125+
*/
126+
private function isBundle(int $entityId): bool
127+
{
128+
if (isset($this->isBundle[$entityId])) {
129+
return $this->isBundle[$entityId];
130+
}
131+
$connection = $this->resourceConnection->getConnection('indexer');
132+
$select = $connection->select();
133+
$select->from(
134+
['cpe' => $this->resourceConnection->getTableName('catalog_product_entity')],
135+
['type_id']
136+
)->where('cpe.entity_id = ?', $entityId);
137+
$typeId = $connection->fetchOne($select);
138+
$this->isBundle[$entityId] = $typeId === Type::TYPE_BUNDLE;
139+
return $this->isBundle[$entityId];
140+
}
141+
}

app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection/FilterApplier.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class FilterApplier
2020
*/
2121
private $conditionTypesMap = [
2222
'eq' => ' = ?',
23-
'in' => 'IN (?)'
23+
'in' => ' IN (?)'
2424
];
2525

2626
/**
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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\Bundle\Plugin\Catalog\Helper;
9+
10+
use Magento\Catalog\Model\Product as ProductModel;
11+
use Magento\Catalog\Model\Product\Type;
12+
use Magento\Catalog\Helper\Product as Subject;
13+
use Magento\Bundle\Model\Product\SelectionProductsDisabledRequired;
14+
use Magento\Framework\App\Config\ScopeConfigInterface;
15+
use Magento\CatalogInventory\Model\Configuration;
16+
use Magento\Store\Model\ScopeInterface;
17+
use Magento\Catalog\Api\ProductRepositoryInterface;
18+
19+
/**
20+
* Plugin to not show bundle product when all products in required option are disabled
21+
*/
22+
class Product
23+
{
24+
/**
25+
* @var SelectionProductsDisabledRequired
26+
*/
27+
private $selectionProductsDisabledRequired;
28+
29+
/**
30+
* @var ScopeConfigInterface
31+
*/
32+
private $scopeConfig;
33+
34+
/**
35+
* @var ProductRepositoryInterface
36+
*/
37+
private $productRepository;
38+
39+
/**
40+
* @param SelectionProductsDisabledRequired $selectionProductsDisabledRequired
41+
* @param ScopeConfigInterface $scopeConfig
42+
* @param ProductRepositoryInterface $productRepository
43+
*/
44+
public function __construct(
45+
SelectionProductsDisabledRequired $selectionProductsDisabledRequired,
46+
ScopeConfigInterface $scopeConfig,
47+
ProductRepositoryInterface $productRepository
48+
) {
49+
$this->selectionProductsDisabledRequired = $selectionProductsDisabledRequired;
50+
$this->scopeConfig = $scopeConfig;
51+
$this->productRepository = $productRepository;
52+
}
53+
54+
/**
55+
* Do not show bundle product when all products in required option are disabled
56+
*
57+
* @param Subject $subject
58+
* @param bool $result
59+
* @param ProductModel|int $product
60+
* @return bool
61+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
62+
*/
63+
public function afterCanShow(Subject $subject, $result, $product)
64+
{
65+
if (is_int($product)) {
66+
$product = $this->productRepository->getById($product);
67+
}
68+
$productId = (int)$product->getEntityId();
69+
if ($result == false || $product->getTypeId() !== Type::TYPE_BUNDLE) {
70+
return $result;
71+
}
72+
$isShowOutOfStock = $this->scopeConfig->getValue(
73+
Configuration::XML_PATH_SHOW_OUT_OF_STOCK,
74+
ScopeInterface::SCOPE_STORE
75+
);
76+
if ($isShowOutOfStock) {
77+
return $result;
78+
}
79+
$productIdsDisabledRequired = $this->selectionProductsDisabledRequired->getChildProductIds($productId);
80+
return $productIdsDisabledRequired ? false : $result;
81+
}
82+
}

0 commit comments

Comments
 (0)