Product variations

This tutorial will show you how to use PageTable (or repeater) field to create product variations

This tutorial shows you also many ways how you can extend and customize the Padloper. So even if you don't need variations, you might find this tutorial helpful.

What we are going to build?

I think most popular example of variations are t-shirts with different size and colors. So let's use this as an example. As always, Padloper tries to do as little as possible and let you use ProcessWire as much as possible. So it is totally up to you how you want to define colors and sizes. You can use options field, you can use pages or you can even go with text field - for Padloper it doesn't matter. We also want to all variations have their own stock and price (but use parent product's price if variation doesn't have one).

This tutorial continues from installation tutorial, so we already have Padloper setup with one product template - which we now extend to allow having variations.

Create the fields and variation template

This time we want to use simple dropdowns for both variables, so go ahead and create two new fields, using Options fieldtype (you might need to install that core module first). Name the fields as "size" and "color" and give them some possible options. After that go and create new template and call it "product-variation" (the name doesn't matter, can be anything). Attach new size and color fields into product variation template, also add price field you are using. If you want to do stock management, then add pad_quantity field. So your product-variation template should look something like this:

If you want to give all variations an unique title, then leave the title as is. If you want to call your variable with combination of size and color, then you can click the title and choose visibility: hidden, not shown in editor and make it not required also. If your title is not global field, then you can of course just remove it from the template.

After you have setup your template and fields, then we need to create the "variable matrix" using pagetable. So go ahead and create new PageTable field (lets call it "variations" and choose "product-variation" as template. Finally attach your new variations field into your product template. (Tip: you can use field dependencies to create checkbox that shows/hides variations pagetable versus simple price field).

Creating some variations

This is how page with our product template should look like, when few variations are added:

From content management side everything looks great. You can add new variations, choose all kinds of variables there, have custom price and stock etc. But Padloper needs some teaching to understand about your new shiny variations.

What we need to do is first tell Padloper, that our new product-variation is valid product template too. So head into PadCart module settings and choose product-variation as a valid Product template. Now starts the interesting part and that is template coding. There are million ways how you might want to show your product variations, but I am a bit boring person and will show simple radio button list. So go ahead and edit your product template:

if ($page->variations->count) {
  $content .= "<form method='post' class='padloper-cart-add-product' action='" . $config->urls->root . "padloper/add/'>";
  foreach ($page->variations as $p) {
    $content .= "<input type='radio' name='product_id' value='{$p->id}'>" . $p->color->title . " " . $p->size->title . "</input><br />";
  }
  $content .= "<input type='submit' name='pad_submit' value='" . __("Add to cart") . "' /></form>";
} else {
  $content .= $modules->get("PadRender")->addToCart();
}

What that code does it first checks if there is any variations (if you created the checkbox for variations, then use that instead). If there isn't, then it will use regular addToCart() template from PadRender-module. But if there is, it will render custom form right away. It is very simple and mimics the addToCart form very closely (which you can find from Padloper/templates/cart-add-product.php). This is how your actual template should look like:

Customizing title and price

We are almost finished now. What we need to do now is tell Padloper how the title for variations is created (remember, we wanted to hide the title field). So now we want to call our variations with formula of <product title>: <size> <color>. Also if variation doesn't have price set, then we use the price from the actual product template instead. This kind of things can be done easily through hooks. Here is very simple autoload module, that modifies the titles and prices of product variations to make them work like we want to. Create new file into site/modules/CustomHooksForVariations.module and paste this (please also go through the code, it is not complicated):

class CustomHooksForVariations extends WireData implements Module {

  public static function getModuleInfo() {
    return array(
      'title' => 'Custom hooks for variations',
      'version' => 1,
      'summary' => 'Updates variation title, price and tax class based on parent product.',
      'singular' => true,
      'autoload' => true,
      );
  }

  public function init() {

    /*
     * Here we define all our hooks. If you look PadCart.module, you will see that those
     * three methods we are hooking are all very simple and made only for hooking.
     */
    $this->addHookAfter('PadCart::getProductTitle', $this, 'customizeTitle');
    $this->addHookAfter('PadCart::getProductPrice', $this, 'customizePrice');
  }

  public function customizeTitle($event) {

    // This way we can access the arguments that are send to hooked method
    $product = $event->arguments('product');

    // We only want to manipulate title if we have variation in hand
    if ($product->template->name != "product-variation") return;

    // Now we need to find the actual product this variation belongs to
    // "variations" is name of our PageTable field
    $p = $this->pages->get("variations=$product");

    // For some reason parent product is not found, so let's fail silently
    if ( ! $p->id) return;


    // The modified return value is set here
    $event->return = $p->title . ": " . $product->color->title . " " . $product->size->title;

  }

  public function customizePrice($event) {

    $product = $event->arguments('product');

    // We only want to manipulate title if we have variation in hand
    if ($product->template->name != "product-variation") return;

    // $event->object returns the object we hooked in, in this case PadCart object.
    $pricefield = $event->object->pricefield;

    // If the variation has price, we use it
    if ($product->$pricefield) return;

    // Otherwise we use price from "parent" product

    $p = $this->pages->get("variations=$product");

    $event->return = $p->$pricefield;
  }
}

Save that with name CustomHooksForVariations.module into site/modules directory and install it. Now you should see meaningful titles and prices on your cart:

If you want to customize your cart even further (showing images or variables with different font etc), then you should copy corresponding template files from site/modules/Padloper/templates into site/templates/padloper and edit them there. If Padloper finds template file from site/templates/padloper, it will use it instead.

Creating all variations automatically

In the setup above we have to create all variations one by one. Depending on your situation, this might be ideal. But if you have always or often the exact same variations, then this approach will add huge amount of manual work creating all those variations. As always with ProcessWire, simple autoload module can make a huge difference. Let's create method, that creates all possible color and size variations for us.

Open up the CustomHooksForVariations.module and add this line to init() method:

$this->addHookAfter('Pages::saveReady', $this, 'createVariations');

Then you need to add createVariations() method:

  /**
   * This method creates PageTable pages that are our variations. It makes sure that all
   * possible combinations can be found.
   */
  public function createVariations($event) {
    $page = $event->arguments[0];

    // If the page saved is not product, just skip
    if ($page->template->name != "product") return;

    // If this product doesn't use variations, just skip
    // Other option would be use another field like "create missing variations"
    // and create variations only when especially required
    if ($page->use_variations == 0) return;

    $colorField = $this->fields->get("color");
    $colors = $colorField->type->getOptions($colorField);

    $sizeField = $this->fields->get("size");
    $sizes =  $sizeField->type->getOptions($sizeField);

    $variationsCount = count($sizes) * count($colors);

    // Let's check if we have as many options as possible (or maybe even some duplicates) and skip if we have
    if ($page->variations->count() >= $variationsCount) return;

    // Now we loop through all possible variations, check if that kind of variable already exists and if not, we create it
    foreach ($sizes as $size) {
      foreach ($colors as $color) {
        $variation = $page->variations->get("color=$color, size=$size");
        if ($variation->id) continue;

        $newVariation = new Page();
        $newVariation->template = "product-variation";
        $newVariation->color = $color;
        $newVariation->size = $size;
        $newVariation->title = $size->title . " " . $color->title; // While title is not used, pw creates pretty name based on this
        $newVariation->save();

        $page->variations->add($newVariation);
      }
    }
  }

That's it. You might need to modify the code a little depending on how you have named your templates and files, how many different variables you have in your variation template etc.

Tips and tricks

Padloper newsletter

To get occasional updates about new features and discounts