Magento2 | PWA | GraphQL

How to create a new product type in Magento 2?


Magento 2 provides 6 product types including simple, grouped, configurable, virtual, bundled and downloadable having unique behaviors and attributes.

In a such kind of requirement where your product class has distinct behavior or attributes then it should be represented by its own custom product type. This allows the product type to have complex and custom logic and presentation, with no impact on other product types, ensuring that default product types can continue to function as intended.

In this post, I will be guiding you on how to create a new Magento 2 product type.

You can find the complete module in Github at Magelearn_BookProductType


Let's start by creating a custom module.

Create a folder inside app/code/Magelearn/BookProductType

Add registration.php file in it:

<?php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Magelearn_BookProductType',
    __DIR__
);

Add composer.json file in it:

{
    "name": "magelearn/magento2-book-product-type",
    "description": "Custom product types for magento 2",
    "license": "proprietary",
    "authors": [
        {
            "name": "vijay rami",
            "email": "vijaymrami@gmail.com"
        }
    ],
    "require": {},
    "type": "magento2-module",
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "Magelearn\\BookProductType\\": ""
        }
    }
}

Add etc/module.xml file in it:

<?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="Magelearn_BookProductType" setup_version="1.0.2">
        <sequence>
            <module name="Magento_Catalog"/>
        </sequence>
    </module>
</config>
To add a new product type we will define all necessary information in the XML declaration of product_types.xml.

For that first, add etc/product_types.xml file.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Catalog:etc/product_types.xsd">
    <type name="book" label="Book Product Type" modelInstance="Magelearn\BookProductType\Model\Product\Type\Bookproducttype"
    				  indexPriority="0" sortOrder="80" isQty="true">
    	<customAttributes>
            <attribute name="refundable" value="true"/>
        </customAttributes>
    	<priceModel instance="Magelearn\BookProductType\Model\Product\Price" />
    </type>
    <composableTypes>
        <type name="book" />
    </composableTypes>
</config>

In type, we can define the following attributes.

  • name: new product code (book)
  • label: product name (Book Product Type)
  • modelInstance: new product model
  • indexPriority: priority index
  • isQty: whether it has a quantity
  • sortOrder: position number in sort list
  • canUseQtyDecimals: whether the quantity is fractional

Also we are capable to override such internal models:

  • priceModel: model of a price
  • indexerModel: model of indexing logic
  • stockIndexerModel: model of stock indexing

Composite Products

When you are adding child products to a composite product type like configurable, grouped or bundle, it will only show products where the associated product type has explicitly declared its eligibility to be the child of a composite product.

So new product types can allow themselves to be children of composite product types by adding a composableTypes node in product_types.xml

Custom Attributes Node

The customAttributes nodes can be used when Magento wants to get a list of product types that comply with the condition.

Inside this node, we can declare some attributes nodes:

  • refundable: It will check the product type, it can be refunded or not. By default Magento CE, the downloadable and virtual product cannot be refunded (In Magento EE added Gift product). The product type in sales order item table will be used for checking the refundable product.
  • taxable: It will check the product type, it is taxable or not.
  • Is_real_product: It is used for checking “it is a real product or not”. Group product, Downloadable Product, Virtual Product are not real products.
  • is_product_set: checks whether product type is a set of products. In group product is_product_set value is set to true.

Product Type Model

Each product type instance is associated with an instance of the corresponding product type model. Here in this Product model calss, you can modify product type behaviour and attributes and is called during several product manipulation processes. You can apply your custom logic in the product type model.

Create the app/code/Magelearn/BookProductType/Model/Product/Type/Bookproducttype.php file

<?php
declare(strict_types=1);

namespace Magelearn\BookProductType\Model\Product\Type;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Type\AbstractType;

/**
 * Book product type implementation
 */
class Bookproducttype extends AbstractType
{
    /**
     * Product type code
     */
    const TYPE_CODE = 'book';
    
    public function save($product)
    {
        parent::save($product);
        //  your additional saving logic
        return $this;
    }

    /**
     * Delete data specific for Simple product type
     *
     * @param \Magento\Catalog\Model\Product $product
     * @return void
     */
    public function deleteTypeSpecificData(Product $product)
    {
    }
}

A product model must inherit from the \Magento\Catalog\Model\Product\Type\AbstractType class. This class has only one abstract method and it is deleteTypeSpecificData. This specific method is called while saving a product instance if its type has changed, and gives the original product type the opportunity to clean up any type-specific data before the type change is finalized.

