# Product variants

> If you want to sell t-shirts, for example, you probably want to sell the shirts in a specific size. This cookbook explains how to sell variations of a product.

To manage product **stock** or to define **individual prices** for each *product variant*, you need to create an own page for each *product variant*.

```filesystem title="Folder structure"
content/
  t-shirt/
    product.txt
    1_medium/
      product-variant.txt
    2_large/
      product-variant.txt
```

## Product template

On the product page, the customer should be able to select the desired variant. Instead of the hidden input field from the [Adding products](https://merx2.wagnerwagner.de/guide/getting-started/adding-products.md) there will be a `select` input field with all available variants. The value of each option is the `uuid` of the *product variant* page.

```php title="/site/templates/product.php"
<?php
/** @var Wagnerwagner\Merx\ProductPage $page */
$variants = $page->children()->template('product-variant')->listed();
?>
<form id="product-form" action="<?= url('api/shop/cart') ?>" method="post">
  <select name="page">
    <?php foreach ($variants as $variant): ?>
      <option value="<?= $variant->uuid() ?>"><?= $variant->title() ?></option>
    <?php endforeach ?>
  </select>
</form>
```

Technically the `product-variant` page is a `product` page. To get all the methods from Merx’ [`ProductPage`](https://merx2.wagnerwagner.de/reference/classes/product-page.md) class, create a model for `product-variant` and extend [`Wagnerwagner\Merx\ProductPage`](https://merx2.wagnerwagner.de/reference/classes/product-page.md).

```php title="/site/models/product-variant.php"
<?php
class ProductVariantPage extends \Wagnerwagner\Merx\ProductPage
{}
```

## Handling the price

Different t-shirt sizes normally have all the same price. Instead of adding the same price to each product variant, you can overwrite the price method of the `product-variant` page model and use the price of the parent product page.

```php title="/site/models/product-variant.php"
<?php
class ProductVariantPage extends \Wagnerwagner\Merx\ProductPage
{
  public function price(null|string|\Wagnerwagner\Merx\PricingRule $pricingRule = null): ?\Wagnerwagner\Merx\Price
  {
    return $this->parent()->price($pricingRule);
  }
}
```

You could also use the price of the parent product page only, when the price field of the product variant page is empty.

```php
return parent::price() === null ? $this->parent()->price($pricingRule) : parent::price();
```

## Extending product variant model

You can further extend the product variant page model, for example you can create a full title from the parent product page and the variant page.

```php title="/site/models/product-variant.php"
<?php
class ProductVariantPage extends \Wagnerwagner\Merx\ProductPage
{
  // …
  public function title(): Field
  {
    $fullTitle = $this->parent()->title() . ' – ' . $this->content()->title();
    return parent::title()->value($fullTitle);
  }
}
```

## Multiple variations

When a product has multiple variations (e.g. *color* **and** *size*), things will get a bit more complicated on the front-end side. 

The structure of the content folder might look like this. Each variant has an own `product-variant` page.

```filesystem title="Folder structure"
content/
  t-shirt/
    product.txt
    1_red-medium/
      product-variant.txt
    2_red-large/
      product-variant.txt
    3_red-medium/
      product-variant.txt
    4_red-large/
      product-variant.txt
```

Each variant should have fields for each attribute. In this example *color* and *size*.

```txt title="/content/t-shirt/1_red-medium/product-variant.txt"
Color: Red

----

Size: Large

----

Uuid: 96satwnkbxtyk4w9
```

### HTML

The customer will expect not one but two select fields. One for the color and one for the size.

First create an array with all *colors* and all *sizes* using [`$pages->pluck()`](https://getkirby.com/docs/reference/objects/cms/pages/pluck) and use these arrays to create two select fields. Also add a hidden `page` input field. The value of this field will late be set using JavaScript.

```php title="/site/templates/product.php"
<?php
/** @var Wagnerwagner\Merx\ProductPage $page */
$variants = $page->children()->template('product-variant')->listed();
$sizes = array_unique($variants->pluck('size'));
$colors = array_unique($variants->pluck('color'));
?>
<form id="product-form" action="<?= url('api/shop/cart') ?>" method="post">
  <select name="color">
    <?php foreach ($colors as $color): ?>
      <option value="<?= $color ?>"><?= $color ?></option>
    <?php endforeach ?>
  </select>
  <select name="size">
    <?php foreach ($sizes as $size): ?>
      <option value="<?= $size ?>"><?= $size ?></option>
    <?php endforeach ?>
  </select>
  <input type="hidden" name="page" value="">
</form>
```

To set the value of the hidden page field, you need a list of all page uuids and the respective attributes (*color* and *size*). Create this list and store it as JSON  in a `<script>` element.

```php title="/site/templates/product.php"
<?php
/** @var \Wagnerwagner\Merx\ProductPage $page */
$variants = $page->children()->template('product-variant');
$variantsArray = $variants->toArray(fn (ProductVariantPage $productVariantPage) => [
  'uuid' => (string)$productVariantPage->uuid(),
  'size' => (string)$productVariantPage->size(),
  'color' => (string)$productVariantPage->color(),
]);
$sizes = array_unique($variants->pluck('size'));
$colors = array_unique($variants->pluck('color'));
?>
<form id="product-form" action="<?= url('api/shop/cart') ?>" method="post">
  <script id="variants">
    <?= json_encode(array_values($variantsArray), JSON_UNESCAPED_SLASHES) ?>
  </script>
```

### JavaScript

Now you have all the things you need to update the value of the hidden `page` input field. 

1. Parse the JSON from the script element with the id `variants` 
2. Create an event listener for each select field.
3. Find the item with the selected attributes
4. Set the value of the hidden `page` field with the uuid of the product variant.

```js title="script.js"
// [1]
const variants = JSON.parse(document.getElementById('variants').innerText);;

function updatePageField() {
  // [3]
  const { color, size } = productForm.elements;
  const variant = variants.find((item) => item.color === color.value && item.size === size.value);

  // [4]
  productForm.elements.page.value = variant?.uuid ?? null;
}

updatePageField();

productForm.querySelectorAll('select').forEach((selectElement) => {
  // [2]
  selectElement.addEventListener('change', updatePageField);
})
```

> [!NOTE]
> You can use the same mechanism to update **product images**, showing the **available stock** or the **specific price**, when you have different prices per variant.

## Add a variant to the cart

The form posts the selected variant’s UUID as `page`, so the API route puts the right variant into the cart without any further work. To add a variant from PHP, pass the variant page as `page` and give it a `key` of your own.

```php
$cart->add([
  'page' => 'page://96satwnkbxtyk4w9',
  'key' => 't-shirt-red-large',
  'data' => [
    'color' => 'Red',
    'size' => 'Large',
  ],
]);
```

The `key` identifies the item inside the cart, so every variant needs its own. Adding the same `key` twice adds up the quantities instead of creating a second line. Title, price and tax are read from the variant page, `data` is yours to fill.

> [!CAUTION]
> [`$cart->add()`](https://merx2.wagnerwagner.de/reference/classes/cart/add.md) accepts the parameters of [`new ListItem()`](https://merx2.wagnerwagner.de/reference/classes/list-item/__construct.md) and nothing else. A key like `color` or `size` at the top level fails with `Unknown named parameter` — your own attributes belong in `data`.

### A one-off price

Most variants should get their price from the page, as described in [Handling the price](#handling-the-price). If you do need to set a price while adding, pass a [`Price`](https://merx2.wagnerwagner.de/reference/classes/price.md) object.

```php
use Wagnerwagner\Merx\Price;

$cart->add([
  'page' => 'page://96satwnkbxtyk4w9',
  'key' => 't-shirt-gold-large',
  'price' => new Price(price: 249.99, currency: $cart->currency()),
]);
```

> [!CAUTION]
> Pass the cart’s [`currency()`](https://merx2.wagnerwagner.de/reference/classes/cart/currency.md) along with the price. The cart compares the currency of every incoming item against its own and silently drops a price that does not match, which would put the variant into the cart for free. A plain `'price' => 249.99` is exactly that case.
