Skip to content
This repository has been archived by the owner on Dec 19, 2019. It is now read-only.

#141 add simple product to cart #170

Merged
merged 22 commits into from
Oct 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
eea5cdd
#141 Added add simple product to cart mutation to quote-graphql module
Sep 7, 2018
50a928c
#141 Added required configurations
Sep 7, 2018
5105989
#141 Added hard dependencies
Sep 7, 2018
1734dfc
#141 Fixed issues from Travis
Sep 7, 2018
7e7e82c
#141 Running tests again
Sep 7, 2018
01404a0
#141 Added descriptions to classes and methods
Sep 17, 2018
1064854
#141 Performed decoupling of the resolvers
Sep 18, 2018
34e0106
#141 Added missed lines in phpdocs
Sep 19, 2018
e8011a8
#141 Removed redundant dependency
Sep 19, 2018
436dee7
#141 Restored config after patching the branch
Sep 20, 2018
4942c6f
#141 Decoupling the CustomOption data provider
Sep 20, 2018
e901431
Merge remote-tracking branch 'origin/2.3-develop' into 141-add-simple…
Sep 25, 2018
7990b83
GraphQL-141: [Mutations] Cart Operations > Add simple product to Cart
Sep 26, 2018
ada51dc
GraphQL-141: [Mutations] Cart Operations > Add simple product to Cart
Sep 26, 2018
484c65c
GraphQL-141: [Mutations] Cart Operations > Add simple product to Cart
Sep 26, 2018
6b7300a
GraphQL-141: [Mutations] Cart Operations > Add simple product to Cart
Sep 26, 2018
c21a016
GraphQL-141: [Mutations] Cart Operations > Add simple product to Cart
Sep 26, 2018
d39230e
GraphQL-141: [Mutations] Cart Operations > Add simple product to Cart
Sep 26, 2018
d43164b
GraphQL-141: [Mutations] Cart Operations > Add simple product to Cart
Oct 1, 2018
fa5940a
Merge remote-tracking branch 'origin/2.3-develop' into 141-add-simple…
Oct 1, 2018
c78b993
GraphQL-141: [Mutations] Cart Operations > Add simple product to Cart
Oct 2, 2018
7e3c023
GraphQL-141: [Mutations] Cart Operations > Add simple product to Cart
Oct 2, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\CatalogGraphQl\Model\Product\Option;

use Magento\Catalog\Model\Product\Option\Type\Date as ProductDateOptionType;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Stdlib\DateTime;

/**
* @inheritdoc
*/
class DateType extends ProductDateOptionType
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this one needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason why it's needed is ProductDateOptionType wants to see date value as an associate array (like ['year' => '2018', 'month' => '1', 'day' => '1']). By this changes, we allow to use string as a date value. This meets our schema shape.
The same approach is used in REST API.

{
/**
* Make valid string as a value of date option type for GraphQl queries
*
* @param array $values All product option values, i.e. array (option_id => mixed, option_id => mixed...)
* @return ProductDateOptionType
*/
public function validateUserValue($values)
{
if ($this->_dateExists() || $this->_timeExists()) {
return parent::validateUserValue($this->formatValues($values));
}

return $this;
}

/**
* Format date value from string to date array
*
* @param [] $values
* @return []
* @throws LocalizedException
*/
private function formatValues($values)
{
if (isset($values[$this->getOption()->getId()])) {
$value = $values[$this->getOption()->getId()];
$dateTime = \DateTime::createFromFormat(DateTime::DATETIME_PHP_FORMAT, $value);
$values[$this->getOption()->getId()] = [
'date' => $value,
'year' => $dateTime->format('Y'),
'month' => $dateTime->format('m'),
'day' => $dateTime->format('d'),
'hour' => $dateTime->format('H'),
'minute' => $dateTime->format('i'),
'day_part' => $dateTime->format('a'),
];
}

return $values;
}

/**
* @inheritdoc
*/
public function useCalendar()
{
return false;
}
}
1 change: 1 addition & 0 deletions app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Magento\Catalog\Model\Product\Option\Type\Date" type="Magento\CatalogGraphQl\Model\Product\Option\DateType" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks risky because we override the default behavior of catalog class. Why is this needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paliarush It's needed for API purpose only (that's why it's defined in scope of graphql area) to bypass requirement for date values which Magento has on the frontend (date value should be an array). So by this, we made it possible to receive and validate data value in string format.

Does it make sense?

<type name="Magento\CatalogGraphQl\Model\ProductInterfaceTypeResolverComposite">
<arguments>
<argument name="productTypeNameResolvers" xsi:type="array">
Expand Down
138 changes: 138 additions & 0 deletions app/code/Magento/QuoteGraphQl/Model/Cart/AddProductsToCart.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\QuoteGraphQl\Model\Cart;

use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Framework\Message\AbstractMessage;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface;
use Magento\Quote\Model\Quote;
use Magento\QuoteGraphQl\Model\Authorization\IsCartMutationAllowedForCurrentUser;

