Skip to content
This repository was archived by the owner on Oct 8, 2025. It is now read-only.

Commit 10a92d1

Browse files
Merge pull request #206 from lchrusciel/add-multiple-items
[Cart] Add multiple items
2 parents 92f2fdf + 9ed49ee commit 10a92d1

15 files changed

+681
-32
lines changed

doc/swagger.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,39 @@ paths:
140140
description: "Invalid input, validation failed."
141141
schema:
142142
$ref: "#/definitions/GeneralError"
143+
/carts/{token}/multiple-items:
144+
parameters:
145+
- name: "token"
146+
in: "path"
147+
description: "Cart identifier."
148+
required: true
149+
type: "string"
150+
post:
151+
tags:
152+
- "cart"
153+
summary: "Add multiple items to your cart."
154+
description: "This endpoint will allow you to add a new item to your cart."
155+
operationId: "cartPutItems"
156+
consumes:
157+
- "application/json"
158+
produces:
159+
- "application/json"
160+
parameters:
161+
- in: "body"
162+
name: "content"
163+
description: "Description of items. The same rules applied to each of the array values as to the previous point."
164+
required: true
165+
schema:
166+
$ref: "#/definitions/PutItemsToCartRequest"
167+
responses:
168+
201:
169+
description: "Item has been added to the cart"
170+
schema:
171+
$ref: "#/definitions/Cart"
172+
400:
173+
description: "Invalid input, validation failed."
174+
schema:
175+
$ref: "#/definitions/GeneralError"
143176
/carts/{token}/items/{identifier}:
144177
parameters:
145178
- name: "token"
@@ -844,6 +877,12 @@ definitions:
844877
additionalProperties:
845878
type: "string"
846879
example: "HAT_SIZE_S"
880+
PutItemsToCartRequest:
881+
type: "array"
882+
description: "Body of request used to put item to the cart."
883+
items:
884+
type: "object"
885+
$ref: "#/definitions/PutItemToCartRequest"
847886
ChangeItemQuantityRequest:
848887
type: "object"
849888
description: "Body of request used to change quantity of an item."

