Skip to content

Commit b9e28fd

Browse files
committed
Single mutation for adding items to the shopping cart
1 parent 86515dd commit b9e28fd

40 files changed

+2830
-341
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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\Quote\Model\Cart;
9+
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Framework\Exception\NoSuchEntityException;
12+
use Magento\Quote\Api\CartRepositoryInterface;
13+
use Magento\Quote\Api\Data\CartInterface;
14+
use Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder;
15+
use Magento\Quote\Model\Cart\Data\AddProductsToCartOutput;
16+
use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface;
17+
use Magento\Quote\Model\Quote;
18+
use Magento\Framework\Message\MessageInterface;
19+
20+
/**
21+
* @inheritdoc
22+
*/
23+
class AddProductsToCart implements AddProductsToCartInterface
24+
{
25+
/**#@+
26+
* Error message codes
27+
*/
28+
private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND';
29+
private const ERROR_INSUFFICIENT_STOCK = 'INSUFFICIENT_STOCK';
30+
private const ERROR_NOT_SALABLE = 'NOT_SALABLE';
31+
private const ERROR_UNDEFINED = 'UNDEFINED';
32+
/**#@-*/
33+
34+
/**
35+
* List of error messages and codes.
36+
*/
37+
private const MESSAGE_CODES = [
38+
'The required options you selected are not available' => self::ERROR_NOT_SALABLE,
39+
'Product that you are trying to add is not available' => self::ERROR_NOT_SALABLE,
40+
'This product is out of stock' => self::ERROR_NOT_SALABLE,
41+
'There are no source items' => self::ERROR_NOT_SALABLE,
42+
'The fewest you may purchase is' => self::ERROR_INSUFFICIENT_STOCK,
43+
'The most you may purchase is' => self::ERROR_INSUFFICIENT_STOCK,
44+
'The requested qty is not available' => self::ERROR_INSUFFICIENT_STOCK,
45+
];
46+
47+
/**
48+
* @var ProductRepositoryInterface
49+
*/
50+
private $productRepository;
51+
52+
/**
53+
* @var array
54+
*/
55+
private $errors = [];
56+
57+
/**
58+
* @var CartRepositoryInterface
59+
*/
60+
private $cartRepository;
61+
62+
/**
63+
* @var MaskedQuoteIdToQuoteIdInterface
64+
*/
65+
private $maskedQuoteIdToQuoteId;
66+
67+
/**
68+
* @var BuyRequestBuilder
69+
*/
70+
private $requestBuilder;
71+
72+
/**
73+
* @param ProductRepositoryInterface $productRepository
74+
* @param CartRepositoryInterface $cartRepository
75+
* @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId
76+
* @param BuyRequestBuilder $requestBuilder
77+
*/
78+
public function __construct(
79+
ProductRepositoryInterface $productRepository,
80+
CartRepositoryInterface $cartRepository,
81+
MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId,
82+
BuyRequestBuilder $requestBuilder
83+
) {
84+
$this->productRepository = $productRepository;
85+
$this->cartRepository = $cartRepository;
86+
$this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId;
87+
$this->requestBuilder = $requestBuilder;
88+
}
89+
90+
/**
91+
* @inheritdoc
92+
*/
93+
public function execute(string $maskedCartId, array $cartItems): AddProductsToCartOutput
94+
{
95+
$cartId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId);
96+
$cart = $this->cartRepository->get($cartId);
97+
98+
foreach ($cartItems as $n => $cartItem) {
99+
$this->addItemToCart($cart, $cartItem, $n);
100+
}
101+
if ($cart->getData('has_error')) {
102+
$errors = $cart->getErrors();
103+
104+
/** @var MessageInterface $error */
105+
foreach ($errors as $error) {
106+
$this->addError($error->getText());
107+
}
108+
}
109+
$this->cartRepository->save($cart);
110+
111+
return $this->prepareErrorOutput($cart);
112+
}
113+
114+
/**
115+
* Adds a particular item to the shopping cart
116+
*
117+
* @param CartInterface|Quote $cart
118+
* @param Data\CartItem $cartItem
119+
* @param int $cartItemPosition
120+
*/
121+
private function addItemToCart(CartInterface $cart, Data\CartItem $cartItem, int $cartItemPosition): void
122+
{
123+
$sku = $cartItem->getSku();
124+
125+
try {
126+
$product = $this->productRepository->get($sku, false, null, true);
127+
} catch (NoSuchEntityException $e) {
128+
$this->addError(
129+
__('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(),
130+
$cartItemPosition,
131+
self::ERROR_PRODUCT_NOT_FOUND
132+
);
133+
134+
return;
135+
}
136+
137+
try {
138+
$result = $cart->addProduct($product, $this->requestBuilder->build($cartItem));
139+
} catch (\Throwable $e) {
140+
$this->addError(
141+
__(
142+
'Could not add the product with SKU %sku to the shopping cart: %message',
143+
['sku' => $sku, 'message' => $e->getMessage()]
144+
)->render(),
145+
$cartItemPosition
146+
);
147+
return;
148+
}
149+
150+
if (is_string($result)) {
151+
$errors = array_unique(explode("\n", $result));
152+
foreach ($errors as $error) {
153+
$this->addError(__($error)->render(), $cartItemPosition);
154+
}
155+
}
156+
}
157+
158+
/**
159+
* Add order line item error
160+
*
161+
* @param string $message
162+
* @param int $cartItemPosition
163+
* @param string|null $code
164+
* @return void
165+
*/
166+
private function addError(string $message, int $cartItemPosition = 0, string $code = ''): void
167+
{
168+
$this->errors[] = new Data\Error(
169+
$message,
170+
$code ?? $this->getErrorCode($message),
171+
$cartItemPosition
172+
);
173+
}
174+
175+
/**
176+
* Get message error code.
177+
*
178+
* @param string $message
179+
* @return string
180+
*/
181+
private function getErrorCode(string $message): string
182+
{
183+
$code = self::ERROR_UNDEFINED;
184+
185+
$matchedCodes = array_filter(
186+
self::MESSAGE_CODES,
187+
function ($key) use ($message) {
188+
return false !== strpos($message, $key);
189+
},
190+
ARRAY_FILTER_USE_KEY
191+
);
192+
193+
if (!empty($matchedCodes)) {
194+
$code = current($matchedCodes);
195+
}
196+
197+
return $code;
198+
}
199+
200+
/**
201+
* Creates a new output from existing errors
202+
*
203+
* @param CartInterface $cart
204+
* @return AddProductsToCartOutput
205+
*/
206+
private function prepareErrorOutput(CartInterface $cart): AddProductsToCartOutput
207+
{
208+
$output = new AddProductsToCartOutput($cart, $this->errors);
209+
$this->errors = [];
210+
$cart->setHasError(false);
211+
212+
return $output;
213+
}
214+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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\Quote\Model\Cart;
9+
10+
use Magento\Framework\Exception\NoSuchEntityException;
11+
use Magento\Quote\Model\Cart\Data\AddProductsToCartOutput;
12+
13+
/**
14+
* Unified approach to add products to the Shopping Cart.
15+
* Client code must validate, that customer is eligible to call service with provided {cartId} and {cartItems}
16+
*/
17+
interface AddProductsToCartInterface
18+
{
19+
/**
20+
* Add cart items to the cart
21+
*
22+
* @param string $cartId
23+
* @param Data\CartItem[] $cartItems
24+
* @return AddProductsToCartOutput
25+
* @throws NoSuchEntityException Could not find a Cart with provided $maskedCartId
26+
*/
27+
public function execute(string $cartId, array $cartItems): AddProductsToCartOutput;
28+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\Quote\Model\Cart\BuyRequest;
9+
10+
use Magento\Framework\DataObject;
11+
use Magento\Framework\DataObjectFactory;
12+
use Magento\Quote\Model\Cart\Data\CartItem;
13+
14+
/**
15+
* Build buy request for adding products to cart
16+
*/
17+
class BuyRequestBuilder
18+
{
19+
/**
20+
* @var BuyRequestDataProviderInterface[]
21+
*/
22+
private $providers;
23+
24+
/**
25+
* @var DataObjectFactory
26+
*/
27+
private $dataObjectFactory;
28+
29+
/**
30+
* @param DataObjectFactory $dataObjectFactory
31+
* @param array $providers
32+
*/
33+
public function __construct(
34+
DataObjectFactory $dataObjectFactory,
35+
array $providers = []
36+
) {
37+
$this->dataObjectFactory = $dataObjectFactory;
38+
$this->providers = $providers;
39+
}
40+
41+
/**
42+
* Build buy request for adding product to cart
43+
*
44+
* @see \Magento\Quote\Model\Quote::addProduct
45+
* @param CartItem $cartItem
46+
* @return DataObject
47+
*/
48+
public function build(CartItem $cartItem): DataObject
49+
{
50+
$requestData = [
51+
['qty' => $cartItem->getQuantity()]
52+
];
53+
54+
/** @var BuyRequestDataProviderInterface $provider */
55+
foreach ($this->providers as $provider) {
56+
$requestData[] = $provider->execute($cartItem);
57+
}
58+
59+
return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]);
60+
}
61+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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\Quote\Model\Cart\BuyRequest;
9+
10+
use Magento\Quote\Model\Cart\Data\CartItem;
11+
12+
/**
13+
* Provides data for buy request for different types of products
14+
*/
15+
interface BuyRequestDataProviderInterface
16+
{
17+
/**
18+
* Provide buy request data from add to cart item request
19+
*
20+
* @param CartItem $cartItem
21+
* @return array
22+
*/
23+
public function execute(CartItem $cartItem): array;
24+
}

0 commit comments

Comments
 (0)