Passed
Push — trunk ( 5f506d...1fbc85 )
by Christian
32:21 queued 15:49
created

CartLineItemController::deleteLineItem()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 1
nop 4
dl 0
loc 19
rs 9.9
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Storefront\Controller;
4
5
use Shopware\Core\Checkout\Cart\Cart;
6
use Shopware\Core\Checkout\Cart\CartException;
7
use Shopware\Core\Checkout\Cart\Error\Error;
8
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
9
use Shopware\Core\Checkout\Cart\LineItemFactoryHandler\ProductLineItemFactory;
10
use Shopware\Core\Checkout\Cart\LineItemFactoryRegistry;
11
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
12
use Shopware\Core\Checkout\Promotion\Cart\PromotionCartAddedInformationError;
13
use Shopware\Core\Checkout\Promotion\Cart\PromotionItemBuilder;
14
use Shopware\Core\Content\Product\Exception\ProductNotFoundException;
15
use Shopware\Core\Content\Product\SalesChannel\AbstractProductListRoute;
16
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
17
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
18
use Shopware\Core\Framework\Feature;
19
use Shopware\Core\Framework\Log\Package;
20
use Shopware\Core\Framework\Routing\Exception\MissingRequestParameterException;
21
use Shopware\Core\Framework\Util\HtmlSanitizer;
22
use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
23
use Shopware\Core\Profiling\Profiler;
24
use Shopware\Core\System\SalesChannel\SalesChannelContext;
25
use Symfony\Component\HttpFoundation\Request;
26
use Symfony\Component\HttpFoundation\Response;
27
use Symfony\Component\Routing\Annotation\Route;
28
29
/**
30
 * @internal
31
 */
