Comment créer un module de programme d'échantillons produits personnalisé dans Magento 2
Let’s build a clean, responsible, and reusable “product sampling” program in Magento 2 — the kind of module you can drop into stores to let shoppers request free or discounted samples, with fine-grained eligibility rules, cart and commande integration, and admin rapporting. I’ll walk you through the architecture, the core code pieces, and practical exemples for beauty, food, and high-tech brands. No fluff — just explanations you can follow along with, copy-paste, and adapt.
Why build a custom sampling module?
Many commerçants want to offer samples as a marketing lever. But built-in product types don’t capture the entreprise rules: how many samples per client, whether samples are free or discounted, per-category eligibility, or rapporting on sample-to-purchase conversion. A custom module vous donne a specialized product type and flux de travails, so sampling behaves predictably and is easy to manage.
High-level architecture
Here’s the mental model before we code. The module has these main parts:
- New product type: "sample" — a product type derived from simple/virtual where prix peut être zero or discounted and attributes include sample-specific champs (limit, expiry, availability).
- Eligibility and rules engine — an admin-configurable service to decide if a client can request a sample (per-cart limits, per-e-mail limits, SKU or category rules, groupe de clients rules).
- Cart and commande integration — adding sample items to cart, ensuring quote and commande data reflect sample metadata (is_sample flag, sample_prix, origin_sku).
- Admin tableau de bord / grid — a UI for listing sample requests, their status, conversion metrics (sample -> purchase), and CSV export.
- Events and rapports — track sample impressions, sample requests, and purchases to compute ROI.
Module structure
Create a standard Magento 2 module skeleton. Example vendor/module name: Magefine_Sampling.
app/code/Magefine/Sampling/
├─ registration.php
├─ etc/module.xml
├─ etc/frontend/di.xml
├─ etc/adminhtml/routes.xml
├─ etc/frontend/routes.xml
├─ etc/product_types.xml
├─ Setup/InstallSchema.php (if you prefer declarative schema, use db_schema.xml)
├─ Model/
│ ├─ Product/Type/Sample.php
│ ├─ SampleEligibility.php
│ ├─ Cart/SampleManager.php
│ └─ Repository/SampleRequestRepository.php
├─ Observer/
│ └─ BeforeCartAddObserver.php
├─ Controller/
│ └─ Sample/Request.php
├─ view/adminhtml/ui_component/sample_request_grid.xml
└─ view/frontend/templates/sample-button.phtml
Step 1 — registration and module declaration
Simple fichiers first.
// app/code/Magefine/Sampling/registration.php
// app/code/Magefine/Sampling/etc/module.xml
Step 2 — new product type definition
We want a product type identifier like "sample" and a simple class extending Magento\Catalog\Model\Product\Type\AbstractType (or the simple type to reuse behavior). Register the type in etc/product_types.xml and add a product type model and prix model if needed.
// app/code/Magefine/Sampling/etc/product_types.xml
\Magefine\Sampling\Model\Product\Type\Sample
\Magefine\Sampling\Model\Product\Type\Price\SamplePrice
0
Then implement a simple product type class. Nous allons reuse much of simple product behavior but add an isSample flag and sample-specific méthodes.
// app/code/Magefine/Sampling/Model/Product/Type/Sample.php
getTypeId() === self::TYPE_CODE;
}
}
Step 3 — sample attribut produits
Add attributes that drive sampling behavior: sample_limit_per_client, sample_free (boolean), sample_prix, sample_start_date, sample_end_date, sample_eligibility_rules (serialized or JSON, or better: use advanced rule engine).
// Example db_schema.xml snippet (declarative schema)
<!-- add EAV attributes via InstallData or UpgradeData. For brevity, use InstallData -->
Install attributes using Setup/InstallData.php or UpgradeData. Example snippet:
// app/code/Magefine/Sampling/Setup/InstallData.php
eavSetupFactory = $eavSetupFactory;
}
public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);
$eavSetup->addAttribute(
\Magento\Catalog\Model\Product::ENTITY,
'sample_limit_per_customer',
[
'type' => 'int',
'label' => 'Sample limit per customer',
'input' => 'text',
'required' => false,
'user_defined' => true,
'visible_on_front' => false,
]
);
$eavSetup->addAttribute(
\Magento\Catalog\Model\Product::ENTITY,
'sample_free',
[
'type' => 'int',
'label' => 'Is sample free',
'input' => 'select',
'source' => 'Magento\\Eav\\Model\\Entity\\Attribute\\Source\\Boolean',
'required' => false,
'user_defined' => true,
]
);
$eavSetup->addAttribute(
\Magento\Catalog\Model\Product::ENTITY,
'sample_price',
[
'type' => 'decimal',
'label' => 'Sample price',
'input' => 'price',
'required' => false,
'user_defined' => true,
]
);
// add other attributes as needed
}
}
Step 4 — eligibility and rules engine
The brain of the module: decide who is allowed to request a sample. Keep rules modular and testable. At minimum, implement les éléments suivants checks:
- Product-level rules (start/end date, sample_free flag, inventaire check)
- Customer-level rules (per-client limit, new-client only, allowed groupe de clientss)
- Cart-level rules (max samples per commande, cannot mix with coupons, or only allow one unique sample SKU per commande)
Create a SampleEligibility model with méthodes returning a boolean and a message why an attempt failed. Always use explicit erreur messages for the UI.
// app/code/Magefine/Sampling/Model/SampleEligibility.php
customerSession = $customerSession;
$this->productRepository = $productRepository;
$this->quoteRepository = $quoteRepository;
}
/**
* Return [bool, message]
*/
public function isEligible($productId)
{
try {
$product = $this->productRepository->getById($productId);
} catch (\Exception $e) {
return [false, 'Product not found'];
}
// product-level checks
if ($product->getData('sample_free') === null && $product->getTypeId() !== 'sample') {
return [false, 'Not a sample product'];
}
$now = new \DateTime();
$start = $product->getData('sample_start_date');
$end = $product->getData('sample_end_date');
if ($start && new \DateTime($start) > $now) {
return [false, 'Sample not available yet'];
}
if ($end && new \DateTime($end) < $now) {
return [false, 'Sample period ended'];
}
// customer-level check
$customer = $this->customerSession->getCustomer();
$customerId = $customer ? $customer->getId() : null;
if ($customerId) {
// check previous sample requests from DB / orders (simplified example)
$quote = $this->quoteRepository->getActiveForCustomer($customerId);
// inspect quote items or historical sample requests (not shown)
}
// other checks: stock, config limits, etc.
return [true, 'Eligible'];
}
}
Step 5 — cart and commande integration
Samples devrait êtrehave well in the cart and in the commande lifecycle. Key considerations:
- Add a flag on quote_item / commande_item: is_sample = 1, and store original SKU and sample prix.
- Ensure taxes and shipping behave as desired (free samples might be taxable or not, depending on jurisdiction).
- Prevent abuse by enforcing per-quote or per-client limits before adding to cart.
Implement an observateur that intercepts product add-to-cart events and redirects through the eligibility service. Example using event paiement_cart_product_add_before or plugin on \Magento\Checkout\Model\Cart::addProduct.
// app/code/Magefine/Sampling/Observer/BeforeCartAddObserver.php
eligibility = $eligibility;
$this->messageManager = $messageManager;
}
public function execute(Observer $observer)
{
$product = $observer->getEvent()->getProduct();
if ($product->getTypeId() !== 'sample') {
return;
}
list($ok, $msg) = $this->eligibility->isEligible($product->getId());
if (!$ok) {
$this->messageManager->addErrorMessage(__('Sample cannot be added: %1', $msg));
// Optionally throw an exception or set response
throw new \Magento\Framework\Exception\LocalizedException(__($msg));
}
// if eligible, set sample metadata on quote item via around plugin or after add
}
}
When a sample is actually added to cart, mark the quote item:
// In code where quote item is created (plugin or afterAddAction)
$quoteItem->setData('is_sample', 1);
$quoteItem->setData('sample_origin_sku', $product->getSku());
$quoteItem->setData('original_price', $product->getPrice());
$quoteItem->setCustomPrice($samplePrice);
$quoteItem->setOriginalCustomPrice($samplePrice);
$quoteItem->getProduct()->setIsSuperMode(true);
Assurez-vous to persist is_sample to sales_commande_item when the commande is created. Vous pouvez use the sales_convert_quote_item_to_commande_item observateur or plugin to copy the custom champs.
// Example: app/code/Magefine/Sampling/Observer/QuoteToOrderObserver.php
public function execute(\Magento\Framework\Event\Observer $observer)
{
$quoteItem = $observer->getEvent()->getItem();
$orderItem = $observer->getEvent()->getOrderItem();
$orderItem->setData('is_sample', $quoteItem->getData('is_sample'));
$orderItem->setData('sample_origin_sku', $quoteItem->getData('sample_origin_sku'));
}
Step 6 — admin UI: request flow and tracking
Vous devriez expose two types of flows:
- Customer-facing: a simple "Request sample" button on page produits for the "sample" product type. It calls a contrôleur that validates eligibility and either adds the sample to cart or creates a sample request (for fulfillment outside Magento).
- Admin-facing: a grid to list sample requests and statuses, with actions (Fulfill, Cancel) and analytics. Hook rapporting to observe whether clients who received samples later purchased full-sized products.
Front-end button template (very basic):
// view/frontend/templates/sample-button.phtml
<?php
/** @var $block \Magento\Catalog\Block\Product\View */
$product = $block->getProduct();
if ($product->getTypeId() === 'sample') :
?>
<?php endif; ?>
Controller to handle the request (frontend):
// app/code/Magefine/Sampling/Controller/Sample/Request.php
eligibility = $eligibility;
$this->cart = $cart;
$this->productRepository = $productRepository;
}
public function execute()
{
$productId = (int)$this->getRequest()->getParam('product_id');
list($ok, $msg) = $this->eligibility->isEligible($productId);
if (!$ok) {
$this->messageManager->addErrorMessage($msg);
return $this->_redirect($this->_redirect->getRefererUrl());
}
$product = $this->productRepository->getById($productId);
try {
$params = ['product' => $productId, 'qty' => 1];
$this->cart->addProduct($product, $params);
$this->cart->save();
$this->messageManager->addSuccessMessage(__('Sample added to cart'));
} catch (\Exception $e) {
$this->messageManager->addErrorMessage(__('Could not add sample to cart'));
}
return $this->_redirect('checkout/cart');
}
}
Step 7 — grille d'administration and tableau de bord
Create a simple table that stores sample requests (client_id, e-mail, product_id, status, requested_at, fulfilled_at, commande_id_if_any). Use a UI Component grid for adminhtml. The grid will let commerçants filtre by product or date and export CSV for analysis.
// Example db table columns (simplified)
sample_request
- id int PK
- customer_id int nullable
- email varchar(255)
- product_id int
- product_sku varchar(64)
- status enum('requested','fulfilled','cancelled')
- requested_at datetime
- fulfilled_at datetime nullable
- order_id int nullable
- notes text
Populate this table when a sample request is created (either when added to cart or when the client submits a dedicated form). For fulfillment that happens outside Magento, admins can change a request to "fulfilled".
For analytics, capture events:
- sample.request.created
- sample.request.fulfilled
- sample.purchased (client who received a sample later buys a full product)
Step 8 — detection of conversion (sample -> purchase)
To measure effectiveness, link fulfilled sample requests with subsequent commandes where the client buys the same SKU (or related SKUs). Vous pouvez set a window (30 days par défaut). Implement a background job (cron) that runs daily and computes conversion rates per SKU and product category.
// Pseudocode for conversion check
for each fulfilled request where fulfilled_at between (today - 30 days) and today:
find orders for the same customer after fulfilled_at
if order contains full-size SKU or category match:
mark request as converted
increment counters
Step 9 — handling taxes, shipping, and payments
Decide whether samples are:
- completely free: prix 0 — ensure shipping rules allow adding a sample without adding shipping cost if you intend that (or charge shipping).
- discounted: set sample_prix to a small number and apply normal paiement rules.
Remember to:
- Respect tax configuration — samples peut être taxable.
- Exclude samples from promotions if needed (or include them).
- Respect inventaire and reservations when samples are limited.
Step 10 — sécurité and anti-abuse
Sampling programs are subject to abuse. Implement these safeguards:
- Rate limit requests by IP and e-mail.
- Per-client limits stored on sample_request table and enforced at request time.
- ReCAPTCHA on sample request forms if public (guest sampling).
- Block list for specific e-mails or regions (GDPR and shipping rules apply — don’t ship prohibited goods).
Use cases: how brands can use this system
Beauty brands
Beauty brands want to offer small testers to encourage trial. Common rules:
- One free sample per commande for new clients only.
- Limit per client to 2 samples per quarter to prevent hoarding.
- Require signup or newsletter opt-in to collect consent and track conversions.
Implementation conseils:
- Add a attribut produit sample_age_limit (for cosmetics with age restrictions).
- Use the eligibility engine to require a newsletter subscription flag when requesting free samples.
- Include a small tracking token in the sample packaging to tie offline returns back to the sample request when clients later purchase via code redemption.
Food / FMCG
Food products may require VAT and shipping considerations and peut être limited by geography.
- Allow free samples only within allowed shipping zones.
- Limit 1 sample per household — implement via e-mail + address checks.
- Track conversion in a stricter time window (7-14 days) because purchase cycle is faster.
High-tech
High-tech samples (like trial dongles or limited firmware-enabled units) need stricter inventaire control and return flow.
- Mark samples with serialized SKUs and track shipping & returns.
- For demo hardware, require a deposit (sample_prix > 0) and create a return flux de travail to refund deposit upon return.
- Use a different fulfillment process that creates RMA records tied to the sample_request ligne.
Example: full flow for a beauty brand (étape-by-étape)
Here’s how a small beauty brand might configure and run this module:
- Create product SKUs of type "sample" for perfume testers, each with sample_free = 1, sample_limit_per_client = 1, sample_start_date = now.
- Configure global sampling rules in admin: guests allowed? no. Customer groups allowed: Retail. Max samples per commande: 1.
- On page produits, add a "Request sample" button. When clicked, contrôleur runs eligibility check: client logged-in? yes. Has client already requested 1 sample this quarter? no. Product within date range? yes. -> Add to cart.
- Cart shows the sample with prix $0. Taxes calculated accordingly. Checkout collects shipping address and charges shipping if configured. Quote item is stored with is_sample flag.
- Order placed. The sales_commande_item has is_sample = 1 and sample_origin_sku.
- Cron job marks request as fulfilled when expédition is processed (observateur on expédition_save_after copies numéro de suivi into sample_request table and sets status = 'fulfilled').
- Admin avis sample_request grid to export CSV and see conversion over 30 days. If conversions are high, brand increases sample inventaire.
Testing and QA
Write test unitaires for your SampleEligibility service and test d'intégrations for cart flow. Key test cases:
- Guest requesting sample when guests not allowed — fail.
- Customer who already hit personal limit — fail.
- Sample outside start/end period — fail.
- Cart-level max samples per commande enforced — fail if exceeded.
- Quote -> Order copy of is_sample champs works.
Performance considerations
Keep the eligibility logic fast. Avoid heavy DB queries per request; cache aggregated counters (e.g. per-client sample counts) in Redis for short lived TTLs. Use batch jobs for analytics au lieu de computing on-the-fly in the grille d'administration.
Extensibility and integration points
The module devrait être easy to extend:
- Expose events: sample.request.created, sample.request.fulfilled, sample.request.cancelled.
- Make eligibility checks pluggable via virtual types or plugins so commerçants can add custom rules.
- Provide a API web (REST) to create and query sample requests for tiers fulfillment systems or mobile apps.
- Provide data export hooks so an analytics platform (Google BigQuery or DataLake) can pull sample activity.
Admin UX details
Simple but useful admin fonctionnalités:
- Grid colonnes: request id, requested_at, client e-mail, product SKU, status, fulfilled_at, commande id, numéro de suivi.
- Filters: date range, product SKU, status, client e-mail.
- Bulk actions: mark as fulfilled (with optional numéro de suivi), export selected CSV.
- Dashboard widget: top 10 sample SKUs by requests, conversion rate by SKU, total cost of samples shipped this month (sample_prix * fulfilled_count).
Real-world constraints and shipping
Depending on region and product category, samples might be regulated (cosmetics, food, electronics batteries). Assurez-vous the module has shipping zone configuration and attribut produits to block requests outside allowed zones.
Analytics and KPI suggestions
Track these clé metrics:
- Request rate: # requests / # page produit views (or per marketing campaign impressions)
- Fulfillment rate: % of requests that were shipped within SLA
- Conversion rate: % of fulfilled samples that lead to a purchase of the same or related SKU within X days
- Cost per conversion: (sample cost + shipping + handling) / conversions
Example: sample-to-commande link (cron pseudocode)
// cron job run daily
$requests = $sampleRequestRepository->getFulfilledRequestsBetween($from, $to);
foreach ($requests as $request) {
$orders = $orderRepository->getCustomerOrdersAfter($request->getCustomerId(), $request->getFulfilledAt());
foreach ($orders as $order) {
foreach ($order->getAllItems() as $item) {
if ($item->getSku() === $request->getProductSku() || isSameCategory($request->getProductId(), $item->getProductId())) {
$request->setConverted(1);
$request->setConversionOrderId($order->getId());
$sampleRequestRepository->save($request);
}
}
}
}
Deployment and mise à jour notes
Follow standard Magento module déploiement: put code in app/code (or package as composer), run setup:mise à jour, compile, déployer static contenu, réindexer if needed, and clear caches. Si vous add attributes in InstallData, handle mise à jours carefully for existing stores.
SEO and marketing integration
Assurez-vous sample page produits are well-indexed when you want organic traffic to find sample offers — but use canonical tags if samples are duplicate or near duplicates of full products. If samples are campaign-limited, consider marking them noindex until they're live.
Résumé and next étapes
Ce guide covered the core pieces to build a robust product sampling module in Magento 2: a new product type, attribute schema, eligibility rules, cart and commande integration, admin UI, analytics, and production considerations. The exemple code gives a skeleton you can expand to add fonctionnalités like multi-item sample packs, deposit/return flows, or API RESTs for external fulfillment partners.
Si vous want, I can:
- Provide a downloadable skeleton module with the fichiers above.
- Help you adapt the eligibility engine to specific entreprise rules (e.g. loyalty tiers, campaign coupons).
- Walk through building the admin UI composant XML and data provider in a follow-up.
Want me to generate the scaffold code for the full module with db schema, UI composants, and a sample installer? Tell me what exact fonctionnalités you need (guest sampling? deposits? analytics window) and I’ll produce the full module skeleton you can install on a dev instance.