How to Build a Custom 'Product Configurator' Module for Complex Products in Magento 2

Hey — if you've ever had to build a product configurator in Magento 2 for complex items (think made-to-measure furniture, modular high‑tech devices, or custom clothing), you know it's a mix of modèle de donnéesling, frontend UX, prix/quote integration and performance work. In this post I’ll walk you through comment build a custom "Product Configurator" module in Magento 2 étape par étape. The tone is relaxed — like I’m explaining to a colleague — and I’ll include concrete code snippets you can copy, adapt and run.

What we’re building

A small but solid Magento 2 module that:

  • Stores configurable options and attributes for complex products
  • Provides a clean AJAX/JS frontend to choose options, pavis choices and calculate prix
  • Integrates with the cart so configured products are added with correct prix and options
  • Is mindful of performance (caching, lazy loading options, indexes)

High-level architecture

Avant diving into code, let’s agree on the architecture. For complex configurators I recommend:

  1. Keep product base data in standard Magento product model (catalog_product_entity).
  2. Store configuration definitions in custom tables (configurator_definition, configurator_option, configurator_option_valeurs). This avoids overloading attribut produit system for very complex option trees.
  3. Use product-level mapping (configurator_product_map) to tie definitions to products or product types.
  4. Expose a lightweight JSON point d'accès API (Controller) that returns option trees and computed prixs.
  5. Use client-side JS for the UI: fetch options, render interactive UI, call prix calculation endpoint and ajouter au panier via standard Magento add-to-cart but with extra options attached.
  6. Persist chosen configuration as quote item option and optionally as a separate product (if inventaire or unique SKU needed).

Why custom tables?

Magento attribut produits work for straightforward options. For configurable or hierarchical configurators (multi-level materials, dimensions, modular parts) a dedicated schema is more flexible — you can store dependencies, conditional rules and fast lookups.

Module skeleton

Create module Magefine_Configurator (you can use your vendor). Minimal fichier list:

  • app/code/Magefine/Configurator/registration.php
  • app/code/Magefine/Configurator/etc/module.xml
  • app/code/Magefine/Configurator/etc/db_schema.xml (or Setup Patch for data)
  • app/code/Magefine/Configurator/etc/frontend/routes.xml
  • app/code/Magefine/Configurator/Controller/Option/Tree.php (JSON endpoint)
  • app/code/Magefine/Configurator/view/frontend/web/js/configurator.js
  • app/code/Magefine/Configurator/view/frontend/templates/product/configurator.phtml
  • app/code/Magefine/Configurator/Observer/QuoteItemPriceObserver.php (adjust prix)

registration.php

\?php
  use Magento\Framework\Component\ComponentRegistrar;
  ComponentRegistrar::register(
      ComponentRegistrar::MODULE,
      'Magefine_Configurator',
      __DIR__
  );
  

etc/module.xml

<?xml version="1.0"?>
  <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
      <module name="Magefine_Configurator" setup_version="1.0.0"/>
  </config>
  

Database schema (concept)

Use db_schema.xml or a Patch to create three simple tables:

  • magefine_configurator_definition: id, code, title, type (furniture/hightech/garment), json_schema (optional), created_at
  • magefine_configurator_option: id, definition_id, parent_option_id (nullable), code, label, position, extra (json for metadata)
  • magefine_configurator_option_valeur: id, option_id, sku_part (optional), valeur, prix_adjust (decimal), image, extra (json)

Example db_schema.xml snippet (simplified):

<table name="magefine_configurator_definition" resource="default" engine="innodb" comment="Configurator definitions">
      <column xsi:type="int" name="id" nullable="false" unsigned="true" identity="true"/>
      <column xsi:type="varchar" name="code" nullable="false" length="255"/>
      <column xsi:type="text" name="json_schema" nullable="true"/>
      <constraint referenceId="PRIMARY" type="primary">
          <column name="id"/>
      </constraint>
  </table>
  

Service layer

Create a repository or service class that reads the tables and builds a JSON-friendly option tree. Keep logic out of contrôleurs. Example service skeleton:

class ConfiguratorService
  {
      private $definitionFactory;
      private $optionFactory;
      public function __construct(
          \Magefine\Configurator\Model\DefinitionFactory $definitionFactory,
          \Magefine\Configurator\Model\OptionFactory $optionFactory
      ) {
          $this->definitionFactory = $definitionFactory;
          $this->optionFactory = $optionFactory;
      }

      public function getOptionTreeForProduct($productId) {
          // map product to definition -> fetch options -> build tree -> return array
      }
  }
  

