diff --git a/CHANGELOG-2.1.x.md b/CHANGELOG-2.1.x.md index da6782f43e..8080238069 100644 --- a/CHANGELOG-2.1.x.md +++ b/CHANGELOG-2.1.x.md @@ -5,4 +5,9 @@ - deprecated [CoreShop\Bundle\StoreBundle\Theme\ThemeHelper](https://github.com/coreshop/CoreShop/blob/master/src/CoreShop/Bundle/StoreBundle/Theme/ThemeHelper.php) in favor of [CoreShop\Bundle\ThemeBundle\Service\ThemeHelper](https://github.com/coreshop/CoreShop/blob/master/src/CoreShop/Bundle/ThemeBundle/Service/ThemeHelper.php) - deprecated [CoreShop\Bundle\StoreBundle\Theme\ThemeHelperInterface](https://github.com/coreshop/CoreShop/blob/master/src/CoreShop/Bundle/StoreBundle/Theme/ThemeHelperInterface.php) in favor of [CoreShop\Bundle\ThemeBundle\Service\ThemeHelperInterface](https://github.com/coreshop/CoreShop/blob/master/src/CoreShop/Bundle/ThemeBundle/Service/ThemeHelperInterface.php) - deprecated [CoreShop\Bundle\StoreBundle\Theme\ThemeResolver](https://github.com/coreshop/CoreShop/blob/master/src/CoreShop/Bundle/StoreBundle/Theme/ThemeResolver.php) in favor of [CoreShop\Bundle\ThemeBundle\Service\ThemeResolver](https://github.com/coreshop/CoreShop/blob/master/src/CoreShop/Bundle/ThemeBundle/Service/ThemeResolver.php) - - deprecated [CoreShop\Bundle\StoreBundle\Theme\ThemeResolverInterface](https://github.com/coreshop/CoreShop/blob/master/src/CoreShop/Bundle/StoreBundle/Theme/ThemeResolverInterface.php) in favor of [CoreShop\Bundle\ThemeBundle\Service\ThemeResolverInterface](https://github.com/coreshop/CoreShop/blob/master/src/CoreShop/Bundle/ThemeBundle/Service/ThemeResolverInterface.php) \ No newline at end of file + - deprecated [CoreShop\Bundle\StoreBundle\Theme\ThemeResolverInterface](https://github.com/coreshop/CoreShop/blob/master/src/CoreShop/Bundle/StoreBundle/Theme/ThemeResolverInterface.php) in favor of [CoreShop\Bundle\ThemeBundle\Service\ThemeResolverInterface](https://github.com/coreshop/CoreShop/blob/master/src/CoreShop/Bundle/ThemeBundle/Service/ThemeResolverInterface.php) + + - Introduce AddToCartFormType. This allows to use validators to check if its allowed to add a product to the cart. If you update from CoreShop 2.0.* change the add-to-cart form in your templates to the following: (https://github.com/coreshop/CoreShop/pull/812/files#diff-3e06a5f0e813be230a0cd232e916738eL29) + ``` + {{ render(url('coreshop_cart_add', {'product': product.id})) }} + ``` diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/config/pimcore/coreshop/coreshop_product_quantity_price_rules.yml b/src/CoreShop/Bundle/CoreBundle/Resources/config/pimcore/coreshop/coreshop_product_quantity_price_rules.yml index 9081348ac1..90ce6dc516 100644 --- a/src/CoreShop/Bundle/CoreBundle/Resources/config/pimcore/coreshop/coreshop_product_quantity_price_rules.yml +++ b/src/CoreShop/Bundle/CoreBundle/Resources/config/pimcore/coreshop/coreshop_product_quantity_price_rules.yml @@ -5,7 +5,7 @@ core_shop_product_quantity_price_rules: model: CoreShop\Component\Core\Model\QuantityRange action_constraints: - - class: 'CoreShop\Bundle\CoreBundle\Validation\Constraints\QuantityRangePriceCurrencyAware' + class: 'CoreShop\Bundle\CoreBundle\Validator\Constraints\QuantityRangePriceCurrencyAware' groups: - 'coreshop_product_quantity_price_rules_range_validation_behaviour_fixed' - 'coreshop_product_quantity_price_rules_range_validation_behaviour_amount_decrease' diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/config/services.yml b/src/CoreShop/Bundle/CoreBundle/Resources/config/services.yml index 71a7cb2f00..8c19671e0c 100755 --- a/src/CoreShop/Bundle/CoreBundle/Resources/config/services.yml +++ b/src/CoreShop/Bundle/CoreBundle/Resources/config/services.yml @@ -25,6 +25,7 @@ imports: - { resource: "services/grid_config.yml" } - { resource: "services/routing.yml" } - { resource: "services/product-quantity-price-rules.yml" } + - { resource: "services/validators.yml" } services: _defaults: diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/config/services/validators.yml b/src/CoreShop/Bundle/CoreBundle/Resources/config/services/validators.yml new file mode 100644 index 0000000000..dc0e513f73 --- /dev/null +++ b/src/CoreShop/Bundle/CoreBundle/Resources/config/services/validators.yml @@ -0,0 +1,7 @@ +services: + coreshop.validator.add_to_cart_availability: + class: CoreShop\Bundle\CoreBundle\Validator\Constraints\AddToCartAvailabilityValidator + arguments: + - '@coreshop.inventory.availability_checker' + tags: + - { name: validator.constraint_validator, alias: 'coreshop_add_to_cart_availability' } diff --git a/src/CoreShop/Bundle/CoreBundle/Resources/config/validation/AddToCartCommand.yml b/src/CoreShop/Bundle/CoreBundle/Resources/config/validation/AddToCartCommand.yml new file mode 100644 index 0000000000..c0a7c4de27 --- /dev/null +++ b/src/CoreShop/Bundle/CoreBundle/Resources/config/validation/AddToCartCommand.yml @@ -0,0 +1,3 @@ +CoreShop\Bundle\OrderBundle\DTO\AddToCart: + constraints: + - \CoreShop\Bundle\CoreBundle\Validator\Constraints\AddToCartAvailability: { message: 'coreshop.cart_item.not_sufficient_stock', groups: ['coreshop'] } diff --git a/src/CoreShop/Bundle/CoreBundle/Validator/Constraints/AddToCartAvailability.php b/src/CoreShop/Bundle/CoreBundle/Validator/Constraints/AddToCartAvailability.php new file mode 100644 index 0000000000..5c1c9f47da --- /dev/null +++ b/src/CoreShop/Bundle/CoreBundle/Validator/Constraints/AddToCartAvailability.php @@ -0,0 +1,39 @@ +availabilityChecker = $availabilityChecker; + } + + /** + * @param AddToCartInterface $addCartItemCommand + * + * {@inheritdoc} + */ + public function validate($addCartItemCommand, Constraint $constraint): void + { + Assert::isInstanceOf($addCartItemCommand, AddToCartInterface::class); + Assert::isInstanceOf($constraint, AddToCartAvailability::class); + + /** + * @var StockableInterface $purchasable + */ + $purchasable = $addCartItemCommand->getPurchasable(); + + $isStockSufficient = $this->availabilityChecker->isStockSufficient( + $purchasable, + $addCartItemCommand->getQuantity() + $this->getExistingCartItemQuantityFromCart($addCartItemCommand->getCart(), $purchasable) + ); + + if (!$isStockSufficient) { + $this->context->addViolation( + $constraint->message, + ['%stockable%' => $purchasable->getInventoryName()] + ); + } + } + + /** + * @param CartInterface $cart + * @param PurchasableInterface $purchasable + * @return int + */ + private function getExistingCartItemQuantityFromCart(CartInterface $cart, PurchasableInterface $purchasable) + { + $cartItem = $cart->getItemForProduct($purchasable); + + if ($cartItem instanceof CartItemInterface) { + return $cartItem->getQuantity(); + } + + return 0; + } +} diff --git a/src/CoreShop/Bundle/CoreBundle/Validation/Constraints/QuantityRangePriceCurrencyAware.php b/src/CoreShop/Bundle/CoreBundle/Validator/Constraints/QuantityRangePriceCurrencyAware.php similarity index 91% rename from src/CoreShop/Bundle/CoreBundle/Validation/Constraints/QuantityRangePriceCurrencyAware.php rename to src/CoreShop/Bundle/CoreBundle/Validator/Constraints/QuantityRangePriceCurrencyAware.php index df91c3d97f..17445e95b8 100644 --- a/src/CoreShop/Bundle/CoreBundle/Validation/Constraints/QuantityRangePriceCurrencyAware.php +++ b/src/CoreShop/Bundle/CoreBundle/Validator/Constraints/QuantityRangePriceCurrencyAware.php @@ -10,7 +10,7 @@ * @license https://www.coreshop.org/license GNU General Public License version 3 (GPLv3) */ -namespace CoreShop\Bundle\CoreBundle\Validation\Constraints; +namespace CoreShop\Bundle\CoreBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; diff --git a/src/CoreShop/Bundle/CoreBundle/Validation/Constraints/QuantityRangePriceCurrencyAwareValidator.php b/src/CoreShop/Bundle/CoreBundle/Validator/Constraints/QuantityRangePriceCurrencyAwareValidator.php similarity index 96% rename from src/CoreShop/Bundle/CoreBundle/Validation/Constraints/QuantityRangePriceCurrencyAwareValidator.php rename to src/CoreShop/Bundle/CoreBundle/Validator/Constraints/QuantityRangePriceCurrencyAwareValidator.php index 2f245e1fb8..0a1589398a 100644 --- a/src/CoreShop/Bundle/CoreBundle/Validation/Constraints/QuantityRangePriceCurrencyAwareValidator.php +++ b/src/CoreShop/Bundle/CoreBundle/Validator/Constraints/QuantityRangePriceCurrencyAwareValidator.php @@ -10,7 +10,7 @@ * @license https://www.coreshop.org/license GNU General Public License version 3 (GPLv3) */ -namespace CoreShop\Bundle\CoreBundle\Validation\Constraints; +namespace CoreShop\Bundle\CoreBundle\Validator\Constraints; use CoreShop\Component\Core\Model\CurrencyInterface; use CoreShop\Component\Core\Model\QuantityRangeInterface; diff --git a/src/CoreShop/Bundle/FrontendBundle/Controller/CartController.php b/src/CoreShop/Bundle/FrontendBundle/Controller/CartController.php index d33939bfec..4fca426a57 100644 --- a/src/CoreShop/Bundle/FrontendBundle/Controller/CartController.php +++ b/src/CoreShop/Bundle/FrontendBundle/Controller/CartController.php @@ -12,21 +12,26 @@ namespace CoreShop\Bundle\FrontendBundle\Controller; +use CoreShop\Bundle\OrderBundle\DTO\AddToCartInterface; +use CoreShop\Bundle\OrderBundle\Form\Type\AddToCartType; use CoreShop\Bundle\OrderBundle\Form\Type\CartType; use CoreShop\Bundle\OrderBundle\Form\Type\ShippingCalculatorType; use CoreShop\Component\Address\Model\AddressInterface; -use CoreShop\Component\Inventory\Model\StockableInterface; use CoreShop\Component\Order\Cart\Rule\CartPriceRuleProcessorInterface; use CoreShop\Component\Order\Cart\Rule\CartPriceRuleUnProcessorInterface; use CoreShop\Component\Order\Context\CartContextInterface; use CoreShop\Component\Order\Manager\CartManagerInterface; +use CoreShop\Component\Order\Model\CartInterface; use CoreShop\Component\Order\Model\CartItemInterface; use CoreShop\Component\Order\Model\CartPriceRuleVoucherCodeInterface; use CoreShop\Component\Order\Model\PurchasableInterface; use CoreShop\Component\Order\Repository\CartPriceRuleVoucherRepositoryInterface; use CoreShop\Component\StorageList\StorageListModifierInterface; use Symfony\Component\EventDispatcher\GenericEvent; +use Symfony\Component\Form\FormError; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Validator\ConstraintViolationListInterface; class CartController extends FrontendController { @@ -148,51 +153,82 @@ public function shipmentCalculationAction(Request $request) /** * @param Request $request * - * @return \Symfony\Component\HttpFoundation\RedirectResponse + * @return \Symfony\Component\HttpFoundation\Response */ public function addItemAction(Request $request) { $product = $this->get('coreshop.repository.stack.purchasable')->find($request->get('product')); if (!$product instanceof PurchasableInterface) { + if ($request->isXmlHttpRequest()) { + return new JsonResponse([ + 'success' => false, + ]); + } + $redirect = $request->get('_redirect', $this->generateCoreShopUrl(null, 'coreshop_index')); return $this->redirect($redirect); } - $quantity = (int) $request->get('quantity', 1); + $addToCart = $this->createAddToCart($this->getCart(), $product, (int) $request->get('quantity', 1)); - if (!is_int($quantity)) { - $quantity = 1; - } + $form = $this->createForm(AddToCartType::class, $addToCart); - $redirect = $request->get('_redirect', $this->generateCoreShopUrl($this->getCart(), 'coreshop_cart_summary')); + if ($request->isMethod('POST')) { + $redirect = $request->get('_redirect', $this->generateCoreShopUrl($this->getCart(), 'coreshop_cart_summary')); - if ($product instanceof StockableInterface) { - $item = $this->getCart()->getItemForProduct($product); - $quantityToCheckStock = $quantity; + if ($form->handleRequest($request)->isValid()) { + /** + * @var AddToCartInterface $addToCart + */ + $addToCart = $form->getData(); - if ($item instanceof CartItemInterface) { - $quantityToCheckStock += $item->getQuantity(); - } + $this->getCartModifier()->addItem($addToCart->getCart(), $addToCart->getPurchasable(), $addToCart->getQuantity()); + $this->getCartManager()->persistCart($this->getCart()); - $hasStock = $this->get('coreshop.inventory.availability_checker.default')->isStockSufficient($product, $quantityToCheckStock); + $this->get('coreshop.tracking.manager')->trackCartAdd($addToCart->getCart(), $addToCart->getPurchasable(), $addToCart->getQuantity()); - if (!$hasStock) { - $this->addFlash('error', $this->get('translator')->trans('coreshop.cart_item.not_sufficient_stock', ['%stockable%' => $product->getName()], 'validators')); + $this->addFlash('success', $this->get('translator')->trans('coreshop.ui.item_added')); + + if ($request->isXmlHttpRequest()) { + return new JsonResponse([ + 'success' => true + ]); + } return $this->redirect($redirect); } - } - $this->getCartModifier()->addItem($this->getCart(), $product, $quantity); - $this->getCartManager()->persistCart($this->getCart()); + foreach ($form->getErrors(true, true) as $error) { + $this->addFlash('error', $error->getMessage()); + } - $this->get('coreshop.tracking.manager')->trackCartAdd($this->getCart(), $product, $quantity); + if ($request->isXmlHttpRequest()) { + return new JsonResponse([ + 'success' => false, + 'errors' => array_map(function(FormError $error) { + return $error->getMessage(); + }, iterator_to_array($form->getErrors(true))) + ]); + } - $this->addFlash('success', $this->get('translator')->trans('coreshop.ui.item_added')); + return $this->redirect($redirect); + } - return $this->redirect($redirect); + if ($request->isXmlHttpRequest()) { + return new JsonResponse([ + 'success' => false, + ]); + } + + return $this->renderTemplate( + $request->get('template', $this->templateConfigurator->findTemplate('Product/_addToCart.html')), + [ + 'form' => $form->createView(), + 'product' => $product + ] + ); } /** @@ -259,6 +295,17 @@ public function createQuoteAction(Request $request) return $this->redirectToRoute('coreshop_quote_detail', ['quote' => $quote->getId()]); } + /** + * @param CartInterface $cart + * @param PurchasableInterface $purchasable + * @param int $quantity + * @return AddToCartInterface + */ + protected function createAddToCart(CartInterface $cart, PurchasableInterface $purchasable, int $quantity) + { + return $this->get('coreshop.factory.add_to_cart')->createWithCartAndPurchasableAndQuantity($cart, $purchasable, $quantity); + } + /** * @return \CoreShop\Component\Resource\Factory\PimcoreFactory */ @@ -327,4 +374,17 @@ protected function getCartManager() { return $this->get('coreshop.cart.manager'); } + + /** + * @param CartItemInterface $cartItem + * + * @return ConstraintViolationListInterface + */ + private function getCartItemErrors(CartItemInterface $cartItem) + { + return $this + ->get('validator') + ->validate($cartItem, null, $this->getParameter('coreshop.form.type.cart_item.validation_groups')) + ; + } } diff --git a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Category/_grid.html.twig b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Category/_grid.html.twig index 95f2c7c5ad..eaf0c1d036 100644 --- a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Category/_grid.html.twig +++ b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Category/_grid.html.twig @@ -26,13 +26,9 @@ {% if coreshop_inventory_is_available(product) %} -
- -
+ {% if coreshop_inventory_is_available(product) %} + {{ render(url('coreshop_cart_add', {'product': product.id})) }} + {% endif %} {% endif %} diff --git a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Category/_list.html.twig b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Category/_list.html.twig index d2872f4f8f..e1105ac8b1 100644 --- a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Category/_list.html.twig +++ b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Category/_list.html.twig @@ -28,15 +28,8 @@ - {% if coreshop_inventory_is_available(product) %} -
- -
+ {{ render(url('coreshop_cart_add', {'product': product.id})) }} {% endif %} diff --git a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Form/theme.html.twig b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Form/theme.html.twig index 1342dfe104..821ce9603b 100644 --- a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Form/theme.html.twig +++ b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Form/theme.html.twig @@ -7,4 +7,4 @@ {{ messages.error(error.message, false) }} {%- endfor -%} {%- endif -%} -{%- endblock form_errors -%} \ No newline at end of file +{%- endblock form_errors -%} diff --git a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/_addToCart.html.twig b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/_addToCart.html.twig new file mode 100644 index 0000000000..65faf0a791 --- /dev/null +++ b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/_addToCart.html.twig @@ -0,0 +1,18 @@ +{% form_theme form '@CoreShopFrontend/Form/theme.html.twig' %} + +{{ form_start(form, {'action': path('coreshop_cart_add', {'product': product.id})}) }} + {{ form_errors(form) }} + +
+ {{ form_row(form.quantity, {label: false, attr: {class: 'form-control'}}) }} +
+ +
+
+ + {{ form_row(form._token) }} +{{ form_end(form, {'render_rest': false}) }} + diff --git a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/_preview.html.twig b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/_preview.html.twig index 4acf60850d..466390a077 100644 --- a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/_preview.html.twig +++ b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/_preview.html.twig @@ -21,14 +21,10 @@ {% if coreshop_inventory_is_available(product) %}
-
- -
+ {% if coreshop_inventory_is_available(product) %} + {{ render(url('coreshop_cart_add', {'product': product.id})) }} + {% endif %}
{% endif %} - \ No newline at end of file + diff --git a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/detail.html.twig b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/detail.html.twig index af7679340d..74a6ee65b5 100644 --- a/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/detail.html.twig +++ b/src/CoreShop/Bundle/FrontendBundle/Resources/views/Product/detail.html.twig @@ -110,14 +110,7 @@ {% if coreshop_inventory_is_available(product) %} -
-
- -
- -
-
-
+ {{ render(url('coreshop_cart_add', {'product': product.id})) }} {% endif %} diff --git a/src/CoreShop/Bundle/OrderBundle/DTO/AddToCart.php b/src/CoreShop/Bundle/OrderBundle/DTO/AddToCart.php new file mode 100644 index 0000000000..07d9237450 --- /dev/null +++ b/src/CoreShop/Bundle/OrderBundle/DTO/AddToCart.php @@ -0,0 +1,94 @@ +cart = $cart; + $this->purchasable = $purchasable; + $this->quantity = $quantity; + } + + /** + * @return CartInterface + */ + public function getCart() + { + return $this->cart; + } + + /** + * @param CartInterface $cart + */ + public function setCart(CartInterface $cart) + { + $this->cart = $cart; + } + + /** + * @return PurchasableInterface + */ + public function getPurchasable() + { + return $this->purchasable; + } + + /** + * @param PurchasableInterface $purchasable + */ + public function setPurchasable(PurchasableInterface $purchasable) + { + $this->purchasable = $purchasable; + } + + /** + * @return int + */ + public function getQuantity() + { + return $this->quantity; + } + + /** + * @param int $quantity + */ + public function setQuantity(int $quantity) + { + $this->quantity = $quantity; + } +} diff --git a/src/CoreShop/Bundle/OrderBundle/DTO/AddToCartInterface.php b/src/CoreShop/Bundle/OrderBundle/DTO/AddToCartInterface.php new file mode 100644 index 0000000000..374906b5ae --- /dev/null +++ b/src/CoreShop/Bundle/OrderBundle/DTO/AddToCartInterface.php @@ -0,0 +1,34 @@ +add('quantity', IntegerType::class, [ + 'required' => false, + 'empty_data' => '1' + ]); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + $resolver->setDefaults([ + 'csrf_protection' => true, + ]); + } + + /** + * {@inheritdoc} + */ + public function getBlockPrefix(): string + { + return 'coreshop_add_to_cart'; + } +} diff --git a/src/CoreShop/Bundle/OrderBundle/Resources/config/services.yml b/src/CoreShop/Bundle/OrderBundle/Resources/config/services.yml index 2fda24e8ab..71a545e85b 100644 --- a/src/CoreShop/Bundle/OrderBundle/Resources/config/services.yml +++ b/src/CoreShop/Bundle/OrderBundle/Resources/config/services.yml @@ -12,6 +12,7 @@ imports: - { resource: "services/cart.yml" } - { resource: "services/grid_config.yml" } - { resource: "services/payment.yml" } + - { resource: "services/forms.yml" } services: _defaults: @@ -87,3 +88,6 @@ services: public: false arguments: - '@coreshop.custom_factory.cart_item.inner' + + coreshop.factory.add_to_cart: + class: CoreShop\Bundle\OrderBundle\Factory\AddToCartFactory diff --git a/src/CoreShop/Bundle/OrderBundle/Resources/config/services/forms.yml b/src/CoreShop/Bundle/OrderBundle/Resources/config/services/forms.yml new file mode 100644 index 0000000000..a6e03bb97a --- /dev/null +++ b/src/CoreShop/Bundle/OrderBundle/Resources/config/services/forms.yml @@ -0,0 +1,11 @@ +parameters: + coreshop.form.type.add_to_cart.validation_groups: [coreshop] + +services: + coreshop.form.type.add_to_cart: + class: CoreShop\Bundle\OrderBundle\Form\Type\AddToCartType + arguments: + - 'CoreShop\Bundle\OrderBundle\DTO\AddToCart' + - '%coreshop.form.type.add_to_cart.validation_groups%' + tags: + - { name: form.type }