/**
* Add products to cart
*/
class AddProductsToCart
{
/**
* @var MaskedQuoteIdToQuoteIdInterface
*/
private $maskedQuoteIdToQuoteId;

/**
* @var CartRepositoryInterface
*/
private $cartRepository;

/**
* @var IsCartMutationAllowedForCurrentUser
*/
private $isCartMutationAllowedForCurrentUser;

/**
* @var AddSimpleProductToCart
*/
private $addProductToCart;

/**
* @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId
* @param CartRepositoryInterface $cartRepository
* @param IsCartMutationAllowedForCurrentUser $isCartMutationAllowedForCurrentUser
* @param AddSimpleProductToCart $addProductToCart
*/
public function __construct(
MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId,
CartRepositoryInterface $cartRepository,
IsCartMutationAllowedForCurrentUser $isCartMutationAllowedForCurrentUser,
AddSimpleProductToCart $addProductToCart
) {
$this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId;
$this->cartRepository = $cartRepository;
$this->isCartMutationAllowedForCurrentUser = $isCartMutationAllowedForCurrentUser;
$this->addProductToCart = $addProductToCart;
}

/**
* Add products to cart
*
* @param string $cartHash
* @param array $cartItems
* @return Quote
* @throws GraphQlInputException
*/
public function execute(string $cartHash, array $cartItems): Quote
{
$cart = $this->getCart($cartHash);

foreach ($cartItems as $cartItemData) {
$this->addProductToCart->execute($cart, $cartItemData);
}

if ($cart->getData('has_error')) {
throw new GraphQlInputException(
__('Shopping cart error: %message', ['message' => $this->getCartErrors($cart)])
);
}

$this->cartRepository->save($cart);
return $cart;
}

/**
* Get cart
*
* @param string $cartHash
* @return Quote
* @throws GraphQlNoSuchEntityException
* @throws GraphQlAuthorizationException
*/
private function getCart(string $cartHash): Quote
{
try {
$cartId = $this->maskedQuoteIdToQuoteId->execute($cartHash);
$cart = $this->cartRepository->get($cartId);
} catch (NoSuchEntityException $e) {
throw new GraphQlNoSuchEntityException(
__('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $cartHash])
);
}

if (false === $this->isCartMutationAllowedForCurrentUser->execute($cartId)) {
throw new GraphQlAuthorizationException(
__(
'The current user cannot perform operations on cart "%masked_cart_id"',
['masked_cart_id' => $cartHash]
)
);
}

/** @var Quote $cart */
return $cart;
}

/**
* Collecting cart errors
*
* @param Quote $cart
* @return string
*/
private function getCartErrors(Quote $cart): string
{
$errorMessages = [];

/** @var AbstractMessage $error */
foreach ($cart->getErrors() as $error) {
$errorMessages[] = $error->getText();
}

return implode(PHP_EOL, $errorMessages);
}
}
149 changes: 149 additions & 0 deletions app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\QuoteGraphQl\Model\Cart;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\DataObject;
use Magento\Framework\DataObjectFactory;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Quote\Model\Quote;

/**
* Add simple product to cart
*
* TODO: should be replaced for different types resolver
*/
class AddSimpleProductToCart
{
/**
* @var ArrayManager
*/
private $arrayManager;

/**
* @var DataObjectFactory
*/
private $dataObjectFactory;

/**
* @var ProductRepositoryInterface
*/
private $productRepository;

/**
* @param ArrayManager $arrayManager
* @param DataObjectFactory $dataObjectFactory
* @param ProductRepositoryInterface $productRepository
*/
public function __construct(
ArrayManager $arrayManager,
DataObjectFactory $dataObjectFactory,
ProductRepositoryInterface $productRepository
) {
$this->arrayManager = $arrayManager;
$this->dataObjectFactory = $dataObjectFactory;
$this->productRepository = $productRepository;
}

/**
* Add simple product to cart
*
* @param Quote $cart
* @param array $cartItemData
* @return void
* @throws GraphQlNoSuchEntityException
* @throws GraphQlInputException
*/
public function execute(Quote $cart, array $cartItemData): void
{
$sku = $this->extractSku($cartItemData);
$qty = $this->extractQty($cartItemData);
$customizableOptions = $this->extractCustomizableOptions($cartItemData);

try {
$product = $this->productRepository->get($sku);
} catch (NoSuchEntityException $e) {
throw new GraphQlNoSuchEntityException(__('Could not find a product with SKU "%sku"', ['sku' => $sku]));
}

$result = $cart->addProduct($product, $this->createBuyRequest($qty, $customizableOptions));

if (is_string($result)) {
throw new GraphQlInputException(__($result));
}
}

/**
* Extract SKU from cart item data
*
* @param array $cartItemData
* @return string
* @throws GraphQlInputException
*/
private function extractSku(array $cartItemData): string
{
$sku = $this->arrayManager->get('data/sku', $cartItemData);
if (!isset($sku)) {
throw new GraphQlInputException(__('Missing key "sku" in cart item data'));
}
return (string)$sku;
}

/**
* Extract Qty from cart item data
*
* @param array $cartItemData
* @return float
* @throws GraphQlInputException
*/
private function extractQty(array $cartItemData): float
{
$qty = $this->arrayManager->get('data/qty', $cartItemData);
if (!isset($qty)) {
throw new GraphQlInputException(__('Missing key "qty" in cart item data'));
}
return (float)$qty;
}

/**
* Extract Customizable Options from cart item data
*
* @param array $cartItemData
* @return array
*/
private function extractCustomizableOptions(array $cartItemData): array
{
$customizableOptions = $this->arrayManager->get('customizable_options', $cartItemData, []);

$customizableOptionsData = [];
foreach ($customizableOptions as $customizableOption) {
$customizableOptionsData[$customizableOption['id']] = $customizableOption['value'];
}
return $customizableOptionsData;
}

/**
* Format GraphQl input data to a shape that buy request has
*
* @param float $qty
* @param array $customOptions
* @return DataObject
*/
private function createBuyRequest(float $qty, array $customOptions): DataObject
{
return $this->dataObjectFactory->create([
'data' => [
'qty' => $qty,
'options' => $customOptions,
],
]);
}
}
Loading