Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@
## Development

- Product permissions have been refined into separate "View", "Create", "Save", and "Delete" permissions.
- Added a `cart/peek-cart` controller action, which returns the existing cart for the current request without creating a new cart or setting cookies — useful for cached pages such as a header cart badge, where `Set-Cookie` responses should be avoided. ([#4263](https://github.com/craftcms/commerce/pull/4263))

## Extensibility

- Added `craft\commerce\services\ProductTypes::getViewableProductTypes()`.
- Added `craft\commerce\services\ProductTypes::getViewableProductTypeIds()`.
- Added `craft\commerce\services\ProductTypes::getCreatableProductTypeIds()`.
- Added `craft\commerce\services\Carts::peekCart()`.
- Added `craft\commerce\controllers\CartController::actionPeekCart()`.
- Deprecated `craft\commerce\services\ProductTypes::hasPermission()`. Use `$user->can()` directly instead.
- Deprecated `craft\commerce\services\ProductTypes::getEditableProductTypes()`. Use `getViewableProductTypes()` instead.
- Deprecated `craft\commerce\services\ProductTypes::getEditableProductTypeIds()`. Use `getViewableProductTypeIds()` instead.
17 changes: 16 additions & 1 deletion src/controllers/CartController.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function behaviors(): array
return array_merge(parent::behaviors(), [
'rateLimiter' => [
'class' => RateLimiter::class,
'only' => ['get-cart', 'update-cart', 'load-cart', 'complete'],
'only' => ['get-cart', 'peek-cart', 'update-cart', 'load-cart', 'complete'],
'enableRateLimitHeaders' => false,
'user' => function() {
// Only apply rate limiting when a cart number is explicitly passed
Expand Down Expand Up @@ -119,6 +119,21 @@ public function actionGetCart(): Response
]);
}

/**
* Returns the existing cart for this session without creating one, setting cookies, or touching the session.
* Returns null (as empty cart data) if no cart exists for the current session.
*/
public function actionPeekCart(): Response
{
$this->requireAcceptsJson();

$cart = Plugin::getInstance()->getCarts()->peekCart();

return $this->asSuccess(data: [
$this->_cartVariable => $cart ? $this->cartArray($cart) : null,
]);
}

/**
* Updates the cart by adding purchasables to the cart, updating line items, or updating various cart attributes.
*
Expand Down
51 changes: 51 additions & 0 deletions src/services/Carts.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,57 @@ public function getCart(bool $forceSave = false): Order
return $this->_cart;
}

/**
* Returns the existing cart for this session without creating one, setting cookies, or touching the session.
* Returns null if no cart cookie is present or no matching cart exists.
*
* @since 5.7.0
*/
public function peekCart(): ?Order
{
if (isset($this->_cart)) {
return $this->_cart;
}

if ($this->_cartNumber === false) {
return null;
}

if (!$this->_cartNumber) {
$cookieNumber = Craft::$app->getRequest()->getCookies()->getValue($this->cartCookie['name'], false);
if (!$cookieNumber) {
return null;
}
$this->_cartNumber = $cookieNumber;
}

/** @var Order|null $cart */
$cart = Order::find()
->number($this->_cartNumber)
->storeId(Plugin::getInstance()->getStores()->getCurrentStore()->id)
->isCompleted(false)
->trashed(false)
->one();

if (!$cart) {
return null;
}

// Don't return a cart that belongs to a credentialed user who isn't currently logged in
// as that user. Mirrors the privacy check in _getCart(), but without forgetting the cart
// (which would set a Set-Cookie header and defeat the purpose of this method).
$cartCustomer = $cart->getCustomer();
if ($cartCustomer && $cartCustomer->getIsCredentialed()) {
$currentUser = Craft::$app->getUser()->getIdentity();
if (!$currentUser || $currentUser->id != $cartCustomer->id) {
return null;
}
}

$this->_cart = $cart;
return $this->_cart;
}

/**
* Get the current cart for this session.
*/
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/services/CartsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,66 @@ public function testGetCartSwitchCustomer(): void
Craft::$app->getUser()->setIdentity($originalIdentity);
Craft::$app->getElements()->deleteElement($cart, true);
}

public function testPeekCartDoesNotStartCartSession(): void
{
$originalCarts = Plugin::getInstance()->getCarts();
$cartNumber = $originalCarts->generateCartNumber();
$cookieName = $originalCarts->cartCookie['name'];

$order = new Order();
$order->number = $cartNumber;
Craft::$app->getElements()->saveElement($order, false);

$carts = $this->make(Carts::class, [
'setSessionCartNumber' => function() {
self::fail('Peek cart retrieval should not update the cart session.');
},
]);
$carts->cartCookie = ['name' => $cookieName];
Plugin::getInstance()->set('carts', $carts);

$requestCookies = new \yii\web\CookieCollection();
$requestCookies->add(new \yii\web\Cookie([
'name' => $cookieName,
'value' => $cartNumber,
]));
$originalRequest = Craft::$app->getRequest();
$requestMock = $this->make(Request::class, [
'getCookies' => $requestCookies,
]);
Craft::$app->set('request', $requestMock);

try {
$cart = Plugin::getInstance()->getCarts()->peekCart();

self::assertNotNull($cart);
self::assertSame($cartNumber, $cart->number);
} finally {
Craft::$app->set('request', $originalRequest);
Craft::$app->getElements()->deleteElement($order, true);
}
}

public function testPeekCartReturnsNullWithNoCookie(): void
{
$cookieName = Plugin::getInstance()->getCarts()->cartCookie['name'];

$carts = $this->make(Carts::class);
$carts->cartCookie = ['name' => $cookieName];
Plugin::getInstance()->set('carts', $carts);

$originalRequest = Craft::$app->getRequest();
$requestMock = $this->make(Request::class, [
'getCookies' => new \yii\web\CookieCollection(),
]);
Craft::$app->set('request', $requestMock);

try {
$cart = Plugin::getInstance()->getCarts()->peekCart();
self::assertNull($cart);
} finally {
Craft::$app->set('request', $originalRequest);
}
}
}
Loading