Magento 2 How to create conditional fieldset in the custom module

1

Hello Friends,

In this blog, I will explain you to How you can create the conditional fieldset in your custom module as you can see in the cart or catalog rules.

We will create custom rules and fieldset using model and interface without using Magento UI. 

So, let’s start, and I hope you have already created your custom module if not then check the below link that helps you to create the custom module with the admin grid.

Quick Tip:- How to Create Admin Grid Using UI Component?

Step 1: First you need to create conditions_serialized field using the installer/upgrade script

->addColumn(
	'conditions_serialized',
	\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
	null,
	['nullable' => true,'default' => null],
	'Conditions'
)

Step 2: Now create the model in the following path:

app/code/[Namespace]/[ModuleName]/Model/MagentoExpertise.php

<?php
namespace Namespace\Modulename\Model;

use Magento\Quote\Model\Quote\Address;
use Magento\Rule\Model\AbstractModel;

class MagentoExpertise extends AbstractModel
{
	protected $_eventPrefix = 'namespace_modulename';
	protected $_eventObject = 'magentoexpertise';
	protected $condCombineFactory;
	protected $condProdCombineF;
	protected $validatedAddresses = [];

	public function __construct(
		\Magento\Framework\Model\Context $context,
		\Magento\Framework\Registry $registry,
		\Magento\Framework\Data\FormFactory $formFactory,
		\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
		\Magento\SalesRule\Model\Rule\Condition\CombineFactory $condCombineFactory,
		\Magento\SalesRule\Model\Rule\Condition\Product\CombineFactory $condProdCombineF,
		\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
		\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
		array $data = []
	) {
		$this->condCombineFactory = $condCombineFactory;
		$this->condProdCombineF = $condProdCombineF;
		parent::__construct($context, $registry, $formFactory, $localeDate, $resource, $resourceCollection, $data);
	}

	protected function _construct()
	{
		parent::_construct();
		$this->_init('Namespace\Modulename\Model\ResourceModel\MagentoExpertise');
		$this->setIdFieldName('entity_id');
	}

	public function getConditionsInstance()
	{
		return $this->condCombineFactory->create();
	}

	public function getActionsInstance()
	{
		return $this->condProdCombineF->create();
	}

	public function hasIsValidForAddress($address)
	{
		$addressId = $this->_getAddressId($address);
		return isset($this->validatedAddresses[$addressId]) ? true : false;
	}

	public function setIsValidForAddress($address, $validationResult)
	{
		$addressId = $this->_getAddressId($address);
		$this->validatedAddresses[$addressId] = $validationResult;
		return $this;
	}

	public function getIsValidForAddress($address)
	{
		$addressId = $this->_getAddressId($address);
		return isset($this->validatedAddresses[$addressId]) ? $this->validatedAddresses[$addressId] : false;
	}

	private function _getAddressId($address)
	{
		if ($address instanceof Address) {
			return $address->getId();
		}
		return $address;
	}
}

In the above code, our model extends the Magento\Rule\Model\AbstractModel model that already has all the needed methods.

Like in the Constructor, we will add condition factories that allow us to work with them, and that’s why we can able to create multiple methods. It gives us a conclusion of how the model works.

Note: We are using the default condition models from the Magento SalesRule \Magento\SalesRule\Model\Rule\Condition module.
If you need to expand the conditions, you can add your classes or rewrite them entirely or acquire from the base available classes.

Step 3: Create the model that extends Rule Abstract Resource using this following path:

app/code/[Namespace]/[Modulename]/Model/ResourceModel/MagentoExpertise.php

<?php
namespace [Namespace]\[Modulename]\Model\ResourceModel;
class MagentoExpertise extends \Magento\Rule\Model\ResourceModel\AbstractResource
{
	protected function _construct()
	{
		$this->_init('Namespace_Modulename', 'entity_id');
	}
}

Step 4: Create the collection that extends Rule AbstractCollection using this following path:

app/code/[Namespace]/[Modulename]/Model/ResourceModel/MagentoExpertise/Collection.php

<?php
namespace [Namespace]\[Modulename]\Model\ResourceModel\MagentoExpertise;
class Collection extends \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection
{
	protected $date;