Unless the new custom product type has such type-specific data, the method can be overridden with an empty method.

Core Product Types

It’s often the case that a custom product type behaves like and has similar requirements for a core product type. In that case, it will be useful to extend one of the core product types instead of the abstract type directly.

The following product type classes are available in the core Magento.
  • Simple: \Magento\Catalog\Model\Product\Type\Simple
  • Virtual: \Magento\Catalog\Model\Product\Type\Virtual
  • Configurable: \Magento\ConfigurableProduct\Model\Product\Type\Configurable
  • Grouped: \Magento\GroupedProduct\Model\Product\Type\Grouped
  • Downloadable: \Magento\Downloadable\Model\Product\Type
  • Bundle: \Magento\Bundle\Model\Product\Type
  • Giftcard (Enterprise Edition Only): \Magento\GiftCard\Model\Catalog\Product\Type\Giftcard

Product Type Model Methods

The behavior of products can be significantly influenced by overriding methods in the product type model. This allows custom logic to be applied within the product type. So you don’t have to create any observer or plugin to do that.

Some examples of such methods:
  • beforeSave() : This method runs during the beforeSave() products of the given type.
  • save(): This method runs during the afterSave() products of the given type.
  • isSalable(): This method allows a custom product type to customize product saleability.
  • _prepareProduct(): This method provides a hook to interact with the information by request when a product is initially added to the cart.
  • isVirtual(): By default it is false. If your product type is like virtual type then you can use this function and return true.

Associate With Common Attributes

Since product attributes can be scoped to relevant product types, any core attributes which are already scoped will not apply to the new product type. Thus attributes that are indeed relevant will need to be associated with the new product type. Hence you have to create one Install data script in order to make these changes.

First, we will add one custom attribute to this new product attribute type.

Create file at app/code/Magelearn/BookProductType/Setup/InstallData.php file
<?php
namespace Magelearn\BookProductType\Setup;

use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Eav\Setup\EavSetupFactory;

/**
 * Class InstallData
 * @package Magelearn\BookProductType\Setup
 *
 * @codeCoverageIgnore
 */
class InstallData implements InstallDataInterface
{
    /**
     * @var EavSetupFactory
     */
    protected $_eavSetupFactory;

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

    /**
     * {@inheritdoc}
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
     * @SuppressWarnings(PHPMD.NPathComplexity)
     */
    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        $eavSetup = $this->_eavSetupFactory->create(["setup"=>$setup]);
        $productTypes = 'book';
        $eavSetup->addAttribute(
            \Magento\Catalog\Model\Product::ENTITY,
            'isbn',
            [
                'group' => 'Product Details',
                'backend' => '',
                'frontend' => '',
                'label' => 'ISBN',
                'type' => 'text',
                'input' => 'text',
                'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE,
                'visible' => true,
                'required' => true,
                'user_defined' => true,
                'apply_to' => $productTypes,
                'visible_on_front' => true,
                'used_in_product_listing' => true,
                'is_used_in_grid' => true,
                'is_visible_in_grid' => false,
                'is_filterable_in_grid' => true,
            ]
        );
    }
}

After we will assign core attributes that are already scoped to this new product attribute type.

For that add file at app/code/Magelearn/BookProductType/Setup/UpgradeData.php

<?php

namespace Magelearn\BookProductType\Setup;

use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\UpgradeDataInterface;

class UpgradeData implements UpgradeDataInterface
{
    /**
     * @var EavSetupFactory
     */
    private $eavSetupFactory;
    /**
     * @var Logger
     */
    private $logger;

    /**
     * @param EavSetupFactory $eavSetupFactory
     */
    public function __construct(EavSetupFactory $eavSetupFactory, \Psr\Log\LoggerInterface $logger)
    {
        $this->eavSetupFactory = $eavSetupFactory;
        $this->logger = $logger;
    }

    public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        if (version_compare($context->getVersion(), '1.0.2', '<')) {
            $this->upgradePriceForBooks($setup);
        }
    }

    public function upgradePriceForBooks(ModuleDataSetupInterface $setup)
    {
        /** @var EavSetup $eavSetup */
        $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);

        //associate these attributes with new product type
        $fieldList = [
            'price',
            'special_price',
            'special_from_date',
            'special_to_date',
            'minimal_price',
            'cost',
            'tier_price',
        ];

        // make these attributes applicable to book product type
        foreach ($fieldList as $field) {
            $applyTo = explode(
                ',',
                $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field, 'apply_to')
            );
            if (!in_array(\Magelearn\BookProductType\Model\Product\Type\Bookproducttype::TYPE_CODE, $applyTo)) {
                $applyTo[] = \Magelearn\BookProductType\Model\Product\Type\Bookproducttype::TYPE_CODE;
                $eavSetup->updateAttribute(
                    \Magento\Catalog\Model\Product::ENTITY,
                    $field,
                    'apply_to',
                    implode(',', $applyTo)
                );
            }
        }
        $this->logger->info('Book attributes upgraded');
    }
}

