Product Types
The six bundled product types, what each is for, and why a configurable product has no price or stock of its own.
Magento\ConfigurableProduct\Api\LinkManagementInterface::getChildren($sku): ProductInterface[]Every product has a type_id. The string in that column selects a behaviour class, registered by a module in its product_types.xml. The type decides whether the product has a weight, whether it holds stock of its own, and how its price is calculated.
The six bundled types
| type_id | What it is for |
|---|---|
simple | One physical item with its own price and stock. |
virtual | Same as simple with no weight and no shipping: a service, a warranty. |
downloadable | Virtual, plus file links the customer receives after purchase. |
configurable | A chooser over simple or virtual children, one per option combination. |
bundle | A kit assembled from options; price can be fixed or dynamic. |
grouped | A list of other products shown together, each added to the cart separately. |
What a configurable actually stores
A configurable product has no price, no weight and no stock. It stores two things: which attributes are the axes, in catalog_product_super_attribute, and which children exist, in catalog_product_super_link. The price on the storefront comes from the children through the catalog_product_price index, and adding to the cart adds the matching child.
So a configurable whose children are all disabled or out of stock shows no price at all. That is the data, not the theme.
Children must be simple or virtual, should be set to Not Visible Individually, and must carry a value for every axis attribute. A child missing its size value is dropped from the chooser silently.
Example
<?php
// app/code/Vendor/Catalog/ViewModel/ChildSkus.php
declare(strict_types=1);
namespace Vendor\Catalog\ViewModel;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\ConfigurableProduct\Api\LinkManagementInterface;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\View\Element\Block\ArgumentInterface;
class ChildSkus implements ArgumentInterface
{
public function __construct(
private readonly LinkManagementInterface $linkManagement
) {
}
/**
* @return string[]
*/
public function getChildSkus(ProductInterface $product): array
{
if ($product->getTypeId() !== Configurable::TYPE_CODE) {
return [];
}
$skus = [];
foreach ($this->linkManagement->getChildren((string)$product->getSku()) as $child) {
$skus[] = (string)$child->getSku();
}
return $skus;
}
}
// Wired into a .phtml through a layout argument. On a configurable product
// page it returns the SKUs of the linked simple products; on a simple product
// page it returns an empty array and the template renders nothing.
// Changing the class requires bin/magento setup:di:compile in production mode.When to use it
- Decide whether a new catalog needs configurable products or just simple ones with a dropdown attribute.
- Diagnose a configurable product whose storefront page shows no price.
- List the variant SKUs behind a configurable for a stock report or a support tool.
- Choose between a bundle and a grouped product for a "build your own box" page.
More examples
Rendering the child SKUs in a template
A merchandising or support view that lists the variant SKUs behind a configurable. Note the two escapers: escapeHtml for text between tags, escapeHtmlAttr for a value inside an attribute. Using escapeHtml in an attribute leaves quotes intact and lets markup out. The $escaper and $block variables are provided to every .phtml by the template engine, so there is nothing to inject. In production mode, run bin/magento setup:static-content:deploy after adding the file.
<?php
// app/code/Vendor/Catalog/view/frontend/templates/product/child-skus.phtml
/** @var \Vendor\Catalog\ViewModel\ChildSkus $viewModel */
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Magento\Catalog\Block\Product\View $block */
$viewModel = $block->getData('child_skus_view_model');
$skus = $viewModel->getChildSkus($block->getProduct());
?>
<?php if ($skus): ?>
<ul>
<?php foreach ($skus as $sku): ?>
<li data-sku="<?= $escaper->escapeHtmlAttr($sku) ?>">
<?= $escaper->escapeHtml($sku) ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>How a type is registered
This is the core file, shown so you can read it rather than copy it. It is worth seeing because it answers where a type's behaviour comes from: modelInstance is the class asked about children and options, priceModel is what computes the displayed price, and composite="true" marks a type made of other products. A custom type lives in your module's own etc/product_types.xml with the same structure, and needs bin/magento setup:upgrade followed by bin/magento cache:flush to be picked up.
<?xml version="1.0"?>
<!-- app/code/Magento/ConfigurableProduct/etc/product_types.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Catalog:etc/product_types.xsd">
<type name="configurable"
label="Configurable Product"
modelInstance="Magento\ConfigurableProduct\Model\Product\Type\Configurable"
composite="true"
indexPriority="30"
sortOrder="20">
<priceModel instance="Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price" />
<indexerModel instance="Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\Configurable" />
<stockIndexerModel instance="Magento\ConfigurableProduct\Model\ResourceModel\Indexer\Stock\Configurable" />
<customAttributes>
<attribute name="refundable" value="true"/>
</customAttributes>
</type>
</config>Where the type codes live
Each type's code is a constant on the class that owns the type, and the constants are not all named alike: Magento\Catalog\Model\Product\Type has TYPE_SIMPLE and TYPE_VIRTUAL, Downloadable uses TYPE_DOWNLOADABLE, and the composite types use TYPE_CODE. Use this when you need to compare a type id in a condition, so a typo in a string literal fails at compile time instead of at runtime on a customer's product page.
<?php
// app/code/Vendor/Catalog/Model/TypeCodes.php
declare(strict_types=1);
namespace Vendor\Catalog\Model;
use Magento\Bundle\Model\Product\Type as BundleType;
use Magento\Catalog\Model\Product\Type as CatalogType;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Downloadable\Model\Product\Type as DownloadableType;
use Magento\GroupedProduct\Model\Product\Type\Grouped;
class TypeCodes
{
/**
* @return string[]
*/
public function all(): array
{
return [
CatalogType::TYPE_SIMPLE,
CatalogType::TYPE_VIRTUAL,
DownloadableType::TYPE_DOWNLOADABLE,
Configurable::TYPE_CODE,
BundleType::TYPE_CODE,
Grouped::TYPE_CODE,
];
}
}
// Output:
// ['simple', 'virtual', 'downloadable', 'configurable', 'bundle', 'grouped']
Discussion