	public function __construct(
		\Magento\Framework\Data\Collection\EntityFactory $entityFactory,
		\Psr\Log\LoggerInterface $logger,
		\Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
		\Magento\Framework\Event\ManagerInterface $eventManager,
		\Magento\Framework\Stdlib\DateTime\TimezoneInterface $date,
		\Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
		\Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null
	) {
		parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $connection, $resource);
		$this->date = $date;
	}

	protected function _construct()
	{
		$this->_init('[Namespace]\[Modulename]\Model\MagentoExpertise', '[Namespace]\[Modulename]\Model\ResourceModel\MagentoExpertise');
	}

	public function setValidationFilter($now = null)
	{
		if (!$this->getFlag('validation_filter')) {
			$this->addDateFilter($now);
			$this->addIsActiveFilter();
			$this->setOrder('sort_order', self::SORT_ORDER_DESC);
			$this->setFlag('validation_filter', true);
		}

		return $this;
	}

	public function addDateFilter($now)
	{
		$this->getSelect()->where(
			'from_date is null or from_date <= ?',
			$now
		)->where(
			'to_date is null or to_date >= ?',
			$now
		);

		return $this;
	}
}

Step 5: Declare the common Controller using this following path:

app/code/[Namespace]/[Modulename]/Controller/Adminhtml/Index/Rule.php

<?php
namespace [Namespace]\[Modulename]\Controller\Adminhtml\Index;

abstract class Rule extends \Magento\Backend\App\Action
{
	protected $coreRegistry = null;
	protected $ruleFactory;

	public function __construct(
		\Magento\Backend\App\Action\Context $context,
		\Magento\Framework\Registry $coreRegistry,
		\[Namespace]\[Modulename]\Model\MagentoExpertiseFactory $ruleFactory,
		array $data = []
	) {
		$this->coreRegistry = $coreRegistry;
		$this->ruleFactory = $ruleFactory;
		parent::__construct($context);
	}

	protected function _initRule()
	{
		$rule = $this->ruleFactory->create();
		$this->coreRegistry->register(
			'magentoexpertise',
			$rule
		);
		$id = (int)$this->getRequest()->getParam('id');

		if (!$id &amp;&amp; $this->getRequest()->getParam('entity_id')) {
			$id = (int)$this->getRequest()->getParam('entity_id');
		}

		if ($id) {
			$this->coreRegistry->registry('magentoexpertise')->load($id);
		}
	}

	protected function _initAction()
	{
		$this->_view->loadLayout();
		$this->_setActiveMenu('[Namespace]_[Modulename]::magentoexpertise_manage')
			->_addBreadcrumb(__('Conditions'), __('Conditions'));
		return $this;
	}

	protected function _isAllowed()
	{
		return $this->_authorization->isAllowed('[Namespace]_[Modulename]::magentoexpertise_manage');
	}
}

In the above Controller, we are using three methods.

_initRule method is responsible for the current rule’s initialization or the creation of a new and empty one with the ability to add it to the register.

_initAction() method loads a layout and makes the modules menu available for actions (also, it adds breadcrumbs).

_isAllowed() method checks if the current admin has access to the Controller.

Step 6: Now your all adminhtml controllers will extend Rule Class, see the example of Delete controller in the following path:

app/code/[Namespace]/[Modulename]/Controller/Adminhtml/Index/Delete.php

<?php
namespace [Namespace]\[Modulename]\Controller\Adminhtml\Index;
class Delete extends \[Namespace]\[Modulename]\Controller\Adminhtml\Index\Rule
{
	protected function _isAllowed()
	{
		return $this->_authorization->isAllowed('[Namespace]_[Modulename]::magentoexpertise_delete');
	}

