Shipping costs and discounts
Shipping costs and discounts are list items with their own type, not product pages. A hook keeps them in sync whenever the cart changes.
Every entry in the cart is a ListItem with a type. Products use the default type product, and shipping costs and discounts use a type of their own. The allowed types are credit, custom, discount, product, promotion and shipping.
Because shipping is just another list item, it needs no content page and no blueprint of its own. It’s part of $cart->total() like everything else, and filterByType() keeps it out of your product listing.
Add shipping as a list item
Build the item with new ListItem() and give it a Price of its own. A negative price makes it a discount.
use Wagnerwagner\Merx\ListItem;
use Wagnerwagner\Merx\Price;
$cart->add(new ListItem(
key: 'shipping',
title: 'Shipping',
price: new Price(price: 7.99, tax: 0.19, currency: $cart->currency()),
type: 'shipping',
));
Always pass the cart’s currency() to the Price. The cart compares the currency of every incoming item against its own and silently drops a price that does not match, which leaves you with a shipping item that costs nothing.
Recalculate whenever the cart changes
Shipping and discounts depend on what is in the cart, so they have to be recalculated after every change. There is no single cart hook — there is one pair of hooks per operation. Point cart.add:after, cart.remove:after and cart.updateItem:after at the same function; the hooks reference has the full list.
The function removes both items first and then adds back whatever currently applies. That keeps it idempotent, so it does not matter how often it runs. The static guard matters: the function changes the cart, which fires the very hooks that call it again.
<?php
use Wagnerwagner\Merx\Cart;
use Wagnerwagner\Merx\ListItem;
use Wagnerwagner\Merx\Price;
function updateCartExtras(Cart $cart): void
{
static $isUpdating = false;
// Adding and removing items fires these hooks again.
if ($isUpdating === true) {
return;
}
$isUpdating = true;
$cart->remove('shipping');
$cart->remove('discount');
if ($cart->quantity('product') > 0) {
// Free shipping from 50 onwards.
if ($cart->total()->toFloat() < 50) {
$cart->add(new ListItem(
key: 'shipping',
title: 'Shipping',
price: new Price(price: 7.99, tax: 0.19, currency: $cart->currency()),
type: 'shipping',
));
}
// 10 off when five or more products are in the cart.
if ($cart->quantity('product') >= 5) {
$cart->add(new ListItem(
key: 'discount',
title: 'Discount',
price: new Price(price: -10, currency: $cart->currency()),
type: 'discount',
));
}
}
$isUpdating = false;
}
return [
'hooks' => [
'wagnerwagner.merx.cart.add:after' => fn (Cart $cart) => updateCartExtras($cart),
'wagnerwagner.merx.cart.remove:after' => fn (Cart $cart) => updateCartExtras($cart),
'wagnerwagner.merx.cart.updateItem:after' => fn (Cart $cart) => updateCartExtras($cart),
],
];
Removing the last product empties the cart completely, because quantity('product') is then 0 and neither item is added back.
Keep them out of the product list
filterByType() returns only the items of one type, which is what you want when you render the cart.
<?php /** @var \Wagnerwagner\Merx\Cart $cart */ ?>
<ul>
<?php foreach ($cart->filterByType('product') as $item): ?>
<li><?= $item->quantity ?> × <?= $item->title ?> <?= $item->total() ?></li>
<?php endforeach ?>
</ul>
<?php foreach ($cart->filterByType('shipping') as $item): ?>
<p><?= $item->title ?>: <?= $item->total() ?></p>
<?php endforeach ?>
<?php foreach ($cart->filterByType('discount') as $item): ?>
<p><?= $item->title ?>: <?= $item->total() ?></p>
<?php endforeach ?>
<p>Total: <?= $cart->total() ?></p>
Discounts and the total
A discount is a list item with a negative price, and Merx does not stop you from discounting more than the cart is worth. It does refuse to order such a cart: isOrderable() is false as soon as the total is no longer greater than zero. Check it before you send the customer to the checkout.
<?php if ($cart->isOrderable() === false): ?>
<p>This cart cannot be ordered.</p>
<?php endif ?>
For how tax is applied to a price, see the taxes guide.