Merx

Personalized products

Granny embroiders a name on any pair of socks you like. Anna’s pair and Ben’s pair are the same product, but not the same line in the cart.

Every cart item has a data array for things like a name. Merx stores it with the item and carries it into the order, and what it means is up to you.

Ask for the name

The form from Adding products gets one more input. Call it data[name] and it arrives in the item’s data.

/site/templates/product.php
<form id="product-form" action="<?= url('api/shop/cart') ?>" method="post">
  <input type="hidden" name="page" value="<?= $page->uuid() ?>">
  <label>
    Name to embroider
    <input type="text" name="data[name]" maxlength="20">
  </label>
  <input type="number" name="quantity" min="1" value="1">
  <button>Add to cart</button>
</form>

Merx tells cart items apart by their key, which defaults to the page’s UUID. Adding the same key again adds up the quantities and replaces data, so Ben’s pair would turn Anna’s into a second pair for Ben. Give every name a key of its own.

script.js
const productForm = document.getElementById('product-form');
productForm.addEventListener('submit', async (event) => {
  event.preventDefault();
  const formData = new FormData(productForm);

  // One line per name
  const name = formData.get('data[name]');
  if (name) {
    formData.set('key', `${formData.get('page')}:${name}`);
  }

  const response = await fetch(productForm.action, {
    method: 'post',
    body: formData,
  });
  const json = await response.json();
  console.log(json.data);
});

Two pairs for Anna still share one line, with a quantity of 2.

Show it in the cart

The name comes back in the item’s data. Add it to the cart element from Showing the cart.

script.js
${items.map(item => `
  <li>
    <strong>${item.title}</strong><br>
    ${item.data?.name ? `Name: ${item.data.name}<br>` : ''}
    Price: ${item.price?.price ?? 'On request'}
  </li>
`).join('')}

Check the name on the server

maxlength only helps the visitor. The API stores whatever it receives, so check the name in the cart.add:before hook, and in cart.updateItem:before so a PATCH request can’t get around it.

/site/config/config.php
<?php

use Kirby\Exception\InvalidArgumentException;
use Wagnerwagner\Merx\Cart;
use Wagnerwagner\Merx\ListItem;

function checkEmbroidery(?array $data): void
{
  $errors = invalid($data ?? [], [
    'name' => ['maxLength' => 20, 'match' => '/^[\p{L}\p{M} -]+$/u'],
  ]);

  if ($errors !== []) {
    throw new InvalidArgumentException(
      key: 'embroidery.name',
      httpCode: 400,
      fallback: 'Enter a name of up to 20 letters.',
    );
  }
}

return [
  'hooks' => [
    'wagnerwagner.merx.cart.add:before' => function (Cart $cart, string|array|ListItem $data): void {
      checkEmbroidery(ListItem::factory($data)->data);
    },
    'wagnerwagner.merx.cart.updateItem:before' => function (Cart $cart, string $key, array $data): void {
      checkEmbroidery($data['data'] ?? null);
    },
  ],
];

invalid() runs Kirby’s validators and skips an empty name, so pairs without one pass, and so do shipping costs and discounts. Allowing only letters, spaces and hyphens also keeps markup out of the cart, the order and the confirmation email. A rejected name comes back with status 400 and the message, ready to show next to the field.

Show it on the order

data is saved with the order and comes back from $orderPage->cart(). Add the name to the cart snippet from Displaying order.

/site/snippets/cart.php
<td>
  <?= $item->title() ?>
  <?php if ($name = $item->data['name'] ?? null): ?>
    <br>Name: <?= esc($name) ?>
  <?php endif ?>
</td>

To see it in the Panel too, extend Merx’ list-items field. The wagnerwagner.merx. prefix loads Merx’ own blueprint, so yours can have the same name.

/site/blueprints/fields/list-items.yml
extends: wagnerwagner.merx.fields/list-items
fields:
  data:
    label: Embroidery
    type: object
    fields:
      name:
        type: text

The table shows it as { … }. Open the row to read the name.

Charge for the embroidery

The price still comes from the product page; data never changes it. If embroidery costs extra, add it as an item of its own, the way Shipping costs and discounts adds shipping.