	public function execute()
	{
		$id = $this->getRequest()->getParam('entity_id');

		$resultRedirect = $this->resultRedirectFactory->create();
		if ($id) {
			$title = "";
			try {
				// init model and delete
				$model = $this->_objectManager->create('[Namespace]\[Modulename]\Model\MagentoExpertise');
				$model->load($id);
				$title = $model->getTitle();
				$model->delete();
				$this->messageManager->addSuccess(__('The data has been deleted.'));
				return $resultRedirect->setPath('*/*/');
			} catch (\Exception $e) {
				$this->messageManager->addError($e->getMessage());                
				return $resultRedirect->setPath('*/*/edit', ['page_id' => $id]);
			}
		}
		$this->messageManager->addError(__('We can\'t find a data to delete.'));
		return $resultRedirect->setPath('*/*/');
	}
}

Step 7: Same as New action will extend Rule Class, see the example in the following path:

app/code/[Namespace]/[Modulename]/Controller/Adminhtml/Index/NewAction.php

<?php
namespace [Modulename]\[Modulename]\Controller\Adminhtml\Index;
class NewAction extends \[Modulename]\[Modulename]\Controller\Adminhtml\Index\Rule
{
	protected $resultForwardFactory;
	public function __construct(
		\Magento\Backend\App\Action\Context $context,
		\Magento\Framework\Registry $registry,
		\[Modulename]\[Modulename]\Model\MagentoExpertiseFactory $ruleFactory,
		\Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory
	) {
		$this->resultForwardFactory = $resultForwardFactory;
		parent::__construct($context,$registry,$ruleFactory);
	}

	protected function _isAllowed()
	{
		return $this->_authorization->isAllowed('[Modulename]_[Modulename]::save');
	}

	public function execute()
	{
		$resultForward = $this->resultForwardFactory->create();
		return $resultForward->forward('edit');
	}
}

Step 8: Same as Edit action will extend Rule Class, see the example in the following path:

app/code/[Namespace]/[Modulename]/Controller/Adminhtml/Index/Edit.php

<?php
namespace [Modulename]\[Modulename]\Controller\Adminhtml\Index;
use Magento\Backend\App\Action;

class Edit extends \[Modulename]\[Modulename]\Controller\Adminhtml\Index\Rule 
{
	protected $_coreRegistry = null;
	protected $resultPageFactory;
	public function __construct(
		\Magento\Backend\App\Action\Context $context,
		\Magento\Framework\View\Result\PageFactory $resultPageFactory,
		\Magento\Framework\Registry $registry,
		\[Modulename]\[Modulename]\Model\MagentoExpertiseFactory $ruleFactory
	) {
		$this->resultPageFactory = $resultPageFactory;
		$this->_coreRegistry = $registry;
		parent::__construct($context,$registry,$ruleFactory);
	}

	protected function _isAllowed()
	{
		return $this->_authorization->isAllowed('[Modulename]_[Modulename]::save');
	}

	protected function _initAction()
	{
		$resultPage = $this->resultPageFactory->create();
		$resultPage->setActiveMenu(
			'[Modulename]_[Modulename]::magentoexpertise_manage'
		)->addBreadcrumb(
			__('[Modulename]'),
			__('[Modulename]')
		)->addBreadcrumb(
			__('Manage Form'),
			__('Manage Form')
		);
		return $resultPage;
	}

	public function execute()
	{
		// Get ID &amp; Create Model
		$id = $this->getRequest()->getParam('entity_id');
		$model = $this->_objectManager->create('[Modulename]\[Modulename]\Model\MagentoExpertise');
		
		if ($id) {
			$model->load($id);
			if (!$model->getId()) {
				$this->messageManager->addError(__('This form no longer exists.'));
				$resultRedirect = $this->resultRedirectFactory->create();
				return $resultRedirect->setPath('*/*/');
			}
		}

		// Set &amp; Save Data
		$data = $this->_objectManager->get('Magento\Backend\Model\Session')->getFormData(true);
		if (!empty($data)) {
			$model->setData($data);
		}

		$model->getConditions()->setJsFormObject('rule_conditions_fieldset');

		// Register Model To Use In Blocks
		$this->_coreRegistry->register('magentoexpertise', $model);

		// Build Edit Form		
		$resultPage = $this->_initAction();
		$resultPage->addBreadcrumb(
			$id ? __('Edit Form') : __('New'),
			$id ? __('Edit Form') : __('New')
		);
		$resultPage->getConfig()->getTitle()->prepend(__('Form'));
		$resultPage->getConfig()->getTitle()
			->prepend($model->getId() ? $model->getName() : __('New'));
		return $resultPage;
	}
}

