How to Build a Custom 'Product Comparison by Attribute' Module in Magento 2

Why build a custom "Product Comparison by Attribute" module in Magento 2?

We all know Magento 2 comes with a basic product compare fonctionnalité. It’s fine for simple stores, but when you need attribute-based comparisons (think comparing technical specs across many SKUs), you’ll want full control: which attributes show, how lignes are grouped, how comparisons are stored and exported, and how the UI behaves on mobile.

In this post I’ll walk you through building a custom module that:

  • Creates custom attribut produits and a dedicated comparison table in the database.
  • Provides a responsive AJAX-driven comparison block for the frontend.
  • Integrates comparison buttons in category lists and page produits.
  • Shows performance patterns and optimizations for large catalogs.
  • Explains extension points: exporting comparisons and auto-suggestion of similar products.

High-level architecture

Think of three main layers:

  1. Data layer: a custom table to store saved comparisons and product-to-comparison links, plus product EAV attributes that utilisateurs compare.
  2. Business layer: models, repositories and services to manipulate comparisons and generate comparison matrices by attributes.
  3. Presentation layer: a block with a phtml template and JS that fetches and updates the comparison via REST-like contrôleur endpoints (AJAX).

Module skeleton

Create a module named Magefine_ProductCompare (replace Magefine with your vendor if you want). Folder structure (app/code/Magefine/ProductCompare):

app/code/Magefine/ProductCompare/
├── etc
│   ├── module.xml
│   ├── frontend
│   │   └── routes.xml
│   ├── db_schema.xml
│   └── di.xml
├── registration.php
├── Composer.json (optional)
├── Setup
│   └── Patch
│       └── Data
│           └── AddComparisonAttributes.php
├── Model
│   ├── Comparison.php
│   ├── ResourceModel
│   │   └── Comparison.php
│   │   └── Collection.php
├── Controller
│   └── Ajax
│       ├── Add.php
│       ├── Remove.php
│       └── GetMatrix.php
├── view
│   └── frontend
│       ├── templates
│       │   └── compare/block.phtml
│       └── web
│           └── js
│               └── compare.js
└── etc
    └── acl.xml (if you need admin endpoints)

1) Data layer: creating a comparison table and attribut produits

Nous allons create a dedicated table to store comparisons metadata and a linking table to store which products are in a comparison list. Use Magento's declarative schema (db_schema.xml):

<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
    <table name="magefine_product_compare" resource="default" engine="innodb" comment="Magefine product comparisons">
        <column xsi:type="int" name="compare_id" unsigned="true" nullable="false" identity="true" comment="Compare ID"/>
        <column xsi:type="varchar" name="session_id" nullable="true" length="255" comment="Session or user identifier"/>
        <column xsi:type="int" name="customer_id" nullable="true" unsigned="true" comment="Customer ID"/>
        <column xsi:type="timestamp" name="created_at" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/>
        <constraint referenceId="PRIMARY" type="primary" columns="compare_id"/>
    </table>

    <table name="magefine_product_compare_item" resource="default" engine="innodb" comment="Products inside a comparison">
        <column xsi:type="int" name="item_id" unsigned="true" nullable="false" identity="true"/>
        <column xsi:type="int" name="compare_id" unsigned="true" nullable="false"/>
        <column xsi:type="int" name="product_id" unsigned="true" nullable="false"/>
        <constraint referenceId="PRIMARY" type="primary" columns="item_id"/>
        <constraint referenceId="CMP_COMPARE_ID" type="foreign" referenceTable="magefine_product_compare" referenceColumn="compare_id" columns="compare_id" onDelete="CASCADE"/>
    </table>
</schema>

This gives us a flexible way to persist comparisons for logged in utilisateurs or sessions.

Adding attribut produits (data correctif)

We want admin-selectable attributes to appear in comparisons (e.g., screen_size, battery_capacity, color). Add a data correctif that creates a boolean attribute “is_comparable” for attributes or mark existing attributes to be used in compare lists. For clarity, we’ll add a attribut produit called "is_comparable" that peut être used when creating attribute groups in the admin.

