Загрузка данных
Subject: [PATCH] ~
---
Index: src/Controller/Front/CartApiController.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/Controller/Front/CartApiController.php b/src/Controller/Front/CartApiController.php
--- a/src/Controller/Front/CartApiController.php (revision 2252d8f735414967db11cf5f14e017509dce76c8)
+++ b/src/Controller/Front/CartApiController.php (date 1784821023869)
@@ -10,6 +10,7 @@
use App\Repository\CartProductRepository;
use App\Repository\CartRepository;
use App\Repository\ProductRepository;
+use App\Utils\Generator\TokenGenerator;
use Doctrine\Persistence\ManagerRegistry as Doctrine;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -48,7 +49,11 @@
$cart = $cartRepository->findOneBy(['token' => $cartToken]);
if (!$cart) {
$cart = new Cart();
- $cart->setToken($cartToken);
+ $cart->setToken(
+ is_string($cartToken) && preg_match('/\A[0-9a-f]{32}\z/', $cartToken)
+ ? $cartToken
+ : TokenGenerator::generateToken()
+ );
}
/** @var CartProduct|null $cartProduct */
Index: tests/Functional/ApiPlatform/CartProductResourceTest.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/tests/Functional/ApiPlatform/CartProductResourceTest.php b/tests/Functional/ApiPlatform/CartProductResourceTest.php
--- a/tests/Functional/ApiPlatform/CartProductResourceTest.php (revision 2252d8f735414967db11cf5f14e017509dce76c8)
+++ b/tests/Functional/ApiPlatform/CartProductResourceTest.php (date 1784821053104)
@@ -10,6 +10,7 @@
use App\Entity\User;
use App\Repository\UserRepository;
use App\Tests\TestUtils\Fixtures\UserFixtures;
+use App\Utils\Generator\TokenGenerator;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
@@ -317,6 +318,24 @@
$this->assertContextLinesUnchanged($context);
}
+ public function testDuplicateCartProductReturnsConflictWithoutChangingState(): void
+ {
+ $client = self::createClient();
+ $context = $this->createCartContext();
+ $this->setCartToken($client, $context['tokenA']);
+ $countBefore = $this->countCartProducts();
+
+ $this->requestPost($client, [
+ 'cart' => $this->cartIri($context['cartA']),
+ 'product' => $this->productIri($context['productA']),
+ 'quantity' => 7,
+ ]);
+
+ self::assertResponseStatusCodeSame(Response::HTTP_CONFLICT);
+ self::assertSame($countBefore, $this->countCartProducts());
+ $this->assertContextLinesUnchanged($context);
+ }
+
#[DataProvider('invalidPostSemanticQuantities')]
public function testPostRejectsSemanticInvalidQuantity(mixed $quantity, string $message): void
{
@@ -543,8 +562,8 @@
{
$entityManager = self::getContainer()->get(EntityManagerInterface::class);
$suffix = str_replace('.', '', uniqid('', true));
- $tokenA = 'cart-a-'.$suffix;
- $tokenB = 'cart-b-'.$suffix;
+ $tokenA = TokenGenerator::generateToken();
+ $tokenB = TokenGenerator::generateToken();
$productA = (new Product())
->setTitle('Cart product A '.$suffix)
->setPrice('10.00')
Index: tests/Integration/Utils/Manager/OrderManagerMoneyTest.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/tests/Integration/Utils/Manager/OrderManagerMoneyTest.php b/tests/Integration/Utils/Manager/OrderManagerMoneyTest.php
--- a/tests/Integration/Utils/Manager/OrderManagerMoneyTest.php (revision 2252d8f735414967db11cf5f14e017509dce76c8)
+++ b/tests/Integration/Utils/Manager/OrderManagerMoneyTest.php (date 1784821058201)
@@ -12,6 +12,7 @@
use App\Entity\User;
use App\Entity\StaticStorage\OrderStaticStorage;
use App\Utils\Manager\OrderManager;
+use App\Utils\Generator\TokenGenerator;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\TestWith;
@@ -54,7 +55,7 @@
->setEmail('order-money-'.uniqid('', true).'@example.test')
->setPassword('not-used-by-this-test')
->setIsVerified(true);
- $cart = new Cart();
+ $cart = (new Cart())->setToken(TokenGenerator::generateToken());
$this->entityManager->persist($user);
Index: tests/Functional/ApiPlatform/OrderCheckoutResourceTest.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/tests/Functional/ApiPlatform/OrderCheckoutResourceTest.php b/tests/Functional/ApiPlatform/OrderCheckoutResourceTest.php
--- a/tests/Functional/ApiPlatform/OrderCheckoutResourceTest.php (revision 2252d8f735414967db11cf5f14e017509dce76c8)
+++ b/tests/Functional/ApiPlatform/OrderCheckoutResourceTest.php (date 1784821054859)
@@ -14,6 +14,7 @@
use App\Repository\UserRepository;
use App\Tests\TestUtils\Fixtures\UserFixtures;
use App\Utils\Money\DecimalMoney;
+use App\Utils\Generator\TokenGenerator;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
@@ -379,7 +380,7 @@
{
$entityManager = $this->getEntityManager();
$suffix = str_replace('.', '', uniqid('', true));
- $token = 'checkout-'.$suffix;
+ $token = TokenGenerator::generateToken();
$cart = (new Cart())->setToken($token);
$lines = [];
Index: migrations/Version20260723000000.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/migrations/Version20260723000000.php b/migrations/Version20260723000000.php
new file mode 100644
--- /dev/null (date 1784821059908)
+++ b/migrations/Version20260723000000.php (date 1784821059908)
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DoctrineMigrations;
+
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\Migrations\AbstractMigration;
+
+final class Version20260723000000 extends AbstractMigration
+{
+ public function getDescription(): string
+ {
+ return 'Enforce cart identity and cart product uniqueness';
+ }
+
+ public function up(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE cart ALTER COLUMN token TYPE VARCHAR(32)');
+ $this->addSql('ALTER TABLE cart ALTER COLUMN token SET NOT NULL');
+ $this->addSql('ALTER TABLE cart ADD CONSTRAINT UNIQ_CART_TOKEN UNIQUE (token)');
+ $this->addSql('ALTER TABLE cart_product ADD CONSTRAINT UNIQ_CART_PRODUCT_CART_PRODUCT UNIQUE (cart_id, product_id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE cart_product DROP CONSTRAINT UNIQ_CART_PRODUCT_CART_PRODUCT');
+ $this->addSql('ALTER TABLE cart DROP CONSTRAINT UNIQ_CART_TOKEN');
+ $this->addSql('ALTER TABLE cart ALTER COLUMN token DROP NOT NULL');
+ $this->addSql('ALTER TABLE cart ALTER COLUMN token TYPE VARCHAR(255)');
+ }
+}
Index: src/Utils/ApiPlatform/Event/Subscriber/SetCartTokenSubscriber.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/Utils/ApiPlatform/Event/Subscriber/SetCartTokenSubscriber.php b/src/Utils/ApiPlatform/Event/Subscriber/SetCartTokenSubscriber.php
--- a/src/Utils/ApiPlatform/Event/Subscriber/SetCartTokenSubscriber.php (revision 2252d8f735414967db11cf5f14e017509dce76c8)
+++ b/src/Utils/ApiPlatform/Event/Subscriber/SetCartTokenSubscriber.php (date 1784821022179)
@@ -38,7 +38,7 @@
$cartToken = $event->getRequest()->cookies->get('CART_TOKEN');
- if (!$cartToken) {
+ if (!is_string($cartToken) || !preg_match('/\A[0-9a-f]{32}\z/', $cartToken)) {
$cartToken = TokenGenerator::generateToken();
}
Index: tests/Integration/Doctrine/CommerceAggregateLifecycleTest.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/tests/Integration/Doctrine/CommerceAggregateLifecycleTest.php b/tests/Integration/Doctrine/CommerceAggregateLifecycleTest.php
--- a/tests/Integration/Doctrine/CommerceAggregateLifecycleTest.php (revision 2252d8f735414967db11cf5f14e017509dce76c8)
+++ b/tests/Integration/Doctrine/CommerceAggregateLifecycleTest.php (date 1784821056559)
@@ -10,6 +10,7 @@
use App\Entity\OrderProduct;
use App\Entity\Product;
use App\Entity\User;
+use App\Utils\Generator\TokenGenerator;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
@@ -57,7 +58,7 @@
public function testCartRootPersistsAndOrphanRemovesItsLines(): void
{
[, $product] = $this->persistUserAndProduct();
- $cart = new Cart();
+ $cart = (new Cart())->setToken(TokenGenerator::generateToken());
$cartProduct = $this->newCartProduct($product);
$cart->addCartProduct($cartProduct);
@@ -97,7 +98,7 @@
public function testRemovingCartRemovesItsLinesButKeepsProduct(): void
{
[, $product] = $this->persistUserAndProduct();
- $cart = new Cart();
+ $cart = (new Cart())->setToken(TokenGenerator::generateToken());
$cart->addCartProduct($this->newCartProduct($product));
$this->entityManager->persist($cart);
$this->entityManager->flush();
@@ -279,7 +280,10 @@
private function insertRawCartProduct(Connection $connection): array
{
[, $productId] = $this->insertRawUserAndProduct($connection);
- $connection->executeStatement('INSERT INTO cart (token, created_at) VALUES (NULL, CURRENT_TIMESTAMP)');
+ $connection->executeStatement(
+ 'INSERT INTO cart (token, created_at) VALUES (?, CURRENT_TIMESTAMP)',
+ [TokenGenerator::generateToken()],
+ );
$cartId = (int) $connection->lastInsertId();
$connection->executeStatement(
'INSERT INTO cart_product (cart_id, product_id, quantity) VALUES (?, ?, 1)',
Index: tests/Functional/ApiPlatform/CartResourceTest.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/tests/Functional/ApiPlatform/CartResourceTest.php b/tests/Functional/ApiPlatform/CartResourceTest.php
new file mode 100644
--- /dev/null (date 1784821607791)
+++ b/tests/Functional/ApiPlatform/CartResourceTest.php (date 1784821607791)
@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Tests\Functional\ApiPlatform;
+
+use App\Entity\Cart;
+use App\Utils\Generator\TokenGenerator;
+use Doctrine\ORM\EntityManagerInterface;
+use PHPUnit\Framework\Attributes\Group;
+use Symfony\Component\BrowserKit\Cookie;
+use Symfony\Component\HttpFoundation\Response;
+
+#[Group(name: 'functional')]
+class CartResourceTest extends ResourceTestUtils
+{
+ public function testCartPostGeneratesAValidTokenWhenOwnershipCookieIsMissing(): void
+ {
+ $client = self::createClient();
+ $client->request(
+ 'POST',
+ '/api/carts',
+ [],
+ [],
+ self::REQUEST_HEADERS,
+ '{}',
+ );
+
+ self::assertResponseStatusCodeSame(Response::HTTP_CREATED);
+ $document = $this->getResponseDecodedContent($client);
+ self::assertIsString($document['token']);
+ self::assertMatchesRegularExpression('/\A[0-9a-f]{32}\z/', $document['token']);
+ }
+
+ public function testCartPostGeneratesAValidTokenForAnInvalidOwnershipCookie(): void
+ {
+ $client = self::createClient();
+ $client->getCookieJar()->set(new Cookie('CART_TOKEN', 'wrong-token'));
+ $client->request(
+ 'POST',
+ '/api/carts',
+ [],
+ [],
+ self::REQUEST_HEADERS,
+ '{}',
+ );
+
+ self::assertResponseStatusCodeSame(Response::HTTP_CREATED);
+ $document = $this->getResponseDecodedContent($client);
+ self::assertIsString($document['token']);
+ self::assertMatchesRegularExpression('/\A[0-9a-f]{32}\z/', $document['token']);
+ }
+
+ public function testCartPostDuplicateTokenReturnsConflict(): void
+ {
+ $token = TokenGenerator::generateToken();
+ $client = self::createClient();
+ $entityManager = self::getContainer()->get(EntityManagerInterface::class);
+ $cart = (new Cart())->setToken($token);
+ $entityManager->persist($cart);
+ $entityManager->flush();
+
+ $client->getCookieJar()->set(new Cookie('CART_TOKEN', $token));
+ $client->request(
+ 'POST',
+ '/api/carts',
+ [],
+ [],
+ self::REQUEST_HEADERS,
+ '{}',
+ );
+
+ self::assertResponseStatusCodeSame(Response::HTTP_CONFLICT);
+ self::assertSame(1, $entityManager->getRepository(Cart::class)->count(['token' => $token]));
+ }
+}
Index: src/Entity/Cart.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/Entity/Cart.php b/src/Entity/Cart.php
--- a/src/Entity/Cart.php (revision 2252d8f735414967db11cf5f14e017509dce76c8)
+++ b/src/Entity/Cart.php (date 1784821556318)
@@ -13,6 +13,7 @@
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
@@ -20,13 +21,16 @@
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\ORM\Mapping\Table;
+use Doctrine\ORM\Mapping\UniqueConstraint;
use Symfony\Component\Serializer\Annotation\Groups;
#[
Table(name: '`cart`'),
Entity(repositoryClass: CartRepository::class)
]
+#[UniqueConstraint(name: 'UNIQ_CART_TOKEN', columns: ['token'])]
#[ApiResource(
+ exceptionToStatus: [UniqueConstraintViolationException::class => 409],
operations: [
new GetCollection(
normalizationContext: ['groups' => ['cart:list']],
@@ -55,7 +59,7 @@
#[Groups(['cart:list', 'cart:item'])]
protected ?int $id;
- #[Column(type: Types::STRING, length: 255, nullable: true)]
+ #[Column(type: Types::STRING, length: 32, nullable: false)]
#[Groups(['cart:list', 'cart:item', 'cart:list:write'])]
protected ?string $token;
Index: src/Entity/CartProduct.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/Entity/CartProduct.php b/src/Entity/CartProduct.php
--- a/src/Entity/CartProduct.php (revision 2252d8f735414967db11cf5f14e017509dce76c8)
+++ b/src/Entity/CartProduct.php (date 1784821557966)
@@ -11,6 +11,7 @@
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\CartProductRepository;
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
@@ -19,6 +20,7 @@
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\Table;
+use Doctrine\ORM\Mapping\UniqueConstraint;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
@@ -26,39 +28,42 @@
Table(name: '`cart_product`'),
Entity(repositoryClass: CartProductRepository::class)
]
-#[ApiResource(operations: [
- new GetCollection(
- normalizationContext: ['groups' => ['cart_product:list']],
- name: 'api_cart_products_get_collection'
- ),
- new Post(
- normalizationContext: ['groups' => ['cart_product:list:write']],
- denormalizationContext: [
- 'groups' => ['cart_product:create'],
- 'allow_extra_attributes' => false,
- ],
- securityPostDenormalize: "is_granted('CART_PRODUCT_EDIT', object)",
- name: 'api_cart_products_post_collection'
- ),
- new Get(
- normalizationContext: ['groups' => ['cart_product:item']],
- security: "is_granted('CART_PRODUCT_READ', object)",
- name: 'api_cart_products_get_item'
- ),
- new Delete(
- security: "is_granted('CART_PRODUCT_DELETE', object)",
- name: 'api_cart_products_delete_item'
- ),
- new Patch(
- inputFormats: ['json' => ['application/merge-patch+json']],
- denormalizationContext: [
- 'groups' => ['cart_product:update'],
- 'allow_extra_attributes' => false,
- ],
- security: "is_granted('CART_PRODUCT_EDIT', object)",
- name: 'api_cart_products_patch_item'
- ),
-])]
+#[UniqueConstraint(name: 'UNIQ_CART_PRODUCT_CART_PRODUCT', columns: ['cart_id', 'product_id'])]
+#[ApiResource(
+ exceptionToStatus: [UniqueConstraintViolationException::class => 409],
+ operations: [
+ new GetCollection(
+ normalizationContext: ['groups' => ['cart_product:list']],
+ name: 'api_cart_products_get_collection'
+ ),
+ new Post(
+ normalizationContext: ['groups' => ['cart_product:list:write']],
+ denormalizationContext: [
+ 'groups' => ['cart_product:create'],
+ 'allow_extra_attributes' => false,
+ ],
+ securityPostDenormalize: "is_granted('CART_PRODUCT_EDIT', object)",
+ name: 'api_cart_products_post_collection'
+ ),
+ new Get(
+ normalizationContext: ['groups' => ['cart_product:item']],
+ security: "is_granted('CART_PRODUCT_READ', object)",
+ name: 'api_cart_products_get_item'
+ ),
+ new Delete(
+ security: "is_granted('CART_PRODUCT_DELETE', object)",
+ name: 'api_cart_products_delete_item'
+ ),
+ new Patch(
+ inputFormats: ['json' => ['application/merge-patch+json']],
+ denormalizationContext: [
+ 'groups' => ['cart_product:update'],
+ 'allow_extra_attributes' => false,
+ ],
+ security: "is_granted('CART_PRODUCT_EDIT', object)",
+ name: 'api_cart_products_patch_item'
+ ),
+ ])]
class CartProduct
{
#[Id, GeneratedValue, Column(type: Types::INTEGER)]