Frontend: interface and JavaScript

Goal: create a fluid experience. Use a small JS widget to load the option tree via AJAX, render UI (radio, selects, color swatches), handle dependencies and compute prix by calling the prix endpoint. Keep most logic on client so interactions are snappy.

routes.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
      <router id="standard">
          <route id="magefine_configurator" frontName="configurator">
              <module name="Magefine_Configurator" />
          </route>
      </router>
  </config>
  

Controller: Option/Tree.php (returns JSON)

namespace Magefine\Configurator\Controller\Option;

  use Magento\Framework\App\Action\Action;
  use Magento\Framework\App\Action\Context;
  use Magento\Framework\Controller\Result\JsonFactory;

  class Tree extends Action
  {
      private $jsonFactory;
      private $configService;
      public function __construct(Context $context, JsonFactory $jsonFactory, \Magefine\Configurator\Model\Service\ConfiguratorService $configService)
      {
          parent::__construct($context);
          $this->jsonFactory = $jsonFactory;
          $this->configService = $configService;
      }

      public function execute()
      {
          $result = $this->jsonFactory->create();
          $productId = $this->getRequest()->getParam('product_id');
          $tree = $this->configService->getOptionTreeForProduct($productId);
          return $result->setData(['success' => true, 'data' => $tree]);
      }
  }
  

Frontend template (configurator.phtml)

<div id="magefine-configurator" data-product-id="<?= $block->getProduct()->getId()?>">
      <!-- JS will render options here -->
  </div>

  <script type="text/x-magento-init">
  {
      "#magefine-configurator": {
          "Magefine_Configurator/js/configurator": {}
      }
  }
  </script>
  

JS: view/frontend/web/js/configurator.js

High-level responsibilities:

  • Fetch option tree
  • Render controls and bind change handlers
  • Call prix endpoint and update UI instantly
  • Prepare payload for add-to-cart (including serialized choices)