And at last, we will add our uninstall script.

Add file at app/code/Magelearn/BookProductType/Setup/Uninstall.php

<?php
namespace Magelearn\BookProductType\Setup;

use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\Setup\UninstallInterface as UninstallInterface;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\ModuleDataSetupInterface;

class Uninstall implements UninstallInterface
{
    /**
    * EAV setup factory
    *
    * @var EavSetupFactory
    */
    private $_eavSetupFactory;

    private $_mDSetup;
    /**
    * Init
    *
    * @param EavSetupFactory $eavSetupFactory
    */
    public function __construct(
        EavSetupFactory $eavSetupFactory,
        ModuleDataSetupInterface $mDSetup
    )
    {
        $this->eavSetupFactory = $eavSetupFactory;
        $this->moduleDataSetup = $mDSetup;
    }
    
    public function uninstall(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $this->moduleDataSetup->getConnection()->startSetup();
        /** @var EavSetup $eavSetup */
        $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]);
        $eavSetup->removeAttribute(\Magento\Catalog\Model\Product::ENTITY, 'isbn');

        $this->moduleDataSetup->getConnection()->endSetup();
    }
}

Price Model

In addition to the product type model, a module can specify a price model in the product type definition XML.

The price model should extend \Magento\Catalog\Model\Product\Type\Price. This class has no abstract methods which must be implemented but allows extending classes to interact with nearly every aspect of price calculation.

Add our Price Model class at app/code/Magelearn/BookProductType/Model/Product/Price.php
<?php
declare(strict_types=1);

namespace Magelearn\BookProductType\Model\Product;

/**
 * Class Price
 * @package Magelearn\BookProductType\Model\Product
 */
class Price extends \Magento\Catalog\Model\Product\Type\Price
{
    /**
     * Default action to get price of product
     *
     * @param Product $product
     * @return float
     */
    public function getPrice($product)
    {
        return $product->getData('price')*1.25;
    }
}

You can also add indexerModel and stockIndexerModel like priceModel in the product type definition XML.

Now we will check how to render this custom attribute for this custom product type on catalog category and catalog product pages.

To display the custom attribute, ISBN on Catalog category view page,

Add file at app/code/Magelearn/BookProductType/view/frontend/layout/catalog_category_view.xml

<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="category.product.addto">
            <block class="Magelearn\BookProductType\Block\Product\View\Book" name="catalog.product.book.isbn" template="Magelearn_BookProductType::display-isbn.phtml" before="compare" />
        </referenceBlock>
    </body>
</page>

Since our custom product type is a 'book' product type,

So that we will add catalog_product_view_type_book.xml file to display this custom attribute on Catalog Product view page.

Add file at app/code/Magelearn/BookProductType/view/frontend/layout/catalog_product_view_type_book.xml

<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<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.main">
            <block class="Magento\Catalog\Block\Product\View" name="product.info.isbn" template="Magelearn_BookProductType::display-isbn.phtml" />
        </referenceContainer>
    </body>
</page>
Now add a Block file at,
app/code/Magelearn/BookProductType/Block/Product/View/Book.php

<?php
declare(strict_types=1);

namespace Magelearn\BookProductType\Block\Product\View;

/**
 * Block for Book in listing page
 *
 * @api
 * @since 100.1.1
 */
class Book extends \Magento\Catalog\Block\Product\ProductList\Item\Block
{

}

And template file at app/code/Magelearn/BookProductType/view/frontend/templates/display-isbn.phtml

<?php
$product = $block->getProduct();
$product_type = $product->getTypeId();
?>
<?php if($product_type == 'book'): ?>
<div class="isbn">
    <label class="label"><span><strong><?= __('ISBN: ')?></strong><?= $block->escapeHtml($product->getIsbn()) ?></span></label>
</div>
<?php endif; ?>

0 Comments On "How to create a new product type in Magento 2?"

Back To Top