<?php
namespace Magefine\ProductCompare\Setup\Patch\Data;

use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\Patch\PatchInterface;
use Magento\Catalog\Model\Product;

class AddComparisonAttributes implements PatchInterface
{
    private $eavSetupFactory;

    public function __construct(EavSetupFactory $eavSetupFactory)
    {
        $this->eavSetupFactory = $eavSetupFactory;
    }

    public function apply()
    {
        $eavSetup = $this->eavSetupFactory->create();

        // This creates a simple attribute to mark attributes as comparable in admin
        $eavSetup->addAttribute(
            Product::ENTITY,
            'is_comparable',
            [
                'type' => 'int',
                'label' => 'Is Comparable (flag for comparison UI)',
                'input' => 'boolean',
                'backend' => '',
                'required' => false,
                'sort_order' => 100,
                'global' => \Magento\Catalog\Model\ResourceModel\Eav\Attribute::SCOPE_GLOBAL,
                'visible' => true,
                'user_defined' => true,
                'default' => 0,
                'group' => 'General',
            ]
        );
    }

    public static function getDependencies() {return [];} 
    public function getAliases() {return [];} 
}

Note: for production, you might want to add a separate EAV attribute definition that classifies attributes (e.g., product_attribute.is_comparable) or create a configuration entity that stores attribute codes to compare. One recommended approche: create an admin UI where the store manager picks which attribute codes appear in the comparison matrix (we’ll show later how that list is read).

2) Business layer: models, resource models and repository

Keep the model simple. Comparison model handles basic CRUD via resource model. Example fichiers are minimal for clarity.

<?php
namespace Magefine\ProductCompare\Model;

use Magento\Framework\Model\AbstractModel;

class Comparison extends AbstractModel
{
    protected function _construct()
    {
        $this->_init('Magefine\ProductCompare\Model\ResourceModel\Comparison');
    }
}

// ResourceModel/Comparison.php
namespace Magefine\ProductCompare\Model\ResourceModel;

use Magento\Framework\Model\ResourceModel\Db\AbstractDb;

class Comparison extends AbstractDb
{
    protected function _construct()
    {
        $this->_init('magefine_product_compare', 'compare_id');
    }
}

Manage items in magefine_product_compare_item with a separate resource model. Keep heavy queries in resource models or dedicated services and not in contrôleurs.

3) Presentation layer: routes, contrôleur endpoints and block

We’ll expose three AJAX endpoints: add a product to comparison, remove a product, and return the comparison matrix (HTML or JSON). Add routes in etc/frontend/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_compare" frontName="magefine_compare">
            <module name="Magefine_ProductCompare"/>
        </route>
    </router>
</config>

Example contrôleur: Add.php

<?php
namespace Magefine\ProductCompare\Controller\Ajax;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Catalog\Model\ProductFactory;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\Serialize\Serializer\Json;

class Add extends Action
{
    private $productFactory;
    private $customerSession;
    private $jsonSerializer;

    public function __construct(Context $context, ProductFactory $productFactory, CustomerSession $customerSession, Json $json)
    {
        parent::__construct($context);
        $this->productFactory = $productFactory;
        $this->customerSession = $customerSession;
        $this->jsonSerializer = $json;
    }

    public function execute()
    {
        $result = ['success' => false];
        $productId = (int)$this->getRequest()->getParam('product_id');

        if ($productId) {
            // Simplified: store in session for non-logged users, or in table for logged users
            $compare = $this->getRequest()->getParam('compare_id');
            // Load or create comparison by session/customer..., then add product id into compare_item table via resource model

            $result['success'] = true;
            $result['message'] = __('Product added to comparison');
        }

        /** @var \Magento\Framework\Controller\Result\Raw $response */
        $response = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_RAW);
        $response->setHeader('Content-type', 'application/json');
        $response->setContents($this->jsonSerializer->serialize($result));
        return $response;
    }
}

