How to create product type in Magento 2
In Magento has 6 types of products available including simple, grouped, configurable, virtual, bundled and downloadable. It also supports creating new attributes and then adding them to products, which almost meets the needs of creating products with an e-commerce site. However it may be because your actual requirement needs to create a product type with behaviors and attributes that are not available from default magento. That’s why we learn how to create new product type in Magento.
In this article, we will assume building a gift product type in the Magerubik_GiftCard module. It is assumed that you have done building the basic structure declaring the module.
1. Declare new product type
Create the app/code/Aureatelabs/NewProductType/etc/product_types.xml file then put content same below:
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../Magento/Catalog/etc/product_types.xsd"> <type name="mrgiftcard" label="Gift Card" modelInstance="Magerubik\GiftCard\Model\Product\Type\GiftCard" composite="false" indexPriority="45" sortOrder="70" isQty="true"> <priceModel instance="Magerubik\GiftCard\Model\Product\Price"/> <customAttributes> <attribute name="refundable" value="true"/> </customAttributes> </type> </config>
- In there
- – “name”: new product type code
- – “label”: new product type name
- – “modelInstance”: new product type model
- – “composite”: allow being children of composite product types
- – “indexPriority”: priority index
- – “isQty”: whether it has a quantity
- – “sortOrder”: position number in the sort list
- The customAttributes nodes can be used when Magento wants to get a list of product types that comply with the condition.
you can also refer to other properties in the file /vendor/magento/module-catalog/etc/product_types.xsd
In addition, you can also override the models: priceModel, indexerModel, stockIndexerModel.
2. Create Product Type Model
Each product type will have a corresponding model, which is the place to modify behavior and attribute. So, you can apply your custom logic in the product type model.
For simplicity you can choose to extend from the product type defalut then override the functions. in this case, I was choose extend from AbstractType
Create the Magerubik\GiftCard\Model\Product\Type\GiftCard.php file then put content same below:
<?php namespace Magerubik\GiftCard\Model\Product\Type; use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\Product\Type\AbstractType; use Magento\Eav\Model\Config; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Request\Http; use Magento\Framework\DataObject; use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Phrase; use Magento\Framework\Registry; use Magento\MediaStorage\Helper\File\Storage\Database; use Magerubik\GiftCard\Helper\Product as DataHelper; use Magerubik\GiftCard\Model\Source\FieldRenderer; use Psr\Log\LoggerInterface; use Zend_Serializer_Exception; class GiftCard extends AbstractType { const TYPE_GIFTCARD = 'mrgiftcard'; protected $_dataHelper; protected $request; protected $logger; public function __construct( Option $catalogProductOption, Config $eavConfig, Type $catalogProductType, ManagerInterface $eventManager, Database $fileStorageDb, Filesystem $filesystem, Registry $coreRegistry, LoggerInterface $logger, ProductRepositoryInterface $productRepository, DataHelper $dataHelper, Http $request, ) { $this->request = $request; $this->_dataHelper = $dataHelper; $this->logger = $logger; parent::__construct( $catalogProductOption, $eavConfig, $catalogProductType, $eventManager, $fileStorageDb, $filesystem, $coreRegistry, $logger, $productRepository ); } /** * Sets flag that product has required options, because gift card always * has some required options, at least - recipient name * @param Product $product * @return $this */ public function beforeSave($product) { parent::beforeSave($product); $product->setTypeHasOptions(true); $product->setTypeHasRequiredOptions(true); return $this; } /** * Check if Gift Card product is available for sale * @param Product|AbstractModel $product * @return bool */ public function isSalable($product) { $product = $product->load($product->getId()); if (!count($product->getGiftCardAmounts())) { return false; } return parent::isSalable($product); } /** * @param DataObject $buyRequest * @param Product $product * @param string $processMode * @return array|Phrase|string */ protected function _prepareProduct(DataObject $buyRequest, $product, $processMode) { $result = parent::_prepareProduct($buyRequest, $product, $processMode); if (is_string($result)) { return $result; } try { return $this->prepareGiftCardData($buyRequest, $product->load($product->getId())); } catch (LocalizedException $e) { return $e->getMessage(); } catch (Exception $e) { $this->logger->critical($e); return __('Something went wrong.'); } } /** * @param DataObject $buyRequest * @param Product $product * @return array * @throws LocalizedException */ protected function prepareGiftCardData($buyRequest, $product) { $redirectUrl = $this->request->getServer('REDIRECT_URL'); if (strpos($redirectUrl, 'wishlist/index/add/') !== false) { return [$product]; } $amount = $this->_validateAmount($buyRequest, $product); $product->addCustomOption(FieldRenderer::AMOUNT, $amount, $product); if ($sender = $buyRequest->getFrom()) { $product->addCustomOption(FieldRenderer::SENDER, ucfirst($sender), $product); } if ($recipient = $buyRequest->getTo()) { $product->addCustomOption(FieldRenderer::RECIPIENT, ucfirst($recipient), $product); } if ($message = $buyRequest->getMessage()) { $product->addCustomOption(FieldRenderer::MESSAGE, $message, $product); } return [$product]; } /** * @param DataObject $buyRequest * @param Product $product * @return float * @throws LocalizedException */ protected function _validateAmount($buyRequest, $product) { $amount = $buyRequest->getAmount(); $currentAction = $this->request->getFullActionName(); $allowAmounts = []; $attribute = $product->getResource()->getAttribute('gift_card_amounts'); if ($attribute) { $attribute->getBackend()->afterLoad($product); $allowAmounts = $product->getGiftCardAmounts(); } $allowAmountValues = array_column($allowAmounts, 'amount'); if ($currentAction !== 'wishlist_index_add' && (!count($allowAmounts) || !in_array($amount, $allowAmountValues, true))) { throw new LocalizedException( __('Please choose your amount again. The allowed amount must be one of these values: %1', implode(',', $allowAmountValues) ) ); } return $amount; } /** * Check if product can be bought * @param Product $product * @return $this * @throws LocalizedException * @throws Zend_Serializer_Exception */ public function checkProductBuyState($product) { parent::checkProductBuyState($product); $option = $product->getCustomOption('info_buyRequest'); if ($option instanceof \Magento\Quote\Model\Quote\Item\Option) { $buyRequest = new DataObject($this->_dataHelper->unserialize($option->getValue())); $this->prepareGiftCardData($buyRequest, $product); } return $this; } /** * @param Product $product * @param DataObject $buyReques * @return array */ public function processBuyRequest($product, $buyRequest) { $delivery = (int)$buyRequest->getDelivery(); $options = [ 'amount' => $buyRequest->getAmount(), 'from' => $buyRequest->getFrom(), 'to' => $buyRequest->getTo(), 'message' => $buyRequest->getMessage(), 'delivery_date' => $buyRequest->getDeliveryDate(), ]; return $options; } /** * Delete data specific for Gift Card product type * @param Product $product * @return void */ public function deleteTypeSpecificData(Product $product) { } }
By overriding the methods in the model you can control the behavior of the product as you like without having to use observers or plugins.Some examples of such methods: beforeSave(), save(), isSalable(), _prepareProduct(), isVirtual()
After the product type is declared in the XML and the corresponding product type model is created, you can see new product type in the admin.
3. Create Install data file
In this file will to create attributes associated with the new product type.
Create the Magerubik\GiftCard\Setup\InstallData.php file then put content same below:
<?php namespace Magerubik\GiftCard\Setup; use Exception; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Backend\Boolean; use Magento\Catalog\Model\Product\Attribute\Backend\Price; use Magento\Catalog\Model\Product\AttributeSet\Options; use Magento\Catalog\Setup\CategorySetup; use Magento\Catalog\Setup\CategorySetupFactory; use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\Eav\Setup\EavSetupFactory; use Magento\Framework\Setup\InstallDataInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magerubik\GiftCard\Model\Attribute\Backend\Amount; use Magerubik\GiftCard\Model\Attribute\Backend\MultiSelect; use Magerubik\GiftCard\Model\Attribute\Backend\Pattern; use Magerubik\GiftCard\Model\GiftCard\Template; use Magerubik\GiftCard\Model\Product\DeliveryMethods; use Magerubik\GiftCard\Model\Product\Type\GiftCard; class InstallData implements InstallDataInterface { protected $eavSetupFactory; protected $categorySetupFactory; protected $templateFactory; protected $_attributeSet; public function __construct( EavSetupFactory $eavSetupFactory, CategorySetupFactory $categorySetupFactory, Options $attributeSet ) { $this->eavSetupFactory = $eavSetupFactory; $this->categorySetupFactory = $categorySetupFactory; $this->_attributeSet = $attributeSet; } public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) { $installer = $setup; $catalogSetup = $this->categorySetupFactory->create(['setup' => $setup]); $installer->startSetup(); $entityTypeId = $catalogSetup->getEntityTypeId(Category::ENTITY); foreach ($this->_attributeSet->toOptionArray() as $set) { $catalogSetup->addAttributeGroup($entityTypeId, $set['value'], 'Gift Card Information', 10); } $catalogSetup->addAttribute(Product::ENTITY, 'gift_code_pattern', array_merge($this->getDefaultOptions(), [ 'label' => 'Gift Code Pattern', 'type' => 'varchar', 'input' => 'text', 'backend' => Pattern::class, 'input_renderer' => \Magerubik\GiftCard\Block\Adminhtml\Product\Helper\Form\Config\Pattern::class, 'required' => true, 'sort_order' => 10 ])); $catalogSetup->addAttribute(Product::ENTITY, 'gift_card_amounts', array_merge($this->getDefaultOptions(), [ 'label' => 'Gift Card Amounts', 'type' => 'varchar', 'input' => 'text', 'backend' => Amount::class, 'global' => ScopedAttributeInterface::SCOPE_WEBSITE, 'sort_order' => 20 ])); $this->addRemoveApply($catalogSetup); $installer->endSetup(); } protected function addRemoveApply($catalogSetup) { $fieldAdd = ['weight']; foreach ($fieldAdd as $field) { $applyTo = $catalogSetup->getAttribute('catalog_product', $field, 'apply_to'); if ($applyTo) { $applyTo = explode(',', $applyTo); if (!in_array(GiftCard::TYPE_GIFTCARD, $applyTo, true)) { $applyTo[] = GiftCard::TYPE_GIFTCARD; $catalogSetup->updateAttribute('catalog_product', 'weight', 'apply_to', implode(',', $applyTo)); } } } $fieldRemove = ['cost']; foreach ($fieldRemove as $field) { $applyTo = explode(',', $catalogSetup->getAttribute('catalog_product', $field, 'apply_to')); if (in_array('mrgiftcard', $applyTo, true)) { foreach ($applyTo as $k => $v) { if ($v === 'mrgiftcard') { unset($applyTo[$k]); break; } } $catalogSetup->updateAttribute('catalog_product', $field, 'apply_to', implode(',', $applyTo)); } } return $this; } protected function getDefaultOptions() { return [ 'group' => 'Gift Card Information', 'backend' => '', 'frontend' => '', 'class' => '', 'source' => '', 'global' => ScopedAttributeInterface::SCOPE_STORE, 'visible' => true, 'required' => false, 'user_defined' => true, 'default' => '', 'searchable' => false, 'filterable' => false, 'comparable' => false, 'visible_on_front' => false, 'unique' => false, 'apply_to' => GiftCard::TYPE_GIFTCARD, 'used_in_product_listing' => true ]; } }
3. Create Price Model
This file allows extending classes to interact with nearly every aspect of price calculation.
Create the Magerubik\GiftCard\Setup\InstallData.php file then put content same below:
<?php namespace Magerubik\GiftCard\Model\Product; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Type\Price as CatalogPrice; class Price extends CatalogPrice { public function getFinalPrice($qty, $product) { if ($qty === null && $product->getCalculatedFinalPrice() !== null) { return $product->getCalculatedFinalPrice(); } $finalPrice = $this->getPrice($product); if ($product->hasCustomOptions()) { $amount = $product->getCustomOption('amount'); $amount = (float)($amount ? $amount->getValue() : 0); $attribute = $product->getResource()->getAttribute('gift_card_amounts'); $attribute->getBackend()->afterLoad($product); $allowAmounts = $product->load($product->getId())->getGiftCardAmounts(); foreach ($allowAmounts as $amountValue) { if ((float)$amountValue['amount'] === $amount) { $finalPrice = $amountValue['price']; break; } } } $product->setFinalPrice($finalPrice); $this->_eventManager->dispatch('catalog_product_get_final_price', ['product' => $product, 'qty' => $qty]); $finalPrice = $product->getData('final_price'); $finalPrice = $this->_applyOptionsPrice($product, $qty, $finalPrice); $finalPrice = max(0, $finalPrice); $product->setFinalPrice($finalPrice); return $finalPrice; } }
To finish creating a product type you need to add behaviors for the product when adding to cart and after checkout. In addition, you also have to build the frontend, backend, email… and we will cover those issues in another article. In the next posts we will learn how to create category attribute in Magento 2. Contact us if you face any problems during the installation process.
You can download the demo code for this entire series from GitHub