src/Controller/Cart/PutItemToCartAction.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,15 @@ public function __invoke(Request $request): Response
7979
private function provideCommandRequest(Request $request)
8080
{
8181
if (!$request->request->has('variantCode') && !$request->request->has('options')) {
82-
return new PutSimpleItemToCartRequest($request);
82+
return PutSimpleItemToCartRequest::fromRequest($request);
8383
}
8484

8585
if ($request->request->has('variantCode') && !$request->request->has('options')) {
86-
return new PutVariantBasedConfigurableItemToCartRequest($request);
86+
return PutVariantBasedConfigurableItemToCartRequest::fromRequest($request);
8787
}
8888

8989
if (!$request->request->has('variantCode') && $request->request->has('options')) {
90-
return new PutOptionBasedConfigurableItemToCartRequest($request);
90+
return PutOptionBasedConfigurableItemToCartRequest::fromRequest($request);
9191
}
9292

9393
throw new NotFoundHttpException('Variant not found for given configuration');
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sylius\ShopApiPlugin\Controller\Cart;
6+
7+
use FOS\RestBundle\View\View;
8+
use FOS\RestBundle\View\ViewHandlerInterface;
9+
use League\Tactician\CommandBus;
10+
use Sylius\ShopApiPlugin\Factory\ValidationErrorViewFactoryInterface;
11+
use Sylius\ShopApiPlugin\View\ValidationErrorView;
12+
use Sylius\ShopApiPlugin\ViewRepository\CartViewRepositoryInterface;
13+
use Sylius\ShopApiPlugin\Request\PutOptionBasedConfigurableItemToCartRequest;
14+
use Sylius\ShopApiPlugin\Request\PutSimpleItemToCartRequest;
15+
use Sylius\ShopApiPlugin\Request\PutVariantBasedConfigurableItemToCartRequest;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
19+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
20+
use Symfony\Component\Validator\ConstraintViolationInterface;
21+
use Symfony\Component\Validator\ConstraintViolationListInterface;
22+
use Symfony\Component\Validator\Validator\ValidatorInterface;
23+
24+
final class PutItemsToCartAction
25+
{
26+
/** @var ViewHandlerInterface */
27+
private $viewHandler;
28+
29+
/** @var CommandBus */
30+
private $bus;
31+
32+
/** @var ValidatorInterface */
33+
private $validator;
34+
35+
/** @var CartViewRepositoryInterface */
36+
private $cartQuery;
37+
38+
/** @var string */
39+
private $validationErrorViewClass;
40+
41+
public function __construct(
42+
ViewHandlerInterface $viewHandler,
43+
CommandBus $bus,
44+
ValidatorInterface $validator,
45+
CartViewRepositoryInterface $cartQuery,
46+
string $validationErrorViewClass
47+
) {
48+
$this->viewHandler = $viewHandler;
49+
$this->bus = $bus;
50+
$this->validator = $validator;
51+
$this->cartQuery = $cartQuery;
52+
$this->validationErrorViewClass = $validationErrorViewClass;
53+
}
54+
55+
public function __invoke(Request $request): Response
56+
{
57+
/** @var ConstraintViolationListInterface[] $validationResults */
58+
$validationResults = [];
59+
$commandRequests = [];
60+
$commandsToExecute = [];
61+
$token = $request->attributes->get('token');
62+
63+
foreach ($request->request->all() as $item) {
64+
$item['token'] = $token;
65+
$commandRequests[] = $this->provideCommandRequest($item);
66+
}
67+
68+
foreach ($commandRequests as $commandRequest) {
69+
$validationResult = $this->validator->validate($commandRequest);
70+
71+
if (0 === count($validationResult)) {
72+
$commandsToExecute[] = $commandRequest->getCommand();
73+
}
74+
75+
$validationResults[] = $validationResult;
76+
}
77+
78+
if (!$this->isValid($validationResults)) {
79+
/** @var ValidationErrorView $errorMessage */
80+
$errorMessage = new $this->validationErrorViewClass();
81+
82+
$errorMessage->code = Response::HTTP_BAD_REQUEST;
83+
$errorMessage->message = 'Validation failed';
84+
85+
foreach ($validationResults as $validationResult) {
86+
$errors = [];
87+
88+
/** @var ConstraintViolationInterface $result */
89+
foreach ($validationResult as $result) {
90+
$errors[$result->getPropertyPath()][] = $result->getMessage();
91+
}
92+
93+
$errorMessage->errors[] = $errors;
94+
}
95+
96+
return $this->viewHandler->handle(View::create($errorMessage, Response::HTTP_BAD_REQUEST));
97+
}
98+
99+
foreach ($commandsToExecute as $commandToExecute) {
100+
$this->bus->handle($commandToExecute);
101+
}
102+
103+
try {
104+
return $this->viewHandler->handle(
105+
View::create($this->cartQuery->getOneByToken($token), Response::HTTP_CREATED)
106+
);
107+
} catch (\InvalidArgumentException $exception) {
108+
throw new BadRequestHttpException($exception->getMessage());
109+
}
110+
}
111+
112+
/**
113+
* @param array $item
114+
*
115+
* @return PutOptionBasedConfigurableItemToCartRequest|PutSimpleItemToCartRequest|PutVariantBasedConfigurableItemToCartRequest
116+
*/
117+
private function provideCommandRequest(array $item)
118+
{
119+
if (!isset($item['variantCode']) && !isset($item['options'])) {
120+
return PutSimpleItemToCartRequest::fromArray($item);
121+
}
122+
123+
if (isset($item['variantCode']) && !isset($item['options'])) {
124+
return PutVariantBasedConfigurableItemToCartRequest::fromArray($item);
125+
}
126+
127+
if (!isset($item['variantCode']) && isset($item['options'])) {
128+
return PutOptionBasedConfigurableItemToCartRequest::fromArray($item);
129+
}
130+
131+
throw new NotFoundHttpException('Variant not found for given configuration');
132+
}
133+
134+
private function isValid(array $validationResults): bool
135+
{
136+
foreach ($validationResults as $validationResult) {
137+
if (0 !== count($validationResult)) {
138+
return false;
139+
}
140+
}
141+
142+
return true;
143+
}
144+
}

src/Request/PutOptionBasedConfigurableItemToCartRequest.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,22 @@ final class PutOptionBasedConfigurableItemToCartRequest
2727
*/
2828
private $quantity;
2929

30-
/**
31-
* @param Request $request
32-
*/
33-
public function __construct(Request $request)
30+
private function __construct($token, $productCode, $options, $quantity)
31+
{
32+
$this->token = $token;
33+
$this->productCode = $productCode;
34+
$this->options = $options;
35+
$this->quantity = $quantity;
36+
}
37+
38+
public static function fromArray(array $item)
39+
{
40+
return new self($item['token'] ?? null, $item['productCode'] ?? null, $item['options'] ?? null, $item['quantity'] ?? null);
41+
}
42+
43+
public static function fromRequest(Request $request)
3444
{
35-
$this->token = $request->attributes->get('token');
36-
$this->productCode = $request->request->get('productCode');
37-
$this->options = $request->request->get('options');
38-
$this->quantity = $request->request->getInt('quantity');
45+
return new self($request->attributes->get('token'), $request->request->get('productCode'), $request->request->get('options'), $request->request->get('quantity'));
3946
}
4047

4148
/**

src/Request/PutSimpleItemToCartRequest.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,21 @@ final class PutSimpleItemToCartRequest
2222
*/
2323
private $quantity;
2424

25-
/**
26-
* @param Request $request
27-
*/
28-
public function __construct(Request $request)
25+
private function __construct($token, $productCode, $quantity)
26+
{
27+
$this->token = $token;
28+
$this->productCode = $productCode;
29+
$this->quantity = $quantity;
30+
}
31+
32+
public static function fromArray(array $item)
33+
{
34+
return new self($item['token'] ?? null, $item['productCode'] ?? null, $item['quantity'] ?? null);
35+
}
36+
37+
public static function fromRequest(Request $request)
2938
{
30-
$this->token = $request->attributes->get('token');
31-
$this->productCode = $request->request->get('productCode');
32-
$this->quantity = $request->request->get('quantity');
39+
return new self($request->attributes->get('token'), $request->request->get('productCode'), $request->request->get('quantity'));
3340
}
3441

3542
/**

src/Request/PutVariantBasedConfigurableItemToCartRequest.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,22 @@ final class PutVariantBasedConfigurableItemToCartRequest
2727
*/
2828
private $quantity;
2929

30-
/**
31-
* @param Request $request
32-
*/
33-
public function __construct(Request $request)
30+
private function __construct($token, $productCode, $variantCode, $quantity)
31+
{
32+
$this->token = $token;
33+
$this->productCode = $productCode;
34+
$this->variantCode = $variantCode;
35+
$this->quantity = $quantity;
36+
}
37+
38+
public static function fromArray(array $item)
39+
{
40+
return new self($item['token'] ?? null, $item['productCode'] ?? null, $item['variantCode'] ?? null, $item['quantity'] ?? null);
41+
}
42+
43+
public static function fromRequest(Request $request)
3444
{
35-
$this->token = $request->attributes->get('token');
36-
$this->productCode = $request->request->get('productCode');
37-
$this->variantCode = $request->request->get('variantCode');
38-
$this->quantity = $request->request->get('quantity');
45+
return new self($request->attributes->get('token'), $request->request->get('productCode'), $request->request->get('variantCode'), $request->request->get('quantity'));
3946
}
4047

4148
/**

src/Resources/config/routing/cart.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ shop_api_add_to_cart:
1616
defaults:
1717
_controller: sylius.shop_api_plugin.controller.cart.put_item_to_cart_action
1818

19+
shop_api_add_multiple_items_to_cart:
20+
path: /carts/{token}/multiple-items
21+
methods: [POST]
22+
defaults:
23+
_controller: sylius.shop_api_plugin.controller.cart.put_items_to_cart_action
24+
1925
shop_api_drop_cart:
2026
path: /carts/{token}
2127
methods: [DELETE]

src/Resources/config/services/actions/cart.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,15 @@
5757
<argument type="service" id="sylius.shop_api_plugin.factory.validation_error_view_factory" />
5858
<argument type="service" id="sylius.shop_api_plugin.view_repository.cart_view_repository" />
5959
</service>
60+
61+
<service id="sylius.shop_api_plugin.controller.cart.put_items_to_cart_action"
62+
class="Sylius\ShopApiPlugin\Controller\Cart\PutItemsToCartAction"
63+
>
64+
<argument type="service" id="fos_rest.view_handler" />
65+
<argument type="service" id="tactician.commandbus" />
66+
<argument type="service" id="validator" />
67+
<argument type="service" id="sylius.shop_api_plugin.view_repository.cart_view_repository" />
68+
<argument type="string">%sylius.shop_api.view.validation_error.class%</argument>
69+
</service>
6070
</services>
6171
</container>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<!--
4+
5+
This file is part of the Sylius package.
6+
7+
(c) Paweł Jędrzejewski
8+
9+
For the full copyright and license information, please view the LICENSE
10+
file that was distributed with this source code.
11+
12+
-->
13+
14+
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/services/constraint-mapping-1.0.xsd">
15+
<class name="Sylius\ShopApiPlugin\Request\PutOptionBasedConfigurableItemToCartRequest">
16+
<property name="token">
17+
<constraint name="NotNull">
18+
<option name="message">sylius.shop_api.token.not_null</option>
19+
</constraint>
20+
<constraint name="Sylius\ShopApiPlugin\Validator\Constraints\CartExists" />
21+
</property>
22+
<property name="quantity">
23+
<constraint name="Type">
24+
<option name="type">integer</option>
25+
<option name="message">sylius.shop_api.quantity.type_integer</option>
26+
</constraint>
27+
28+
<constraint name="GreaterThan">
29+
<option name="value">0</option>
30+
<option name="message">sylius.shop_api.quantity.greater_than_0</option>
31+
</constraint>
32+
33+
<constraint name="NotNull">
34+
<option name="message">sylius.shop_api.quantity.not_null</option>
35+
</constraint>
36+
</property>
37+
<property name="productCode">
38+
<constraint name="NotNull">
39+
<option name="message">sylius.shop_api.product.not_null</option>
40+
</constraint>
41+
<constraint name="Sylius\ShopApiPlugin\Validator\Constraints\ProductExists" />
42+
<constraint name="Sylius\ShopApiPlugin\Validator\Constraints\ConfigurableProduct" />
43+
</property>
44+
</class>
45+
</constraint-mapping>

0 commit comments

Comments
 (0)