This contrôleur is intentionally concise. Keep entreprise logic in a service class that handles merging session comparisons with client comparisons when a utilisateur logs in.

Block and template

Create a block that returns a rendered comparison matrix. The template (view/frontend/templates/compare/block.phtml) will have both the table and hooks for AJAX updates. Example template:

<?php /** @var $block \Magefine\ProductCompare\Block\Compare */ ?>
<div id="magefine-compare" class="magefine-compare-block">
    <div class="mf-compare-header">
        <button id="mf-clear-compare" class="action secondary">Clear</button>
        <span class="mf-count"><?= $block->getProductCount() ?> items</span>
    </div>

    <div id="mf-compare-matrix" class="mf-compare-matrix">
        <?= $block->getMatrixHtml() ?>
    </div>
</div>

JS: ajax interactions

Simple jQuery-based script (view/frontend/web/js/compare.js). The script listens for add/remove events and reloads the matrix via AJAX.

define(['jquery'], function($) {
    'use strict';
    var endpoints = {
        add: '/magefine_compare/ajax/add',
        remove: '/magefine_compare/ajax/remove',
        matrix: '/magefine_compare/ajax/getmatrix'
    };

    function refreshMatrix() {
        $('#mf-compare-matrix').load(endpoints.matrix);
    }

    $(document).on('click', '.mf-add-to-compare', function(e) {
        e.preventDefault();
        var pid = $(this).data('product-id');
        $.post(endpoints.add, {product_id: pid}, function(resp) {
            if (resp.success) refreshMatrix();
            else alert(resp.message || 'Error');
        }, 'json');
    });

    $(document).on('click', '.mf-remove-from-compare', function(e) {
        e.preventDefault();
        var pid = $(this).data('product-id');
        $.post(endpoints.remove, {product_id: pid}, function(resp) {
            if (resp.success) refreshMatrix();
        }, 'json');
    });

    $(document).ready(function() {
        refreshMatrix();
    });
});

4) Building the comparison matrix

The matrix generation is the critical part. We must query attribut produits only once per attribute and get scalar valeurs for many products. Avoid loading full product models; use product collections and addAttributeToSelect for attributes chosen. Example resource-model méthode pseudocode:

public function getAttributeMatrix(array $productIds, array $attributeCodes)
{
    // Use product collection
    $collection = $this->productCollectionFactory->create();
    $collection->addAttributeToFilter('entity_id', ['in' => $productIds]);
    $collection->addAttributeToSelect($attributeCodes);
    
    $matrix = [];
    foreach ($collection as $product) {
        $row = [];
        foreach ($attributeCodes as $code) {
            $row[$code] = $product->getData($code);
        }
        $matrix[$product->getId()] = $row;
    }
    return $matrix;
}

For readable presentation you’ll want attribute labels and option labels for select attributes. Use the EAV attribute repository to get frontend labels or attribute options.

Render stratégie (HTML vs JSON)

Rendering server-side HTML is fast for small matrices and easy to integrate with .load(). For very large comparisons (dozens of products & attributes) consider returning JSON and rendering the table via client-side templating for smoother UX.

5) Frontend UX integration

Where to place compare actions:

  • Category/product list templates: inject a button near product actions (ajouter au panier, liste de souhaits).
  • Product view: provide a comparably styled button or sticky toolbar.
  • Mini-compare floating block: a small bottom drawer that shows selected products and a "compare now" CTA.

Example layout injection to add the compare block to the sidebar (layout xml):

<referenceContainer name="sidebar.additional">
    <block class="Magefine\ProductCompare\Block\Compare" name="magefine.compare.block" template="Magefine_ProductCompare::compare/block.phtml"/>
</referenceContainer>

Design & Accessibility conseils

  • Assurez-vous the matrix is horizontally scrollable on mobile (use CSS overflow-x: auto on the table wrapper).
  • Use aria-labels for add/remove buttons and cléboard-accessible controls.
  • Cache matrix HTML fragments for anonymous utilisateurs with cache clés basé sur session id, and vary by groupe de clients for logged-in utilisateurs.

