Skip to content

Commit d190dff

Browse files
Merge branch '2.4-develop' into 2.4.8-graphql-api-enhancements
2 parents ea222bc + 66dea0d commit d190dff

File tree

38 files changed

+1854
-25
lines changed

38 files changed

+1854
-25
lines changed

app/code/Magento/Backend/view/adminhtml/layout/default.xml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<body>
1616
<attribute name="id" value="html-body"/>
1717
<block name="require.js" class="Magento\Backend\Block\Page\RequireJs" template="Magento_Backend::page/js/require_js.phtml"/>
18+
<block class="Magento\Framework\View\Element\Template" name="head.critical" as="head.critical" template="Magento_Backend::page/container.phtml"/>
1819
<block class="Magento\Framework\View\Element\Template" name="head.additional" template="Magento_Backend::page/container.phtml"/>
1920
<referenceContainer name="global.notices">
2021
<block class="Magento\Backend\Block\Page\Notices" name="global_notices" as="global_notices" template="Magento_Backend::page/notices.phtml"/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2024 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ************************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\Catalog\Model\Category;
20+
21+
use Magento\Catalog\Model\Category;
22+
use Magento\Framework\App\Cache\Tag\StrategyInterface;
23+
use Magento\Framework\Model\AbstractModel;
24+
use Magento\Widget\Model\Widget\Instance;
25+
26+
/**
27+
* Get additional layout cache tag for category layout.
28+
*/
29+
class LayoutCacheTagResolver implements StrategyInterface
30+
{
31+
/**
32+
* @inheritDoc
33+
*/
34+
public function getTags($object)
35+
{
36+
if ($this->isExistingCategoryLayoutChange($object)) {
37+
return [
38+
str_replace('{{ID}}', (string) $object->getId(), Instance::SINGLE_CATEGORY_LAYOUT_HANDLE)
39+
];
40+
}
41+
return [];
42+
}
43+
44+
/**
45+
* Check if existing category page layout change
46+
*
47+
* @param Category $object
48+
* @return bool
49+
*/
50+
private function isExistingCategoryLayoutChange(Category $object): bool
51+
{
52+
return !$object->isObjectNew() && $this->isObjectChanged($object);
53+
}
54+
55+
/**
56+
* Check if the page layout of the given category is changed
57+
*
58+
* @param AbstractModel $object
59+
* @return bool
60+
*/
61+
private function isObjectChanged(AbstractModel $object): bool
62+
{
63+
$isChanged = false;
64+
$objectNewPageLayout = $object->getData('page_layout');
65+
$objectOldPageLayout = $object->getOrigData('page_layout');
66+
if ($objectNewPageLayout !== 'empty' &&
67+
$objectNewPageLayout !== $objectOldPageLayout
68+
) {
69+
$isChanged = true;
70+
}
71+
return $isChanged;
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2024 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ************************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\Catalog\Model\Product;
20+
21+
use Magento\Catalog\Model\Product;
22+
use Magento\Framework\App\Cache\Tag\StrategyInterface;
23+
use Magento\Framework\Model\AbstractModel;
24+
use Magento\Widget\Model\Widget\Instance;
25+
26+
/**
27+
* Get additional layout cache tag for product layout.
28+
*/
29+
class LayoutCacheTagResolver implements StrategyInterface
30+
{
31+
/**
32+
* @inheritDoc
33+
*/
34+
public function getTags($object)
35+
{
36+
if ($this->isExistingProductLayoutChange($object)) {
37+
return [
38+
str_replace('{{ID}}', (string) $object->getId(), Instance::SINGLE_PRODUCT_LAYOUT_HANDLE)
39+
];
40+
}
41+
return [];
42+
}
43+
44+
/**
45+
* Check if existing Product page layout change
46+
*
47+
* @param Product $object
48+
* @return bool
49+
*/
50+
private function isExistingProductLayoutChange(Product $object): bool
51+
{
52+
return !$object->isObjectNew() && $this->isObjectChanged($object);
53+
}
54+
55+
/**
56+
* Check if the page layout of the given product is changed
57+
*
58+
* @param AbstractModel $object
59+
* @return bool
60+
*/
61+
private function isObjectChanged(AbstractModel $object): bool
62+
{
63+
$isChanged = false;
64+
$objectNewPageLayout = $object->getData('page_layout');
65+
$objectOldPageLayout = $object->getOrigData('page_layout');
66+
if ($objectNewPageLayout !== 'empty' &&
67+
$objectNewPageLayout !== $objectOldPageLayout
68+
) {
69+
$isChanged = true;
70+
}
71+
return $isChanged;
72+
}
73+
}

app/code/Magento/Catalog/Model/ProductRepository.php

+7
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,13 @@ public function save(ProductInterface $product, $saveOptions = false)
526526
$assignToCategories = false;
527527
$tierPrices = $product->getData('tier_price');
528528
$productDataToChange = $product->getData();
529+
530+
if (!$product->getSku()) {
531+
throw new CouldNotSaveException(
532+
__("The \"%1\" attribute value is empty. Set the attribute and try again.", "sku")
533+
);
534+
}
535+
529536
try {
530537
$existingProduct = $product->getId() ?
531538
$this->getById($product->getId()) :

app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php

+14
Original file line numberDiff line numberDiff line change
@@ -1568,4 +1568,18 @@ public function testSaveExistingWithMediaGalleryEntries(): void
15681568
$this->model->save($this->product);
15691569
$this->assertEquals($expectedResult, $this->initializedProduct->getMediaGallery('images'));
15701570
}
1571+
1572+
/**
1573+
* @return void
1574+
*/
1575+
public function testSaveCouldNotSaveException(): void
1576+
{
1577+
$this->expectException(\Magento\Framework\Exception\CouldNotSaveException::class);
1578+
$productData = [
1579+
'name' => 'Simple Product',
1580+
'price' => 100
1581+
];
1582+
$this->product->setData($productData);
1583+
$this->model->save($this->product);
1584+
}
15711585
}

app/code/Magento/Catalog/etc/di.xml

+12
Original file line numberDiff line numberDiff line change
@@ -1359,4 +1359,16 @@
13591359
</argument>
13601360
</arguments>
13611361
</type>
1362+
<type name="Magento\Theme\Model\LayoutCacheTagResolverFactory">
1363+
<arguments>
1364+
<argument name="cacheTagsResolvers" xsi:type="array">
1365+
<item name="Magento\Catalog\Model\Product" xsi:type="object">
1366+
Magento\Catalog\Model\Product\LayoutCacheTagResolver
1367+
</item>
1368+
<item name="Magento\Catalog\Model\Category" xsi:type="object">
1369+
Magento\Catalog\Model\Category\LayoutCacheTagResolver
1370+
</item>
1371+
</argument>
1372+
</arguments>
1373+
</type>
13621374
</config>

app/code/Magento/Catalog/i18n/en_US.csv

+1
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ Category,Category
319319
"Please enter a number 0 or greater in this field.","Please enter a number 0 or greater in this field."
320320
"The value of attribute ""%1"" must be set","The value of attribute ""%1"" must be set"
321321
"The "%1" attribute value is empty.","The "%1" attribute value is empty."
322+
"The ""%1"" attribute value is empty. Set the attribute and try again.","The ""%1"" attribute value is empty. Set the attribute and try again."
322323
"SKU length should be %1 characters maximum.","SKU length should be %1 characters maximum."
323324
"Please enter a valid number in this field.","Please enter a valid number in this field."
324325
"We found a duplicate website, tier price, customer group and quantity.","We found a duplicate website, tier price, customer group and quantity."

app/code/Magento/Checkout/Controller/Cart/Add.php

+14-8
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
*/
77
namespace Magento\Checkout\Controller\Cart;
88

9-
use Magento\Checkout\Model\Cart\RequestQuantityProcessor;
10-
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
119
use Magento\Catalog\Api\ProductRepositoryInterface;
10+
use Magento\Checkout\Model\AddProductToCart;
1211
use Magento\Checkout\Model\Cart as CustomerCart;
12+
use Magento\Checkout\Model\Cart\RequestQuantityProcessor;
13+
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
1314
use Magento\Framework\App\ObjectManager;
1415
use Magento\Framework\App\ResponseInterface;
1516
use Magento\Framework\Controller\ResultInterface;
@@ -33,6 +34,11 @@ class Add extends \Magento\Checkout\Controller\Cart implements HttpPostActionInt
3334
*/
3435
private $quantityProcessor;
3536

37+
/**
38+
* @var AddProductToCart
39+
*/
40+
private AddProductToCart $addProductToCart;
41+
3642
/**
3743
* @param \Magento\Framework\App\Action\Context $context
3844
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
@@ -42,6 +48,7 @@ class Add extends \Magento\Checkout\Controller\Cart implements HttpPostActionInt
4248
* @param CustomerCart $cart
4349
* @param ProductRepositoryInterface $productRepository
4450
* @param RequestQuantityProcessor|null $quantityProcessor
51+
* @param AddProductToCart|null $addProductToCart
4552
* @codeCoverageIgnore
4653
*/
4754
public function __construct(
@@ -52,7 +59,8 @@ public function __construct(
5259
\Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator,
5360
CustomerCart $cart,
5461
ProductRepositoryInterface $productRepository,
55-
?RequestQuantityProcessor $quantityProcessor = null
62+
?RequestQuantityProcessor $quantityProcessor = null,
63+
?AddProductToCart $addProductToCart = null
5664
) {
5765
parent::__construct(
5866
$context,
@@ -65,6 +73,8 @@ public function __construct(
6573
$this->productRepository = $productRepository;
6674
$this->quantityProcessor = $quantityProcessor
6775
?? ObjectManager::getInstance()->get(RequestQuantityProcessor::class);
76+
$this->addProductToCart = $addProductToCart
77+
?? ObjectManager::getInstance()->get(AddProductToCart::class);
6878
}
6979

7080
/**
@@ -123,11 +133,7 @@ public function execute()
123133
return $this->goBack();
124134
}
125135

126-
$this->cart->addProduct($product, $params);
127-
if (!empty($related)) {
128-
$this->cart->addProductsByIds(explode(',', $related));
129-
}
130-
$this->cart->save();
136+
$this->addProductToCart->execute($this->cart, $product, $params, $related ? explode(',', $related) : []);
131137

132138
/**
133139
* @todo remove wishlist observer \Magento\Wishlist\Observer\AddToCart
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
/**
3+
* Copyright 2024 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Checkout\Model;
9+
10+
use Magento\Catalog\Model\Product;
11+
use Magento\Framework\Stdlib\DateTime\DateTime;
12+
use Magento\Quote\Api\CartRepositoryInterfaceFactory;
13+
use Magento\Quote\Api\Data\CartInterface;
14+
use Magento\Quote\Model\QuoteMutexInterface;
15+
16+
class AddProductToCart
17+
{
18+
/**
19+
* @param QuoteMutexInterface $quoteMutex
20+
* @param CartRepositoryInterfaceFactory $quoteRepositoryFactory
21+
* @param DateTime $dateTime
22+
*/
23+
public function __construct(
24+
private readonly QuoteMutexInterface $quoteMutex,
25+
private readonly CartRepositoryInterfaceFactory $quoteRepositoryFactory,
26+
private readonly DateTime $dateTime
27+
) {
28+
}
29+
30+
/**
31+
* Add product to cart
32+
*
33+
* @param Cart $cart
34+
* @param Product $product
35+
* @param array $buyRequest Buy request info
36+
* @param array $related Product IDs to add to the cart along with the main product
37+
* @return bool
38+
* @throws \Throwable
39+
*/
40+
public function execute(Cart $cart, Product $product, array $buyRequest = [], array $related = []): bool
41+
{
42+
if (!$cart->getQuote()->getId()) {
43+
return $this->add($cart, $product, $buyRequest, $related);
44+
}
45+
46+
return $this->quoteMutex->execute(
47+
[(int) $cart->getQuote()->getId()],
48+
function (array $quotes = []) use ($cart, $product, $buyRequest, $related) {
49+
$reload = true;
50+
// check if the mutex provided the quote
51+
if (!empty($quotes)) {
52+
// check if the quote was updated since the last load
53+
// if not, we can use the quote in memory to avoid full reload which is expensive and unnecessary
54+
$lastUpdatedAt = $cart->getQuote()->getUpdatedAt()
55+
?: $cart->getQuote()->getOrigData(CartInterface::KEY_UPDATED_AT);
56+
$quote = current($quotes);
57+
$updatedAt = $quote->getUpdatedAt();
58+
$reload = $updatedAt
59+
&& $lastUpdatedAt
60+
&& $this->dateTime->timestamp($updatedAt) > $this->dateTime->timestamp($lastUpdatedAt);
61+
}
62+
if ($reload) {
63+
// bypass repository cache by creating a new repository instead of using the shared repository
64+
$quote = $this->quoteRepositoryFactory->create()->getActive($cart->getQuote()->getId());
65+
$cart->setQuote($quote);
66+
$cart->getCheckoutSession()->replaceQuote($quote);
67+
}
68+
return $this->add($cart, $product, $buyRequest, $related);
69+
},
70+
);
71+
}
72+
73+
/**
74+
* Add product to cart
75+
*
76+
* @param Cart $cart
77+
* @param Product $product
78+
* @param array $buyRequest
79+
* @param array $related
80+
* @return bool
81+
*/
82+
private function add(Cart $cart, Product $product, array $buyRequest, array $related = []): bool
83+
{
84+
$cart->addProduct($product, $buyRequest);
85+
if (!empty($related)) {
86+
$cart->addProductsByIds($related);
87+
}
88+
$cart->save();
89+
return true;
90+
}
91+
}

0 commit comments

Comments
 (0)