HowTo: Create a custom bundle product type in Magento 2

When the native bundle is close, but not quite what you need


On a recent project the native bundle product was almost what we needed, but not quite. Instead of fighting the bundle model with plugins everywhere, I decided to create a custom product type that inherits from it and only changes the bits I cared about.

It turned out to be simpler than I expected, and it kept the customisation contained in one place. These are the three things I had to sort out.

This post assumes that there is already a custom module created in app/code/Vendor/Module. If you need to know how to create a custom module, look at How to create a custom module for Magento 2.

1. Declare the custom product type

Every product type in Magento is declared in etc/product_types.xml. We inherit the native bundle model so we get all of its behaviour for free and only override what we need.

xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/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="custom_bundle"
          label="Custom Bundle Product"
          modelInstance="Vendor\Module\Model\Product\Type\CustomBundle"
          composite="true"
          indexPriority="60"
          sortOrder="60">
        <priceModel instance="Magento\Bundle\Model\Product\Price"/>
        <indexerModel instance="Magento\Bundle\Model\ResourceModel\Indexer\Price"/>
        <customAttributes>
            <attribute name="refundable" value="true"/>
        </customAttributes>
    </type>
</config>

The important attribute here is composite="true", that is what tells Magento the product is made of other products, exactly like the native bundle.

2. The Type model

Now the model referenced in modelInstance. Because we extend the native bundle type, we only implement the method that changes our behaviour and let the parent do the rest.

php
<?php
/** app/code/Vendor/Module/Model/Product/Type/CustomBundle.php */
namespace Vendor\Module\Model\Product\Type;

use Magento\Bundle\Model\Product\Type as BundleType;

class CustomBundle extends BundleType
{
    const TYPE_CODE = 'custom_bundle';

    /**
     * Example override: change how the selections are prepared.
     */
    public function getSku($product)
    {
        return parent::getSku($product) . '-custom';
    }
}

3. Why the product did not show in the frontend

This is the part that had me scratching my head for a while. After creating the type, the product simply would not appear on the storefront. The reason is that a brand new product type is not added to the list of visible types automatically.

You have to register it so the catalog knows it can be listed and added to the cart:

xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Catalog\Model\Product\Type">
        <arguments>
            <argument name="composableTypes" xsi:type="array">
                <item name="custom_bundle" xsi:type="string">custom_bundle</item>
            </argument>
        </arguments>
    </type>
</config>

After a bin/magento setup:upgrade and a cache flush, the product finally behaved like a first-class citizen in the catalog.

Hope this helps.

If you have any question or know a better way to achieve this, feel free to share in the comments below.