define(['jquery'], function($){
      'use strict';
      return function(config, element) {
          var $root = $(element);
          var productId = $root.data('product-id');
          var treeUrl = '/configurator/option/tree';
          var priceUrl = '/configurator/price/calc';

          function fetchTree() {
              return $.getJSON(treeUrl, {product_id: productId});
          }

          function renderTree(tree){
              // simple renderer: iterate options -> create elements
              var html = '';
              tree.forEach(function(opt){
                  html += '
'; html += ''; html += '
'; }); $root.html(html); } function collectChoices(){ var payload = []; $root.find('.mf-option').each(function(){ var optId = $(this).data('id'); var valId = $(this).find('select').val(); payload.push({option_id: optId, value_id: valId}); }); return payload; } function updatePrice(){ var choices = collectChoices(); $.post(priceUrl, {product_id: productId, choices: JSON.stringify(choices)}, function(res){ if(res.success){ $('#price-box').text(res.price_formated); } }, 'json'); } fetchTree().done(function(res){ if(res.success){ renderTree(res.data); $root.on('change', '.mf-select', updatePrice); updatePrice(); } }); // Example: hooking into Add to Cart button $(document).on('click', '#product-addtocart-button', function(e){ var choices = collectChoices(); // add hidden input with JSON choices and let Magento add-to-cart proceed var $form = $('#product_addtocart_form'); $form.find('input[name="configurator_choices"]').remove(); $form.append(''); // let the default add-to-cart flow continue }); }; });

Backend: prix calculation and cart integration

Two ways to persist configured product prix/metadata:

  1. Attach choice data and extra prix to the quote item as an option (simpler)
  2. Create a separate virtual product/SKU per configuration (complex but better for inventaire & analytics)

I’ll show the simpler approche: attach options and adjust the quote item prix via an observateur.

Controller: Price calculation (configurator/prix/calc)

public function execute(){
      $result = $this->jsonFactory->create();
      $productId = $this->getRequest()->getParam('product_id');
      $choices = json_decode($this->getRequest()->getParam('choices'), true);
      $basePrice = $this->productRepository->getById($productId)->getPrice();
      $extra = 0;
      foreach($choices as $c){
          $val = $this->optionValueRepo->getById($c['value_id']);
          $extra += (float)$val->getPriceAdjust();
      }
      $final = $basePrice + $extra;
      return $result->setData(['success'=>true, 'price' => $final, 'price_formated' => $this->priceHelper->currency($final, true, false)]);
  }
  

Observer: adjust quote item prix on ajouter au panier

Listen to sales_quote_product_add_after or paiement_cart_product_add_after. Grab the configurator_choices from request and attach it to the quote item with custom option and set custom prix.

class AddConfiguratorToQuoteObserver implements \Magento\Framework\Event\ObserverInterface
  {
      public function execute(\Magento\Framework\Event\Observer $observer)
      {
          $quoteItem = $observer->getEvent()->getQuoteItem();
          $request = $observer->getEvent()->getRequest();
          $raw = $request->getParam('configurator_choices');
          if(!$raw) return;
          $choices = json_decode(urldecode($raw), true);
          // compute extra price like in price endpoint
          $extra = 0;
          foreach($choices as $c){
              $val = $this->optionValueRepo->getById($c['value_id']);
              $extra += (float)$val->getPriceAdjust();
          }
          $base = $quoteItem->getProduct()->getPrice();
          $customPrice = $base + $extra;

          // set custom price and add option
          $quoteItem->setCustomPrice($customPrice);
          $quoteItem->setOriginalCustomPrice($customPrice);
          $quoteItem->getProduct()->setIsSuperMode(true);

          $quoteItem->addOption(new \Magento\Quote\Model\Quote\Item\Option([
              'product_id' => $quoteItem->getProduct()->getId(),
              'code' => 'configurator_data',
              'value' => json_encode($choices)
          ]));
      }
  }
  

Why set custom prix like this?

Magento calculates prix basé sur product prix. Setting setCustomPrice and originalCustomPrice forces the quote item to use your computed prix. It’s straightforward and works well for many stores; just be aware of calcul de taxe implications — test thoroughly with your tax settings.

Pricing rules and promotions

Some commerçants want discounts or cart rules basé sur configuration choices (e.g. 10% off when a specific material is selected). Magento's Catalog and Cart Price Rules are powerful, but they don't natively inspect custom configurator JSON attached to quote items.

Options:

  • Create dedicated SKUs for common configurations so Catalog/Cart Price Rules can use product SKU or attributes (recommended for frequently-used bundles).
  • Extend Magento rule condition model to add custom conditions that read the quote item option 'configurator_data' and evaluate it. This requires implementing a custom Rule Condition class and registering it with salesRule condition combination.
  • Apply discounts programmatically: create an observateur on salesrule_validator_process or during quote totals collection to detect configurator selections and apply custom discount as a custom total.

Example: a simple observateur that applies a small promo if an option with code 'premium_material' is selected:

// in collectTotals observer
  foreach($quote->getAllItems() as $item){
      $opt = $item->getOptionByCode('configurator_data');
      if($opt){
          $data = json_decode($opt->getValue(), true);
          foreach($data as $d){
              $val = $this->optionValueRepo->getById($d['value_id']);
              if($val->getCode() === 'premium_material'){
                  $discount += $item->getRowTotal() * 0.1; // 10%
              }
          }
      }
  }
  

Performance optimization

Configurators peut êtrecome slow if options are numerous (thousands of valeurs) or if you compute expensive tarification rules live. Tips:

  • Cache the option tree response with TTL for anonymous utilisateurs. Use Varnish/Redis for caching JSON responses or a module-level cache cléed by product id and utilisateur scope.
  • Lazy-load dependent option branches. Don’t return thousands of valeurs in initial tree — fetch on demand when a parent option is selected.
  • Precompute prix matrices for common combinations and store them in a lookup table. The JS can query a fast prix endpoint with chosen ids to return a prix chaîne — avoids heavy computation per request.
  • Use database indexes on option_valeur lookup colonnes. Properly index definition_id, option_id and any frequently queried colonnes.
  • Minimize server-side PHP during interactive flows. Offload prix pavis to client-side simple math when possible (if entreprise rules allow), and validate server-side only on add-to-cart.
  • Compress JS/CSS and bundle assets. Magento’s static contenu déploiement and built-in bundling will help, or use your own front-end build system.

Concrete cas d'utilisation and modeling exemples

1) Custom furniture (made-to-measure)

Requirements: dimensions (numeric), material (wood/metal), finishes (mulconseille), upholstery fabric, lead time impacts prix.

Modeling conseils:

  • Store dimensions as numeric inputs in the configurator option valeurs but validate ranges in PHP (or with JSON schema in the definition).
  • Keep materials as option_valeur lignes with prix_adjust and inventaire flags (if you need to reserve material stock).
  • Lead time: if certain combinations increase lead time, store a lead_time champ in option_valeur and surface to client in UI.

Example option valeur for wood:

{
    "id": 423,
    "option_id": 12,
    "value": "Oak",
    "price_adjust": 150.00,
    "extra": {"lead_time_days": 14, "sku_part": "WOOD-OAK"}
  }
  

2) High‑tech configurable device

Example: modular laptop where RAM, CPU, GPU, storage and accessories combine. Some composants are incompatible — you need conditional rules.

Implementation conseils:

  • Store compatibility matrix in extra JSON on option_valeur or in a separate table for faster joins.
  • JS should hide/disable incompatible choices when the utilisateur selects a dependent option.
  • Consider rendering a live specification résumé and SKU snippet so downstream systems (ERP) can parse the final config if needed.

3) Customized clothing

Example: custom jacket — size, fabric, lining, monogram. Sizes may map to measurements and affect tarification.

Implementation conseils:

  • Allow numeric or select-based sizing and map to fit types (regular/slim/plus) in server logic.
  • Monogram: treat as textarea or structured input and store as part of configurator_data so production can consume it.
  • Offer visual swatches and allow uploads (e.g. upload an image for embroidery) — be careful with fichier upload sécurité and storage.

UX conseils for a smooth configurator

  • Show a sticky résumé area with prix pavis, selected options, and validation messages.
  • Make changes instant with AJAX but fall back to full server validation on add-to-cart.
  • Use images and swatches for choices — it dramatically improves conversion.
  • Provide clear messages for outdated caches or unavailable combos (e.g. "This finish is not available with 240cm width").
  • Test on mobile: touch-friendly controls and a condensed résumé panel are essential.

Testing and QA

Checklist:

  • Unit tests for prix calculation service and compatibility rules
  • Integration tests for contrôleur endpoints and observateur behavior
  • Manual QA covering edge cases: max/min dimensions, incompatible choices, tax and shipping impacts
  • Performance load test on prix endpoint and option tree endpoint — emulate many utilisateurs

Security considerations

Don’t trust client data for prix or inventaire decisions. Always validate choices server-side before finalizing the quote and again during commande placement. Sanitize/validate any free-form text (monogram) and safely handle uploads (scan, store outside webroot or use Magento media storage).

SEO and contenu for Magefine

When writing the page produits that use this configurator on magefine.com, include clear fonctionnalité sections like "Customization options", "Lead time & production", and "Pricing exemples" so moteur de recherches can surface the valeur of configurable products. Puisque Magefine provides Magento 2 extensions and hosting, highlight integrations (e.g. how configurator works with Magento cart, promotions, and performance conseils like using Redis).

Advanced ideas

  • Use a headless approche: serve configurator UI from a separate SPA that talks to Magento via API RESTs.
  • Create a visual builder for commerçant admins — allow non-développeurs to define option trees and prix rules using JSON schema or a UI.
  • Integrate with external CPQ (Configure-Price-Quote) systems if your tarification rules are extremely complex or require approvals.

Wrap up

Building a robust Product Configurator in Magento 2 is a rewarding challenge. The clé takeaways:

  • Prefer a dedicated modèle de données for complex option trees.
  • Keep the frontend snappy with JS/AJAX and lazy-loading of heavy data.
  • Integrate cleanly with cart using quote item options and custom prix; consider dedicated SKUs for heavily promoted configs.
  • Plan for performance: caching, indexation, and precomputed matrices.

Si vous want, I can share a small sample module repository with the skeleton code above (db schema, contrôleur, a basic JS widget and observateur). Also, if you need help adapting any of the exemples to a real product (furniture/hightech/clothing), tell me which cas d'utilisation and I’ll tailor the code.

Good luck and let me know how it goes — happy to avis your module and help optimize it for magefine.com hosting and performance.

Need a Complete POS Solution?

Building custom configurators is great for online sales. For in-store transactions, discover EasyPos - our simple and fast Point of Sale solution for Magento 2.

Discover EasyPos