Step 9: Same as Save action will extend Rule Class, see the example in the following path:

app/code/[Namespace]/[Modulename]/Controller/Adminhtml/Index/Save.php

<?php
namespace [Namespace]\[Modulename]\Controller\Adminhtml\Index;

use Magento\Framework\App\ObjectManager;
use Magento\Backend\App\Action;
use Magento\Framework\Serialize\Serializer\FormData;

class Save extends \[Namespace]\[Modulename]\Controller\Adminhtml\Index\Rule
{
	protected $dataProcessor;
	private $formDataSerializer;
	public function __construct(
		\Magento\Backend\App\Action\Context $context, 
		FormData $formDataSerializer = null,
		\Magento\Framework\Registry $registry,
		\[Namespace]\[Modulename]\Model\MagentoExpertiseFactory $ruleFactory,
		PostDataProcessor $dataProcessor
	) {
		$this->dataProcessor = $dataProcessor;
		$this->formDataSerializer = $formDataSerializer
			?: ObjectManager::getInstance()->get(FormData::class);
		parent::__construct($context,$registry,$ruleFactory);
	}

	protected function _isAllowed()
	{
		return $this->_authorization->isAllowed('[Namespace]_[Modulename]::save');
	}

	public function execute()
	{
		try {
			$optionData = $this->formDataSerializer
				->unserialize($this->getRequest()->getParam('serialized_options', '[]'));
		} catch (\InvalidArgumentException $e) {
			$message = __("The fee couldn't be saved due to an error. Verify your information and try again. "
				. "If the error persists, please try again later.");
			$this->messageManager->addErrorMessage($message);
			return $this->returnResult('magentoexpertise/*/edit', ['_current' => true], ['error' => true]);
		}
		$this->_eventManager->dispatch(
			'adminhtml_controller_magentoexpertise_prepare_save',
			['request' => $this->getRequest()]
		);
		$data = $this->getRequest()->getPostValue();		

		if(!empty($optionData)) {
			foreach ($optionData['option'] as $key => $value) {
				foreach ($value as $key1 => $value1) {
					if (!empty($optionData['option']['delete'][$key1])) {
						unset($optionData['option'][$key][$key1]);
					}
				}
			}

			foreach ($optionData['option'] as $key => $value) {
				foreach ($value as $key1 => $value1) {
					if($key == 'value'){
						foreach($value1 as $label){
							if (isset($value1[0]) &amp;&amp; $value1[0] == '') {
								$message = __("The value of Admin scope can't be empty");
								$this->messageManager->addErrorMessage($message);
								$this->_redirect('*/*/edit', ['entity_id' => $this->getRequest()->getParam('entity_id'), '_current' => true]);
								return;
							}
						}
					}
					if($key == 'price'){
						if(is_numeric($value1)){
							$optionData['option'][$key][$key1] = number_format($optionData['option'][$key][$key1],2);
						} else if(isset($value1) &amp;&amp; $value1 == '') {
							$optionData['option'][$key][$key1] = number_format(0,2);
						} else {
							$message = __("Please use only decimal value in price.");
							$this->messageManager->addErrorMessage($message);
							$this->_redirect('*/*/edit', ['entity_id' => $this->getRequest()->getParam('entity_id'), '_current' => true]);
							return;
						}
					}
				}
			}

			$serializer = $this->_objectManager->create(\Magento\Framework\Serialize\SerializerInterface::class);
			$data['serialized_options'] = $serializer->serialize($optionData);
		} else {
			$data['serialized_options'] = '';
		}

		if ($data) {
			$data = $this->dataProcessor->filter($data);
			$model = $this->_objectManager->create('[Namespace]\[Modulename]\Model\MagentoExpertise');
			$id = $this->getRequest()->getParam('entity_id');
			if ($id) {
				$model->load($id);
			}				
			$data = $this->prepareData($data);
			
			if (!$this->dataProcessor->validate($data)) {
				$this->_redirect('*/*/edit', ['entity_id' => $model->getId(), '_current' => true]);
				return;
			}
			$model->loadPost($data);
			try {
				$model->save();
				//$model->save();
				$this->messageManager->addSuccess(__('The Data has been saved.'));
				$this->_objectManager->get('Magento\Backend\Model\Session')->setFormData(false);
				if ($this->getRequest()->getParam('back')) {
					$this->_redirect('*/*/edit', ['entity_id' => $model->getId(), '_current' => true]);
					return;
				}
				$this->_redirect('*/*/');
				return;
			} catch (\Magento\Framework\Model\Exception $e) {
				$this->messageManager->addError($e->getMessage());
			} catch (\RuntimeException $e) {
				$this->messageManager->addError($e->getMessage());
			} catch (\Exception $e) {
				$this->messageManager->addException($e, __('Something went wrong while saving the data.'));
			}
			$this->_getSession()->setFormData($data);
			$this->_redirect('*/*/edit', ['entity_id' => $this->getRequest()->getParam('entity_id')]);
			return;
		}
		$this->_redirect('*/*/');
	}