6) Performance: optimizing queries & caching for large catalogs

Performance is the tricky bit for big catalogs. Key recommendations:

  • Don't load full product models in loops. Use collections and addAttributeToSelect with only necessary attributes.
  • Use resource model SQL for heavy joins; EAV joins peut être expensive when you ask for many attributes. Use MIN/MAX/COALESCE patterns or pivot queries where possible.
  • Create proper indexes on your comparison tables (compare_id, product_id). Add index on session_id if you store by session.
  • Cache the comparison matrix for anonymous utilisateurs and refresh on add/remove. Use Redis or Varnish for caching fragments or full pages. Magento block caching with cache clés and lifetimes helps a lot.
  • Consider pre-computing attribute matrices for commonly compared product groups with tâches cron. C'est useful when you have correctifed comparison sets (e.g., top CPUs compared by specs), or in B2B where comparisons are reused.

Database conseils

Sample SQL to speed up queries (add these indexes using db_schema.xml):

<index referenceId="IDX_COMPARE_PRODUCT" indexType="btree" columns="product_id" />
<index referenceId="IDX_COMPARE_SESSION" indexType="btree" columns="session_id" />

When reading many attributes for many products, EAV performs many joins. If your comparisons are heavy, create a denormalized table that stores attribute_code, product_id, and valeur_text/valeur_int. This table is much faster for pivoting because you can query valeurs by attribute_code and product_id in a single pass and then assemble the matrix in PHP.

Avoiding N+1

Never request attribute labels inside a product loop. Fetch attribute metadata (frontend_label, options) for all attribute codes at once and then map option valeurs to labels using lookup tableaus.

7) UX integration details: product lists & page produits

Small snippets to insert the compare button into list and view templates. Example for product/list.phtml or a custom template override:

<div class="product-item-actions"
     data-role="product-item-actions"
>
    <button class="action mf-add-to-compare" data-product-id="<?= $_product->getId() ?>" aria-label="Add to compare">
        <span>Compare</span>
    </button>
</div>

On the product detail page, include a similar button; trigger a small animation that adds product thumbnail to the floating compare block to give feedback to the utilisateur.

8) Extensions you can add later

Une fois the basic module is in place, consider these useful extensions:

  • Export comparisons: add a contrôleur action that returns CSV or XLSX of the matrix. Implement pagination for huge exports and optionally queue a background export job and notify the utilisateur when ready.
  • Auto-suggestions: when a utilisateur adds product A and B, automatically suggest product C that matches attribute patterns. Implement a simple scoring algorithm: count attribute matches, weight important attributes higher, and return top N similar products. For large catalogs, precompute similarity signatures (hashes) per product and do approximate nearest-neighbor recherchees.
  • Admin UI for attribute selection: a system configuration page where the commerçant picks which attribute codes appear in comparison lists (and the commande).
  • Import/Export of comparison definitions: allow commerçants to save named comparison sets and export or share them via URL.

Example: export as CSV contrôleur

<?php
namespace Magefine\ProductCompare\Controller\Ajax;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Csv\Csv;

class Export extends Action
{
    private $csv;
    private $comparisonService;

    public function __construct(Context $context, Csv $csv, \Magefine\ProductCompare\Model\ComparisonService $comparisonService)
    {
        parent::__construct($context);
        $this->csv = $csv;
        $this->comparisonService = $comparisonService;
    }

    public function execute()
    {
        $compareId = (int)$this->getRequest()->getParam('compare_id');
        $matrix = $this->comparisonService->getMatrixForCompareId($compareId);

        // Convert matrix to rows: first line headers
        $header = array_merge(['product_id', 'sku', 'name'], array_keys(reset($matrix)));
        $rows = [];
        foreach ($matrix as $productId => $attrs) {
            $row = array_merge([$productId, $attrs['sku'] ?? '', $attrs['name'] ?? ''], $attrs);
            $rows[] = $row;
        }
        $data = array_merge([$header], $rows);
        $csvContent = $this->csv->getDataAsString($data);

        $this->getResponse()->setHeader('Content-Type', 'text/csv');
        $this->getResponse()->setHeader('Content-Disposition', 'attachment; filename="comparison_'.time().'.csv"');
        $this->getResponse()->setBody($csvContent);
    }
}

