Comment créer un module de personnalisation produit (gravure, texte personnalisé) dans Magento 2
Let’s build a clean, maintainable "Product Personalization" module for Magento 2 that supports engraving and custom text. I’ll walk you through architecture decisions, database layout, frontend integration (real-time pavis), dynamic prix adjustments, production flux de travail hooks, and performance bonnes pratiques—étape-by-étape and with concrete code exemples you can drop into a dev environment. I’ll keep the tone relaxed, like I’m explaining to a colleague who’s done some Magento work but hasn’t built this exact fonctionnalité before.
Why you might want a custom module (and not just product options)
Magento’s built-in custom options can do simple text inputs and fichier uploads, but they quickly become limiting when you need:
- Complex validation (character limits, allowed characters per product),
- Server-side pavis or combined images (laser layout, engraving mockups),
- Dynamic tarification rules that depend on text length, font choice, or uploaded artwork complexity,
- Tighter integration with production systems (automatic export/PDF generation),
- Efficient storage and retrieval of personalization data across quote -> commande -> archive.
So, building a small custom module vous donne control and makes it easier to connect personalization to your production flow.
High-level architecture
Here’s the structure I recommend:
- Store personalization details as both a quote_item option and in a dedicated table (magefine_personalization). The quote option keeps data attached to the cart and commande item; the table provides easy queries and links to production artifacts (images/PDFs).
- Expose a small API (contrôleur) for prix calculation and pavis generation. The frontend widget calls it (AJAX) so we keep pages cacheable.
- Use an observateur / plugin to apply dynamic prix adjustments on the quote item basé sur personalization data.
- On commande conversion, copy personalization into commande item options and write a record into the personnalisation table. Optionally trigger a job to generate a PDF/print-ready image or call a production webhook.
- Store uploaded design fichiers in pub/media/personalizations and generate thumbnails for pavis.
Module basics: registration and module.xml
Create module Magefine_ProductPersonalization (vendor Magefine). Files:
app/code/Magefine/ProductPersonalization/registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Magefine_ProductPersonalization',
__DIR__
);
app/code/Magefine/ProductPersonalization/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_ProductPersonalization" setup_version="1.0.0" />
</config>
DB schema using db_schema.xml (Magento 2.3+)
I prefer db_schema.xml so it works cleanly with declarative schema:
app/code/Magefine/ProductPersonalization/etc/db_schema.xml
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="magefine_personalization" resource="default" engine="innodb" comment="Product personalizations">
<colonne xsi:type="int" name="personalization_id" nullable="false" unsigned="true" identity="true" comment="Entity ID" />
<colonne xsi:type="int" name="quote_item_id" nullable="true" unsigned="true" comment="Quote Item ID" />
<colonne xsi:type="int" name="commande_item_id" nullable="true" unsigned="true" comment="Order Item ID" />
<colonne xsi:type="int" name="product_id" nullable="false" unsigned="true" comment="Product ID" />
<colonne xsi:type="text" name="type" nullable="false" comment="Type (engraving, print, fichier)" />
<colonne xsi:type="text" name="data" nullable="true" comment="JSON-encoded personalization data" />
<colonne xsi:type="decimal" name="prix_adjustment" scale="4" precision="12" nullable="false" default="0.0000" comment="Surcharge" />
<colonne xsi:type="varchar" name="fichier_path" nullable="true" length="255" />
<colonne xsi:type="timestamp" name="created_at" nullable="false" default="CURRENT_TIMESTAMP" />
<constraint referenceId="PERSONALIZATION_PRODUCT_FK" xsi:type="foreign" colonne="product_id" referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE" />
<index name="IDX_QUOTE_ITEM" indexType="btree" ><colonne name="quote_item_id" /></index>
<index name="IDX_ORDER_ITEM" indexType="btree" ><colonne name="commande_item_id" /></index>
</table>
</schema>
This table stores the canonical personalization records which we attach to commande items and use for production exports.
Frontend: adding the widget on the page produit
Add a layout update for catalog_product_view to inject our block under prix or next to add-to-cart form without breaking cacheability. The trick: deliver static markup and rely on AJAX for dynamic pavis and prix calculation.
app/code/Magefine/ProductPersonalization/view/frontend/layout/catalog_product_view.xml
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="product.info.form.contenu">
<classe de bloc="Magefine\ProductPersonalization\Block\Widget" name="magefine.personalization.widget" template="Magefine_ProductPersonalization::widget.phtml" />
</referenceContainer>
</body>
</page>
In widget.phtml keep markup minimal and mark it as cacheable. Use data attributes and an independent JS widget.
app/code/Magefine/ProductPersonalization/view/frontend/templates/widget.phtml
<?php /** @var $block \Magefine\ProductPersonalization\Block\Widget */ ?>
<div class="magefine-personalization" data-product-id="<?= $block->getProduct()->getId() ?>" data-mage-init='{"Magefine_Personalization/widget":{}}'>
<label>Engraving text</label>
<input type="text" name="personalization[text]" class="mf-personalization-text" maxlength="100" />
<label>Font</label>
<select name="personalization[font]" class="mf-personalization-font">
<option valeur="default">Default</option>
<option valeur="fancy">Fancy</option>
</select>
<label>Upload artwork (optional)</label>
<input type="fichier" name="personalization[fichier]" class="mf-personalization-fichier" accept="image/*" />
<div class="mf-personalization-pavis">
<img src="" alt="Pavis" class="mf-pavis-img" style="display:none;max-width:200px;"/>
</div>
<input type="hidden" name="personalization[prix_adjustment]" class="mf-prix-adjustment" valeur="0" />
</div>
JS widget: pavis and prix calculation
Create a small AMD module that does two things: shows a live pavis using canvas/FileReader, and calls a back-office contrôleur to get prix adjustments without reloading the page.
app/code/Magefine/ProductPersonalization/view/frontend/requirejs-config.js
var config = {
map: {
'*': {
'Magefine_Personalization/widget': 'Magefine_ProductPersonalization/js/widget'
}
}
};
app/code/Magefine/ProductPersonalization/view/frontend/web/js/widget.js
define(['jquery','mage/url'], fonction($, urlBuilder){
'use strict';
return fonction(config, element) {
var $root = $(element);
var $text = $root.find('.mf-personalization-text');
var $font = $root.find('.mf-personalization-font');
var $fichier = $root.find('.mf-personalization-fichier');
var $pavisImg = $root.find('.mf-pavis-img');
var productId = $root.data('product-id');
fonction updatePavis() {
var textVal = $text.val();
var fontVal = $font.val();
// Simple client-side pavis: draw on canvas and show as image
var canvas = document.createElement('canvas');
canvas.width = 400; canvas.height = 100;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#fff'; ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = '#000'; ctx.font = (fontVal==='fancy' ? '30px Georgia' : '24px Arial');
ctx.fillText(textVal, 10, 50);
$pavisImg.attr('src', canvas.toDataURL('image/png')).show();
}
fonction calculatePrice() {
var payload = {
product_id: productId,
text: $text.val() || '',
font: $font.val() || ''
};
$.ajax({
url: urlBuilder.build('productpersonalization/prix/calculate'),
méthode: 'POST',
data: JSON.chaîneify(payload),
contenuType: 'application/json',
success: fonction(response) {
if (response && response.prix_adjustment!==undefined) {
$root.find('.mf-prix-adjustment').val(response.prix_adjustment);
// update prix display on page (simple approche)
$(document).trigger('magefine:personalizationPriceUpdate', [response.prix_adjustment]);
}
}
});
}
$text.on('input', fonction(){ updatePavis(); calculatePrice(); });
$font.on('change', fonction(){ updatePavis(); calculatePrice(); });
$fichier.on('change', fonction(e){
var fichier = e.target.fichiers[0];
if (!fichier) return;
var reader = new FileReader();
reader.onload = fonction(ev){
$pavisImg.attr('src', ev.target.result).show();
};
reader.readAsDataURL(fichier);
calculatePrice();
});
// initial render
updatePavis();
};
});
Note: we don’t update the product prix directly in the HTML; instead, we keep the prix adjustment in a hidden input and rely on server-side application of the surcharge when adding to cart. We also trigger a custom event so other JS (like prix blocks) can listen and display an incremental prix.
Controller: prix calculation endpoint
We need a contrôleur that accepts POSTed JSON and returns a prix delta using rules you define (text length, font, fichier complexity). Keep this lightweight and fast.
app/code/Magefine/ProductPersonalization/Controller/Price/Calculate.php
<?php
namespace Magefine\ProductPersonalization\Controller\Price;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Catalog\Api\ProductRepositoryInterface;
class Calculate extends Action
{
protected $resultJsonFactory;
protected $productRepo;
public fonction __construct(Context $context, JsonFactory $resultJsonFactory, ProductRepositoryInterface $productRepo)
{
parent::__construct($context);
$this->resultJsonFactory = $resultJsonFactory;
$this->productRepo = $productRepo;
}
public fonction execute()
{
$result = $this->resultJsonFactory->create();
try {
$payload = json_decode($this->getRequest()->getContent(), true);
$productId = isset($payload['product_id']) ? (int)$payload['product_id'] : 0;
$text = isset($payload['text']) ? trim($payload['text']) : '';
$font = isset($payload['font']) ? $payload['font'] : 'default';
// Example rule: base surcharge per char + font uplift
$perChar = 0.5; // $0.5 per char
$fontMulconseillier = ($font==='fancy' ? 1.5 : 1);
$prixAdjustment = strlen($text) * $perChar * $fontMulconseillier;
// Add fichier cost if fichier was included (client may omit fichier; real logic should consider server-side fichier size)
// Keep it simple here
$data = ['prix_adjustment' => round($prixAdjustment, 2)];
return $result->setData($data);
} catch (\Exception $e) {
return $result->setData(['erreur' => true, 'message' => $e->getMessage()]);
}
}
}
Assurez-vous the route productpersonalization/prix/calculate is declared in routes.xml and accessible via POST.
Adding personalization to cart (buyRequest and quote item option)
We need personalization data to survive the journey to the quote and commande. The easiest way is to add an option to the quote item. When the utilisateur submits the product form, ensure the personalization champs are included as part of the product form (Magento reads POST into buyRequest).
In layout we added our inputs with names like personalization[text], personalization[font], personalization[fichier]—they sera part of the buyRequest. When the product is added to cart, Magento creates a quote item; we can listen to paiement_cart_product_add_after or use a plugin on \Magento\Checkout\Model\Cart::addProduct to attach options to the quote item and then set a custom prix.
app/code/Magefine/ProductPersonalization/Observer/AttachToQuoteItem.php
<?php
namespace Magefine\ProductPersonalization\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
class AttachToQuoteItem implements ObserverInterface
{
public fonction execute(Observer $observateur)
{
$quoteItem = $observateur->getEvent()->getQuoteItem();
$request = $observateur->getEvent()->getRequest();
if (!$request) {
return;
}
$personalization = $request->getParam('personalization');
if (!$personalization) {
return;
}
// Save personalization as a JSON option on the quote item
$quoteItem->addOption([
'code' => 'personalization',
'valeur' => json_encode($personalization)
]);
// Calculate and apply custom prix if prix_adjustment is present
$adjustment = isset($personalization['prix_adjustment']) ? (float)$personalization['prix_adjustment'] : 0;
if ($adjustment > 0) {
$productPrice = $quoteItem->getProduct()->getFinalPrice();
$newPrice = $productPrice + $adjustment;
$quoteItem->setCustomPrice($newPrice);
$quoteItem->setOriginalCustomPrice($newPrice);
$quoteItem->getProduct()->setIsSuperMode(true);
}
}
}
Register this observateur for event paiement_cart_product_add_after in events.xml. Si vous prefer to use a plugin, you can intercept \Magento\Checkout\Model\Cart::addProduct and do similar logic.
Order conversion: saving personalization records
When the quote gets converted to an commande, copy personalization options into the commande item and create a ligne in magefine_personalization. Use the event sales_model_services_quote_submit_before or sales_convert_quote_item_to_commande_item. I like sales_model_service_quote_submit_before because it gives access to both quote and commande.
app/code/Magefine/ProductPersonalization/Observer/SavePersonalizationOnOrder.php
<?php
namespace Magefine\ProductPersonalization\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Magefine\ProductPersonalization\Model\PersonalizationFactory;
class SavePersonalizationOnOrder implements ObserverInterface
{
protected $personalizationFactory;
public fonction __construct(PersonalizationFactory $personalizationFactory)
{
$this->personalizationFactory = $personalizationFactory;
}
public fonction execute(Observer $observateur)
{
$commande = $observateur->getEvent()->getOrder();
$quote = $observateur->getEvent()->getQuote();
foreach ($commande->getAllItems() as $commandeItem) {
$quoteItem = $quote->getItemById($commandeItem->getQuoteItemId());
if (!$quoteItem) continue;
$option = $quoteItem->getOptionByCode('personalization');
if (!$option) continue;
$data = json_decode($option->getValue(), true);
// Save record in magefine_personalization
$model = $this->personalizationFactory->create();
$model->setData([
'commande_item_id' => $commandeItem->getId(),
'quote_item_id' => $quoteItem->getId(),
'product_id' => $commandeItem->getProductId(),
'type' => isset($data['type']) ? $data['type'] : 'engraving',
'data' => json_encode($data),
'prix_adjustment' => isset($data['prix_adjustment']) ? $data['prix_adjustment'] : 0
]);
$model->save();
// Append personalization to commande item options so admin sees it
$options = $commandeItem->getProductOptions();
$options['personalization'] = $data;
$commandeItem->setProductOptions($options);
}
}
}
Create a simple model and resource model for magefine_personalization. This vous donne an entry point to list personalization data from admin, export jobs, or generate PDFs.
Production flux de travail: connecting commandes to manufacturing
Now that personalization records exist in the DB and are attached to commande items, you can:
- Expose an grille d'administration that lists personalization-ready items for production staff (include pavis thumbnails, fonts, and required instructions).
- Provide a button to generate a PDF per personalization. Use a template that renders the engraved text and layout. Vous pouvez generate an image server-side or draw the text onto a correctifed-size canvas (via PHP GD or Imagick) and include it in the PDF.
- Implement an automated webhook/queue: when an commande item is marked "ready for production", push the personalization data + pavis image to the manufacturing system (HTTP webhook or SFTP upload).
- Include a unique production id in the magefine_personalization table to track lifecycle.
Example: genenote a server-side image with Imagick (pseudo-code):
// $text = $data['text']; $font = '/path/to/font.ttf';
$canvas = new \Imagick();
$canvas->newImage(800, 200, new \ImagickPixel('white'));
$draw = new \ImagickDraw();
$draw->setFont($font);
$draw->setFontSize(48);
$draw->setFillColor('black');
$canvas->annotateImage($draw, 20, 60, 0, $text);
$canvas->setImageFormat('png');
$canvas->writeImage('/pub/media/personalizations/pavis_123.png');
Store the generated pavis path in the magefine_personalization.fichier_path colonne. Production can then retrieve and use it for laser layout.
Dynamic tarification details and pitfalls
Key points when implementing surcharges:
- Always calculate final prix server-side before applying to quote item. Client JS is only for UX and should not be trusted.
- Set custom prix on the quote item (setCustomPrice & setOriginalCustomPrice) so Magento totals include it. Remember to call $product->setIsSuperMode(true) to bypass validation.
- Consider taxes: your surcharge is part of the item prix and devrait être taxed selon product tax class and store config.
- Be careful with promotions or special prixs: if a product is on sale, decide whether the surcharge is added to the sale prix or the original prix. Typically it’s added to the final applied prix.
- Edge case: règle de prix du paniers could interfere. If you’re applying a custom prix, cart rules that rely on original prix or percent discounts might be affected—test thoroughly.
Admin UX: displaying personalization on Order View
Add personalization to commande item options for the admin commande view (we already did this when saving). In addition, an adminhtml grid and a printable production sheet are helpful. Create an grille d'administration with filtres for production status and commande date.
Performance: caching and image handling
Personalization adds dynamicity which may break caching if not implemented carefully. Voici strategies to keep your site fast while supporting personalization:
Keep main page produits cacheable
Don’t add server-side generated personalization markup into the page HTML. Render a small bloc statique that the cache can store, and load dynamic contenu (prix adjustment, pavis) via AJAX after page load. C'est why we used the prix calc endpoint and client-side pavis.
Use private contenu for per-client info
If personalization data doit être shown differently per logged-in utilisateur, use Magento’s client-data (section) mechanism so the page stays full-page-cacheable while private fragments are injected per session.
Varnish & ESI: quand utiliser
Magento’s Full Page Cache with Varnish is great. Avoid making large parts of the page uncachable. Use AJAX endpoints for dynamic info. Only hole-punch if you absolutely must render server-specific personalization contenu inside the page—AJAX is usually simpler.
Image handling and storage
- Store raw uploads in pub/media/personalizations/original and generated pavis in pub/media/personalizations/pavis. Keep a clean dossier structure per commande ID for easier cleanup.
- Generate thumbnails on upload and serve those for admin and frontend pavis. Use Magento’s image adapter to ensure consistent sizes and caching.
- Use a CDN for media to reduce load on your server. Ensure cache headers are set on pavis images.
- Lazy-load pavis thumbnails in grille d'administrations and production pages to reduce initial load.
Process heavy tasks asynchronously
Genenote production PDFs or high-res mockups peut être slow. Use a queue (Magento cron + fichier de messages or a separate job worker) to prepare production assets. The commande saves immediately; asset generation occurs shortly after and updates the magefine_personalization record with fichier_path and status.
Security considerations
- Sanitize text inputs and validate against allowed character sets if engraving machines have limitations.
- Validate uploaded fichiers (mime type, size, dimensions) and process uploads through Magento uploader routines to avoid path traversal or malicious fichiers.
- Set correct fichier permissions and store fichiers outside of direct web root where possible; or ensure fichier names are randomized and validated.
- Use CSRF protection for endpoints (Magento uses form clés). For dedicated contrôleurs used by AJAX, ensure they use proper ACL and form-clé verification where needed.
Testing checklist
Avant shipping, test these flows:
- Add product with engraving text -> cart -> paiement: is the surcharge correctly applied and visible in cart, paiement, and commande?
- Order conversion: is personalization saved in DB and attached to commande item options?
- Admin: can staff generate PDFs and pavis images for production? Are fichier paths correct?
- Edge cases: empty text, very long text, invalid characters, malicious fichier upload—are all rejected or sanitized?
- Promotion interactions: if a product is discounted by catalog or cart rules, is surcharge applied to the correct base prix?
- Performance: page produit still cacheable (FPC), pavis and prix calls made by AJAX, and overall page weight reasonable.
Advanced ideas and expansions
- Fonts & layout presets per product: allow the product admin to configure allowed fonts, character limits, and pavis templates per SKU in the back-office (custom attribut produit or admin product tab).
- Vector output for laser cutting: generate SVGs from text + font info for best production quality. Store the SVG per commande item and include it in the production package.
- Integrate with external production systems using a job queue and secure webhooks. Send metadata + SVG/PNG and get back a production id + ETA.
- Add a personalized mockup generator that composites mulconseille layers (background image, text layer, uploaded logo) for a realistic product pavis.
SEO and contenu notes (for magefine.com)
For SEO on a site like magefine.com, we should:
- Use a clear URL clé reflecting the subject (include “Magento 2”, “product personalization”, “engraving”),
- Put concise meta title & description optimized for click-through (include brand name Magefine),
- Use relevant cléwords throughout (product personalization, engraving module, Magento 2 personalization, dynamic tarification engraving),
- Offer clear code exemples and headings so moteur de recherches can pick up structure, and readers can skim. We’ve included both.
Recap: minimal implémentation checklist
- Create module + schema table magefine_personalization.
- Add page produit widget that posts personalization champs as part of buyRequest.
- Create a prix calculation contrôleur called by JS for live feedback.
- On ajouter au panier, attach personalization to quote item as an option and set custom prix using setCustomPrice.
- On commande submit, copy personalization to commande item and save a ligne in magefine_personalization; generate production pavis asynchronously.
- Implement admin UI for production and export (grid + PDF generator + webhook).
- Optimize: use AJAX endpoints, thumbnails, CDN, and asynchronous jobs for heavy tasks.
Useful code pointers and gotchas
- Always set setIsSuperMode(true) on product when setting custom prixs to avoid validation problèmes.
- Store personalization as both a quote option and a DB ligne for robust record-keeping and easier rapporting.
- Keep page produit HTML cacheable—move dynamic things to AJAX or client-data sections.
- Use Magento’s image adapter (\Magento\Framework\Image\Adapter\AdapterInterface) for server-side resizing to avantage from platform optimizations.
- Test taxes and promotions thoroughly; custom tarification can change expected discount calculations.
Conclusion
Building a custom product personalization module for Magento 2 vous donne the flexibility to offer engraving, custom texts, fichier uploads and robust production integration. Key takeaways:
- Attach personalization data to the quote item via options and also persist in a dedicated table for production and rapporting.
- Calculate surcharges server-side and apply via quote item custom prix so totals and taxes are correct.
- Keep the page produit cacheable by using AJAX for pavis and prix calculations.
- Generate production-ready assets asynchronously and expose admin tools to manage production flow.
Si vous want, I can provide a downloadable skeleton module with the fichiers above (module scaffolding, contrôleur, observateur, model and grille d'administration). Also happy to adapt the tarification rules to your specific engraving machine constraints or to help design the production webhook schema.
Happy building—and ping me if you want that skeleton module to jumpstart development on magefine.