	protected function prepareData($data)
	{   
		if (isset($data['rule']['conditions'])) {
			$data['conditions'] = $data['rule']['conditions'];
		}
		unset($data['rule']);
		return $data;
	}
}

In the above case, you should pay attention to the field where the conditions come. The prepareData method allows us to transfer conditions to the model before saving correctly.

Step 10: Here, you can see how you can add a new condition. Create new condition file in the following path:

app/code/[Namespace]/[Modulename]/Controller/Adminhtml/Index/NewConditionHtml.php

<?php
namespace [Namespace]\[Modulename]\Controller\Adminhtml\Index;

class NewConditionHtml extends \[Namespace]\[Modulename]\Controller\Adminhtml\Index\Rule
{
	public function execute()
	{
		$id = $this->getRequest()->getParam('id');
		$ruleFactory = $this->_objectManager->create('[Namespace]\[Modulename]\Model\MagentoExpertiseFactory');
		$typeArr = explode('|', str_replace('-', '/', $this->getRequest()->getParam('type')));
		$type = $typeArr[0];

		$model = $this->_objectManager->create(
			$type
		)->setId(
			$id
		)->setType(
			$type
		)->setRule(
			$ruleFactory->create()
		)->setPrefix(
			'conditions'
		);
		if (!empty($typeArr[1])) {
			$model->setAttribute($typeArr[1]);
		}

		if ($model instanceof \Magento\Rule\Model\Condition\AbstractCondition) {
			$model->setFormName('condition_form')->setJsFormObject('conditions_fieldset');			
			$html = $model->asHtmlRecursive();
		} else {
			$html = '';
		}
		$this->getResponse()->setBody($html);
	}
}

The above class is responsible for loading the conditions.

Step 11: Create the index action file in the following path:

app/code/[Namespace]/[Modulename]/Controller/Adminhtml/Index/Index.php

<?php
namespace [Namespace]\[Modulename]\Controller\Adminhtml\Index;

use Magento\Backend\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;

class Index extends \[Namespace]\[Modulename]\Controller\Adminhtml\Index\Rule
{
	protected $resultPageFactory;

	public function __construct(
		Context $context,
		\Magento\Framework\Registry $registry,
		\[Namespace]\[Modulename]\Model\MagentoExpertiseFactory $ruleFactory,
		PageFactory $resultPageFactory
	) {
		parent::__construct($context,$registry,$ruleFactory);
		$this->resultPageFactory = $resultPageFactory;
	}

	protected function _isAllowed()
	{
		return $this->_authorization->isAllowed('[Namespace]_[Modulename]::magentoexpertise_manage');
	}

	public function execute()
	{		
		$resultPage = $this->resultPageFactory->create();
		$resultPage->setActiveMenu(
			'[Namespace]_[Modulename]::magentoexpertise_manage'
		)->addBreadcrumb(
			__('[Modulename]'),
			__('[Modulename]')
		)->addBreadcrumb(
			__('Manage Form'),
			__('Manage Form')
		);
		$resultPage->getConfig()->getTitle()->prepend(__('Manage Conditional Fieldset'));
		return $resultPage;
	}
}