32
#[Route(defaults: ['_routeScope' => ['storefront']])]
33
#[Package('storefront')]
34
class CartLineItemController extends StorefrontController
35
{
36
    /**
37
     * @internal
38
     */
39
    public function __construct(
40
        private readonly CartService $cartService,
41
        private readonly PromotionItemBuilder $promotionItemBuilder,
42
        private readonly ProductLineItemFactory $productLineItemFactory,
43
        private readonly HtmlSanitizer $htmlSanitizer,
44
        private readonly AbstractProductListRoute $productListRoute,
45
        private readonly LineItemFactoryRegistry $lineItemFactoryRegistry
46
    ) {
47
    }
48
49
    #[Route(path: '/checkout/line-item/delete/{id}', name: 'frontend.checkout.line-item.delete', defaults: ['XmlHttpRequest' => true], methods: ['POST', 'DELETE'])]
50
    public function deleteLineItem(Cart $cart, string $id, Request $request, SalesChannelContext $context): Response
51
    {
52
        return Profiler::trace('cart::delete-line-item', function () use ($cart, $id, $request, $context) {
53
            try {
54
                if (!$cart->has($id)) {
55
                    throw CartException::lineItemNotFound($id);
56
                }
57
58
                $cart = $this->cartService->remove($cart, $id, $context);
59
60
                if (!$this->traceErrors($cart)) {
61
                    $this->addFlash(self::SUCCESS, $this->trans('checkout.cartUpdateSuccess'));
62
                }
63
            } catch (\Exception) {
64
                $this->addFlash(self::DANGER, $this->trans('error.message-default'));
65
            }
66
67
            return $this->createActionResponse($request);
68
        });
69
    }
70
71
    /**
72
     * It has some individual code for the storefront layouts, like visual
73
     * error and success messages.
74
     */
75
    #[Route(path: '/checkout/promotion/add', name: 'frontend.checkout.promotion.add', defaults: ['XmlHttpRequest' => true], methods: ['POST'])]
76
    public function addPromotion(Cart $cart, Request $request, SalesChannelContext $context): Response
77
    {
78
        return Profiler::trace('cart::add-promotion', function () use ($cart, $request, $context) {
79
            try {
80
                $code = (string) $request->request->get('code');
81
82
                if ($code === '') {
83
                    throw new \InvalidArgumentException('Code is required');
84
                }
85
86
                $lineItem = $this->promotionItemBuilder->buildPlaceholderItem($code);
87
88
                $cart = $this->cartService->add($cart, $lineItem, $context);
89
90
                // we basically show all cart errors or notices
91
                // at the moments its not possible to show success messages with "green" color
92
                // from the cart...thus it has to be done in the storefront level
93
                // so if we have an promotion added notice, we simply convert this to
94
                // a success flash message
95
                $addedEvents = $cart->getErrors()->filterInstance(PromotionCartAddedInformationError::class);
96
                if ($addedEvents->count() > 0) {
97
                    $this->addFlash(self::SUCCESS, $this->trans('checkout.codeAddedSuccessful'));
98
99
                    return $this->createActionResponse($request);
100
                }
101
102
                // if we have no custom error message above
103
                // then simply continue with the default display
104
                // of the cart errors and notices
105
                $this->traceErrors($cart);
106
            } catch (\Exception) {
107
                $this->addFlash(self::DANGER, $this->trans('error.message-default'));
108
            }
109
110
            return $this->createActionResponse($request);
111
        });
112
    }
113
114
    #[Route(path: '/checkout/line-item/change-quantity/{id}', name: 'frontend.checkout.line-item.change-quantity', defaults: ['XmlHttpRequest' => true], methods: ['POST'])]
115
    public function changeQuantity(Cart $cart, string $id, Request $request, SalesChannelContext $context): Response
116
    {
117
        return Profiler::trace('cart::change-quantity', function () use ($cart, $id, $request, $context) {
118
            try {
119
                $quantity = $request->get('quantity');
120
121
                if ($quantity === null) {
122
                    throw new \InvalidArgumentException('quantity field is required');
123
                }
124
125
                if (!$cart->has($id)) {
126
                    throw CartException::lineItemNotFound($id);
127
                }
128
129
                $cart = $this->cartService->changeQuantity($cart, $id, (int) $quantity, $context);
130
131
                if (!$this->traceErrors($cart)) {
132
                    $this->addFlash(self::SUCCESS, $this->trans('checkout.cartUpdateSuccess'));
133
                }
134
            } catch (\Exception) {
135
                $this->addFlash(self::DANGER, $this->trans('error.message-default'));
136
            }
137
138
            return $this->createActionResponse($request);
139
        });
140
    }
141
142
    #[Route(path: '/checkout/product/add-by-number', name: 'frontend.checkout.product.add-by-number', methods: ['POST'])]
143
    public function addProductByNumber(Request $request, SalesChannelContext $context): Response
144
    {
145
        return Profiler::trace('cart::add-product-by-number', function () use ($request, $context) {
146
            $number = (string) $request->request->get('number');
147
148
            if (!$number) {
149
                throw new MissingRequestParameterException('number');
150
            }
151
152
            $criteria = new Criteria();
153
            $criteria->setLimit(1);
154
            $criteria->addFilter(new EqualsFilter('productNumber', $number));
155
156
            $data = $this->productListRoute->load($criteria, $context)->getProducts()->getIds();
157
158
            if (empty($data)) {
159
                $this->addFlash(self::DANGER, $this->trans(
160
                    'error.productNotFound',
161
                    ['%number%' => $this->htmlSanitizer->sanitize($number, null, true)]
162
                ));
163
164
                return $this->createActionResponse($request);
165
            }
166
167
            /** @var string $productId */
168
            $productId = array_shift($data);
169
170
            $product = $this->productLineItemFactory->create(['id' => $productId, 'referencedId' => $productId], $context);
171
172
            $cart = $this->cartService->getCart($context->getToken(), $context);
173
174
            $cart = $this->cartService->add($cart, $product, $context);
175
176
            if (!$this->traceErrors($cart)) {
177
                $this->addFlash(self::SUCCESS, $this->trans('checkout.addToCartSuccess', ['%count%' => 1]));
178
            }
179
180
            return $this->createActionResponse($request);
181
        });
182
    }
183
184
    /**
185
     * requires the provided items in the following form
186
     * 'lineItems' => [
187
     *     'anyKey' => [
188
     *         'id' => 'someKey'
189
     *         'quantity' => 2,
190
     *         'type' => 'someType'
191
     *     ],
192
     *     'randomKey' => [
193
     *         'id' => 'otherKey'
194
     *         'quantity' => 2,
195
     *         'type' => 'otherType'
196
     *     ]
197
     * ]
198
     */
199
    #[Route(path: '/checkout/line-item/add', name: 'frontend.checkout.line-item.add', defaults: ['XmlHttpRequest' => true], methods: ['POST'])]
200
    public function addLineItems(Cart $cart, RequestDataBag $requestDataBag, Request $request, SalesChannelContext $context): Response
201
    {
202
        return Profiler::trace('cart::add-line-item', function () use ($cart, $requestDataBag, $request, $context) {
203
            /** @var RequestDataBag|null $lineItems */
204
            $lineItems = $requestDataBag->get('lineItems');
205
            if (!$lineItems) {
206
                throw new MissingRequestParameterException('lineItems');
207
            }
208
209
            $count = 0;
210
211
            try {
212
                $items = [];
213
                /** @var RequestDataBag $lineItemData */
214
                foreach ($lineItems as $lineItemData) {
215
                    try {
216
                        $item = $this->lineItemFactoryRegistry->create($this->getLineItemArray($lineItemData), $context);
217
                        $count += $item->getQuantity();
218
219
                        $items[] = $item;
220
                    } catch (CartException $e) {
221
                        if ($e->getErrorCode() !== CartException::CART_LINE_ITEM_TYPE_NOT_SUPPORTED_CODE) {
222
                            throw $e;
223
                        }
224
225
                        /**
226
                         * @deprecated tag:v6.6.0 - remove complete try/catch and just leave the try content
227
                         */
228
                        Feature::triggerDeprecationOrThrow(
229
                            'v6.6.0.0',
230
                            'With Shopware 6.6.0.0, you will only be able to create line items only with registered LineItemFactories',
231
                        );
232
233
                        $lineItem = new LineItem(
234
                            $lineItemData->getAlnum('id'),
235
                            $lineItemData->getAlnum('type'),
236
                            $lineItemData->get('referencedId'),
237
                            $lineItemData->getInt('quantity', 1)
238
                        );
239
240
                        $lineItem->setStackable($lineItemData->getBoolean('stackable', true));
241
                        $lineItem->setRemovable($lineItemData->getBoolean('removable', true));
242
243
                        $count += $lineItem->getQuantity();
244
245
                        $items[] = $lineItem;
246
                    }
247
                }
248
249
                $cart = $this->cartService->add($cart, $items, $context);
250
251
                if (!$this->traceErrors($cart)) {
252
                    $this->addFlash(self::SUCCESS, $this->trans('checkout.addToCartSuccess', ['%count%' => $count]));
253
                }
254
            } catch (ProductNotFoundException) {
255
                $this->addFlash(self::DANGER, $this->trans('error.addToCartError'));
256
            }
257
258
            return $this->createActionResponse($request);
259
        });
260
    }
261
262
    private function traceErrors(Cart $cart): bool
263
    {
264
        if ($cart->getErrors()->count() <= 0) {
265
            return false;
266
        }
267
268
        $this->addCartErrors($cart, fn (Error $error) => $error->isPersistent());
269
270
        return true;
271
    }
272
273
    /**
274
     * @return array<string|int, mixed>
275
     */
276
    private function getLineItemArray(RequestDataBag $lineItemData): array
277
    {
278
        $lineItemArray = $lineItemData->all();
279
        $lineItemArray['quantity'] = $lineItemData->getInt('quantity', 1);
280
        $lineItemArray['stackable'] = $lineItemData->getBoolean('stackable', true);
281
        $lineItemArray['removable'] = $lineItemData->getBoolean('removable', true);
282
283
        if (isset($lineItemArray['priceDefinition']) && isset($lineItemArray['priceDefinition']['quantity'])) {
284
            $lineItemArray['priceDefinition']['quantity'] = (int) $lineItemArray['priceDefinition']['quantity'];
285
        }
286
287
        if (isset($lineItemArray['priceDefinition']) && isset($lineItemArray['priceDefinition']['isCalculated'])) {
288
            $lineItemArray['priceDefinition']['isCalculated'] = (int) $lineItemArray['priceDefinition']['isCalculated'];
289
        }
290
291
        return $lineItemArray;
292
    }
293
}
294