9) Auto-suggestion algorithm (basic)

Simple approche: treat each comparable attribute as a fonctionnalité and compute a similarity score between the current selection and candidate products. For small catalogs you can do this in SQL; for large ones precompute vectors and use approximate nearest neighbors (ANN) libraries.

// Pseudocode: simple attribute-match scoring
function suggestSimilarProducts($selectedProductIds, $limit = 5) {
    // 1. Get attribute set for all products in selected set
    $selectedAttrs = getCombinedAttributes($selectedProductIds); // returns map attribute=>value frequency

    // 2. For each candidate product (exclude selected ids), compute score
    // score = sum(weight(attr) * matchScore(candidate[attr], selectedAttrs[attr]))

    // 3. Return top N products with highest score
}

Weight important attributes higher (e.g., processor_family more weight than color). For more advanced use, use vector embeddings of categorical attributes and cosine similarity.

10) Security and data hygiene

  • Always validate product IDs and compare permissions in contrôleurs.
  • Rate limit expensive endpoints or use a small throttle for anonymous utilisateurs to prevent DB spikes.
  • When storing session-based comparisons, expire old records (cron) to avoid unbounded gligneth.

11) Testing and QA

Write test unitaires for your comparison service and test d'intégrations for contrôleur endpoints. Test with both simple and large attribute sets. Measure memory and SQL time under load and correctif N+1 problems early.

12) Deployment & configuration

Use the standard Magento déployer pipeline:

  1. composer update / module registration
  2. php bin/magento setup:mise à jour (declarative schema will create tables)
  3. php bin/magento setup:di:compile
  4. php bin/magento setup:static-contenu:déployer
  5. flush cache

Provide a system.xml if you want site admins to configure default attributes and cache TTLs for the compare block.

13) Example: end-to-end quick flow

  1. Admin marks attributes as comparable or configures a list in system.xml.
  2. User clicks "Compare" on product list pages. JS posts product_id to /magefine_compare/ajax/add.
  3. Server adds product to session-based comparison and returns success.
  4. Client refreshes the compare matrix container via AJAX; server renders HTML matrix using product collection and attribute list.
  5. User clicks "Export CSV"; contrôleur builds matrix and returns CSV download or creates a queued job if the export is large.

14) Real-life performance checklist

  • Use Redis for session storage (keeps session-based comparisons quick).
  • Enable Redis or Varnish for caching responses. Use proper cache tags to invalidate matrix when attribut produits change.
  • Precompute heavy joins for commonly compared sets via cron and store denormalized snapshots.
  • Limit the UI to a reasonable number of products in a single comparison (e.g., max 6–8) if rendering client-side. Allow an "advanced comparison" flow for power utilisateurs that handles more products asynchronously.

15) Final thoughts & next étapes

Ce module is a balance between UX, flexibility and performance. Start simple: implement session-based comparison, server-rendered matrix and a manageable set of attributes. Then add admin UI for attribute selection, export, and auto-suggestion when you have a stable foundation.

If you’re using Magefine for extensions or hosting, you’ll appreciate how a custom-built compare module vous donne a tailored experience for your catalog and clients. The same modular architecture peut être extended to product spec sheets, automated recommendations and advanced analytics.

Appendix: Useful helpers and references

  • Magento 2 développeur docs — EAV attribute management and declarative schema.
  • Use Magento's collection factories and avoid loading full product models in loops.
  • Consider using Fastly/Varnish, Redis and PHP-FPM tuning for high concurrency.

Si vous want, I can: provide the full module fichiers ready to drop into app/code, produce the configuration d'administration for attribute selection, or give a sample similarity algorithm tuned for 50k products. Tell me which one you prefer and I’ll generate the code.

Happy coding — and don’t hesitate to ping me if you want a code avis once you have an initial version.