Step 12: Now, we need to create the Conditions tab in the following path:

app/code/[Namespace]/[Modulename]/Block/Adminhtml/MagentoExpertise/Edit/Tab/Conditions.php

<?php
namespace [Namespace]\[Modulename]\Block\Adminhtml\MagentoExpertise\Edit\Tab;

use Magento\Backend\Block\Widget\Form\Generic;
use Magento\Backend\Block\Widget\Tab\TabInterface;

class Conditions extends Generic implements TabInterface
{
	protected $rendererFieldset;
	protected $conditions;
	protected $ruleFactory;

	public function __construct(
		\Magento\Backend\Block\Template\Context $context,
		\Magento\Framework\Registry $registry,
		\Magento\Framework\Data\FormFactory $formFactory,
		\Magento\Rule\Block\Conditions $conditions,
		\[Namespace]\[Modulename]\Model\MagentoExpertiseFactory $ruleFactory,
		\Magento\Backend\Block\Widget\Form\Renderer\Fieldset $rendererFieldset,
		array $data = []
	) {
		$this->rendererFieldset = $rendererFieldset;
		$this->conditions = $conditions;
		$this->ruleFactory = $ruleFactory;
		parent::__construct($context, $registry, $formFactory, $data);
	}

	public function getTabLabel()
	{
		return __('Conditions');
	}

	public function getTabTitle()
	{
		return __('Conditions');
	}

	public function canShowTab()
	{
		return true;
	}

	public function isHidden()
	{
		return false;
	}

	protected function _prepareForm()
	{
		$model = $this->_coreRegistry->registry('magentoexpertise');
		$form = $this->addTabToForm($model);
		$this->setForm($form);
		return parent::_prepareForm();
	}

	protected function addTabToForm($model, $fieldsetId = 'conditions_fieldset', $formName = 'condition_form')
	{
		if (!$model) {
			$id = $this->getRequest()->getParam('id');
			$model = $this->ruleFactory->create();
			$model->load($id);
		}

		$newChildUrl = $this->getUrl('magentoexpertise/index/newConditionHtml/form/rule_conditions_fieldset');
		
		$form = $this->_formFactory->create();
		$form->setHtmlIdPrefix('rule_');
		$renderer = $this->rendererFieldset->setTemplate(
			'Magento_CatalogRule::promo/fieldset.phtml'
		)->setNewChildUrl(
			$newChildUrl
		);
		$fieldset = $form->addFieldset(
			$fieldsetId,
			[
				'legend' => __(
					'Apply the rule only if the following conditions are met (leave blank for all products).'
				)
			]
		)->setRenderer(
			$renderer
		);
		$fieldset->addField(
			'conditions',
			'text',
			[
				'name'           => 'conditions',
				'label'          => __('Conditions'),
				'title'          => __('Conditions'),
				'required'       => true,
				'data-form-part' => $formName
			]
		)->setRule(
			$model
		)->setRenderer(
			$this->conditions
		);
		$form->setValues($model->getData());		

		$this->setConditionFormName($model->getConditions(), $formName);
		return $form;
	}

	private function setConditionFormName(\Magento\Rule\Model\Condition\AbstractCondition $conditions, $formName)
	{
		$conditions->setFormName($formName);
		if ($conditions->getConditions() &amp;&amp; is_array($conditions->getConditions())) {
			foreach ($conditions->getConditions() as $condition) {
				$this->setConditionFormName($condition, $formName);
			}
		}
	}    
}

We use the Magento_CatalogRule:: promo/fieldset.phtml template to render a fieldset and magentoexpertise/index/newConditionHtml/form/rule_conditions_fieldset links to the below action which we created earlier  app/code/[Namespace]/[Modulename]/Controller/Adminhtml/Index/NewConditionHtml.php

Thanks for reading this post!

About the author

I’m Magento Certified Developer having quite 5 years of commercial development expertise in Magento as well as in Shopify. I’ve worked primarily with the Magento and Shopify e-commerce platform, managing the complexities concerned in building e-commerce solutions tailored to a client’s specific desires.

Related Posts

Leave a Reply