The example of Magento 2 Module with Conditions Model and Fieldset (Part 1)

9
43440
Reading Time: 13 minutes

While browsing Magento forums we often bump to the threads where developers are asking how to create an extension with custom rules and fieldset. Hence, we’ve decided to illustrate that on the example of a sample module that can be also used for further validating and other development purposes.

Let’s roll in!

To add a conditional field to the default model (and further utilize it for validating purposes), you will need to create a separate model (or modify the existing one) that contains the corresponding fields.

Below, you’ll find a description of how to add the condition to both the model and interface without using Magento UI.

First, let’s create a new and simple module for that (or make some changes in the module you already have):

app/code/Vendor/Rules/registration.php

    <?php
    \Magento\Framework\Component\ComponentRegistrar::register(
        \Magento\Framework\Component\ComponentRegistrar::MODULE,
        'Vendor_Rules',
        __DIR__
    );
    ?>

app/code/Vendor/Rules/composer.json

    {
        "name": "vendor/module-rules",
        "description": "N/A",
        "type": "magento2-module",
        "version": "1.0.0",
        "license": [
            "OSL-3.0",
            "AFL-3.0"
        ],
        "autoload": {
            "files": [
                "registration.php"
            ],
            "psr-4": {
                "Vendor\\Rules\\": ""
            }
        }
    }

app/code/Vendor/Rules/etc/module.xml

    <?xml version="1.0"?>
    <config xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
        <module name="Vendor_Rules" setup_version="1.0.0">
        </module>
    </config>

These files declare the Rules module in the Vendor namespace.

Next, let’s add the files responsible for the main configuration setup:

app/code/Vendor/Rules/etc/acl.xml

    <?xml version="1.0"?>
    <config xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
        <acl>
            <resources>
                <resource id="Magento_Backend::admin">
                    <resource id="Magento_Backend::marketing">
                        <resource id="Magento_CatalogRule::promo">
                            <resource id="Vendor_Rules::rules" sortOrder="40" title="Example Rules" />
                        </resource>
                    </resource>
                </resource>
            </resources>
        </acl>
    </config>

Note, we’ve added acl in order to disable the module for certain administrators.

app/code/Vendor/Rules/etc/adminhtml/routes.xml

<?xml version="1.0"?>
<config xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="vendor_rules" frontName="vendor_rules">
            <module name="Vendor_Rules" before="Magento_Backend" />
        </route>
    </router>
</config>

Also, we’ve added an admin route with the `vendor_rules` name and id. Later, we can use it to get an access to the modules’ interface in the admin panel.

app/code/Vendor/Rules/etc/adminhtml/menu.xml

<?xml version="1.0"?>
<config xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
    <menu>
        <add id="Vendor_Rules::vendor_rules"
             title="Example Rules"
             module="Vendor_Rules"
             parent="Magento_CatalogRule::promo"
             sortOrder="40"
             action="vendor_rules/example_rule/index"
             dependsOnModule="Magento_Sales"
             resource="Vendor_Rules::rule"
        />
    </menu>
</config>

By doing that, we’ve added a menu section for our module in the Marketing tab.

Now, let’s add the tables to the module database. These fields are default, and you can expand them whenever needed.

Let’s name the table according to the module name – `vendor_rules`.

app/code/Vendor/Rules/Setup/InstallSchema.php

<?php

namespace Vendor\Rules\Setup;

use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

class InstallSchema implements InstallSchemaInterface
{

    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;
        $installer->startSetup();

        $table = $installer->getConnection()->newTable(
            $installer->getTable('vendor_rules')
        )->addColumn(
            'rule_id',
            Table::TYPE_INTEGER,
            null,
            ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
            'Rule Id'
        )->addColumn(
            'name',
            Table::TYPE_TEXT,
            255,
            [],
            'Name'
        )->addColumn(
            'description',
            Table::TYPE_TEXT,
            '64k',
            [],
            'Description'
        )->addColumn(
            'from_date',
            Table::TYPE_DATE,
            null,
            ['nullable' => true, 'default' => null],
            'From'
        )->addColumn(
            'to_date',
            Table::TYPE_DATE,
            null,
            ['nullable' => true, 'default' => null],
            'To'
        )->addColumn(
            'is_active',
            Table::TYPE_SMALLINT,
            null,
            ['nullable' => false, 'default' => '0'],
            'Is Active'
        )->addColumn(
            'conditions_serialized',
            Table::TYPE_TEXT,
            '2M',
            [],
            'Conditions Serialized'
        )->addColumn(
            'sort_order',
            Table::TYPE_INTEGER,
            null,
            ['unsigned' => true, 'nullable' => false, 'default' => '0'],
            'Sort Order (Priority)'
        )->addIndex(
            $installer->getIdxName('vendor_rules', ['sort_order', 'is_active', 'to_date', 'from_date']),
            ['sort_order', 'is_active', 'to_date', 'from_date']
        )->setComment(
            'Own Rules'
        );

        $installer->getConnection()->createTable($table);
        $installer->endSetup();
    }
}

Our conditions will be stored in the `conditions_serialized` field. The rest of the data just describes the model (example).

Also, don’t forget to add the module uninstall file! That is done to delete the table after installing it via `Composer` and/or deleting.

app/code/Vendor/Rules/Setup/Uninstall.php

<?php

namespace Vendor\Rules\Setup;

use Magento\Framework\Setup\UninstallInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\Setup\ModuleContextInterface;

class Uninstall implements UninstallInterface
{
    /**
     * Module uninstall code
     *
     * @param SchemaSetupInterface $setup
     * @param ModuleContextInterface $context
     * @return void
     */
    public function uninstall(
        SchemaSetupInterface $setup,
        ModuleContextInterface $context
    ) {
        $setup->startSetup();

        $connection = $setup->getConnection();

        $connection->dropTable($connection->getTableName('vendor_rules'));

        $setup->endSetup();
    }
}

Now, we have a table with the model description and we need to complete the model itself, and include into it the appropriate resource model and collection.

The model will be called ‘Rule‘:

app/code/Vendor/Rules/Model/Rule.php

<?php

namespace Vendor\Rules\Model;

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

/**
 * Class Rule
 * @package Vendor\Rules\Model
 *
 * @method int|null getRuleId()
 * @method Rule setRuleId(int $id)
 */
class Rule extends AbstractModel
{
    /**
     * Prefix of model events names
     *
     * @var string
     */
    protected $_eventPrefix = 'vendor_rules';

    /**
     * Parameter name in event
     *
     * In observe method you can use $observer->getEvent()->getRule() in this case
     *
     * @var string
     */
    protected $_eventObject = 'rule';

    /** @var \Magento\SalesRule\Model\Rule\Condition\CombineFactory */
    protected $condCombineFactory;

    /** @var \Magento\SalesRule\Model\Rule\Condition\Product\CombineFactory */
    protected $condProdCombineF;

    /**
     * Store already validated addresses and validation results
     *
     * @var array
     */
    protected $validatedAddresses = [];

    /**
     * @param \Magento\Framework\Model\Context $context
     * @param \Magento\Framework\Registry $registry
     * @param \Magento\Framework\Data\FormFactory $formFactory
     * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
     * @param \Magento\SalesRule\Model\Rule\Condition\CombineFactory $condCombineFactory
     * @param \Magento\SalesRule\Model\Rule\Condition\Product\CombineFactory $condProdCombineF
     * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
     * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
     * @param array $data
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
     */
    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);
    }

    /**
     * Set resource model and Id field name
     *
     * @return void
     */
    protected function _construct()
    {
        parent::_construct();
        $this->_init('Vendor\Rules\Model\ResourceModel\Rule');
        $this->setIdFieldName('rule_id');
    }

    /**
     * Get rule condition combine model instance
     *
     * @return \Magento\SalesRule\Model\Rule\Condition\Combine
     */
    public function getConditionsInstance()
    {
        return $this->condCombineFactory->create();
    }

    /**
     * Get rule condition product combine model instance
     *
     * @return \Magento\SalesRule\Model\Rule\Condition\Product\Combine
     */
    public function getActionsInstance()
    {
        return $this->condProdCombineF->create();
    }

    /**
     * Check cached validation result for specific address
     *
     * @param Address $address
     * @return bool
     */
    public function hasIsValidForAddress($address)
    {
        $addressId = $this->_getAddressId($address);
        return isset($this->validatedAddresses[$addressId]) ? true : false;
    }

    /**
     * Set validation result for specific address to results cache
     *
     * @param Address $address
     * @param bool $validationResult
     * @return $this
     */
    public function setIsValidForAddress($address, $validationResult)
    {
        $addressId = $this->_getAddressId($address);
        $this->validatedAddresses[$addressId] = $validationResult;
        return $this;
    }

    /**
     * Get cached validation result for specific address
     *
     * @param Address $address
     * @return bool
     * @SuppressWarnings(PHPMD.BooleanGetMethodName)
     */
    public function getIsValidForAddress($address)
    {
        $addressId = $this->_getAddressId($address);
        return isset($this->validatedAddresses[$addressId]) ? $this->validatedAddresses[$addressId] : false;
    }

    /**
     * Return id for address
     *
     * @param Address $address
     * @return string
     */
    private function _getAddressId($address)
    {
        if ($address instanceof Address) {
            return $address->getId();
        }
        return $address;
    }
}

As you can see, our model inherits from the `Magento\Rule\Model\AbstractModel` model that already has all the required methods.

Right in the Constructor, we’ll add condition factories that allow us to work with them and create multiple methods. This should give us the understanding of how the model works.

Note that 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 own classes and/or rewrite them completely or inherit from the base available classes. In can be useful when you want to add a special condition that is not included in the default conditions. For example, **Subtotal With Discount**.

Now, it’s time to add a resource model from the Constructor:

app/code/Vendor/Rules/Model/ResourceModel/Rule.php

<?php

namespace Vendor\Rules\Model\ResourceModel;

class Rule extends \Magento\Rule\Model\ResourceModel\AbstractResource
{

    /**
     * Initialize main table and table id field
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init('vendor_rules', 'rule_id');
    }
}

And a collection:

app/code/Vendor/Rules/Model/ResourceModel/Rule/Collection.php

<?php

namespace Vendor\Rules\Model\ResourceModel\Rule;

class Collection extends \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection
{
    /**
     * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface
     */
    protected $date;

    /**
     * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory
     * @param \Psr\Log\LoggerInterface $logger
     * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
     * @param \Magento\Framework\Event\ManagerInterface $eventManager
     * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $date
     * @param mixed $connection
     * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource
     */
    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;
    }

    /**
     * Set resource model and determine field mapping
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init('Vendor\Rules\Model\Rule', 'Vendor\Rules\Model\ResourceModel\Rule');
    }

    /**
     * Filter collection by specified date.
     * Filter collection to only active rules.
     *
     * @param string|null $now
     * @use $this->addStoreGroupDateFilter()
     * @return $this
     */
    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;
    }

    /**
     * From date or to date filter
     *
     * @param $now
     * @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;
    }
}

There are also some collection filter methods that can help you cut off the inactive or mismatching the current date rules while validating. For example, these will help us decrease server overloads: `setValidationFilter` и `addDateFilter`.

Now, we’ve finished with the structure declaration and are halfway through with this task!

Next, let’s switch to the interface in the admin panel. We need the Controller with a set of actions (such as Save, Add, Edit, Grid Display, Conditions reload) and a layout with blocks.

Let’s start with the Controller itself. First, declare the common Controller:

app/code/Vendor/Rules/Controller/Adminhtml/Example/Rule.php

<?php

namespace Vendor\Rules\Controller\Adminhtml\Example;

abstract class Rule extends \Magento\Backend\App\Action
{
    /**
     * Core registry
     *
     * @var \Magento\Framework\Registry
     */
    protected $coreRegistry = null;

    /**
     * @var \Magento\Framework\App\Response\Http\FileFactory
     */
    protected $fileFactory;

    /**
     * @var \Magento\Framework\Stdlib\DateTime\Filter\Date
     */
    protected $dateFilter;

    /**
     * @var \Vendor\Rules\Model\RuleFactory
     */
    protected $ruleFactory;

    /**
     * @var \Psr\Log\LoggerInterface
     */
    protected $logger;

    /**
     * @param \Magento\Backend\App\Action\Context $context
     * @param \Magento\Framework\Registry $coreRegistry
     * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory
     * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter
     * @param \Vendor\Rules\Model\RuleFactory $ruleFactory
     * @param \Psr\Log\LoggerInterface $logger
     */
    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \Magento\Framework\Registry $coreRegistry,
        \Magento\Framework\App\Response\Http\FileFactory $fileFactory,
        \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter,
        \Vendor\Rules\Model\RuleFactory $ruleFactory,
        \Psr\Log\LoggerInterface $logger
    ) {
        parent::__construct($context);
        $this->coreRegistry = $coreRegistry;
        $this->fileFactory = $fileFactory;
        $this->dateFilter = $dateFilter;
        $this->ruleFactory = $ruleFactory;
        $this->logger = $logger;
    }

    /**
     * Initiate rule
     *
     * @return void
     */
    protected function _initRule()
    {
        $rule = $this->ruleFactory->create();
        $this->coreRegistry->register(
            'current_rule',
            $rule
        );
        $id = (int)$this->getRequest()->getParam('id');

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

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

    /**
     * Initiate action
     *
     * @return Rule
     */
    protected function _initAction()
    {
        $this->_view->loadLayout();
        $this->_setActiveMenu('Vendor_Rules::vendor_rules')
            ->_addBreadcrumb(__('Example Rules'), __('Example Rules'));
        return $this;
    }

    /**
     * Returns result of current user permission check on resource and privilege
     *
     * @return bool
     */
    protected function _isAllowed()
    {
        return $this->_authorization->isAllowed('Vendor_Rules::rules');
    }
}

Here, we need to call out our models’ factory in the Constructor. That is done to make them (and some auxiliary classes, like a register and a logger) publicly available.

The `_initRule` method is responsible for the current rule initialization or creating of a new and empty one with the ability of adding it to the register.
The `_initAction()` method loads a layout and makes the modules’ menu available for actions (also, it adds breadcumbs).
The `_isAllowed()` method checks if the current admin has an access to the Controller.

At the next step, we are going to add the default actions:

  • Deleting:

app/code/Vendor/Rules/Controller/Adminhtml/Example/Rule/Delete.php

<?php

namespace Vendor\Rules\Controller\Adminhtml\Example\Rule;

class Delete extends \Vendor\Rules\Controller\Adminhtml\Example\Rule
{
    /**
     * Delete rule action
     *
     * @return void
     */
    public function execute()
    {
        $id = $this->getRequest()->getParam('id');
        if ($id) {
            try {
                /** @var \Vendor\Rules\Model\Rule $model */
                $model = $this->ruleFactory->create();
                $model->load($id);
                $model->delete();
                $this->messageManager->addSuccessMessage(__('You deleted the rule.'));
                $this->_redirect('vendor_rules/*/');
                return;
            } catch (\Magento\Framework\Exception\LocalizedException $e) {
                $this->messageManager->addErrorMessage($e->getMessage());
            } catch (\Exception $e) {
                $this->messageManager->addErrorMessage(
                    __('We can\'t delete the rule right now. Please review the log and try again.')
                );
                $this->logger->critical($e);
                $this->_redirect('vendor_rules/*/edit', ['id' => $this->getRequest()->getParam('id')]);
                return;
            }
        }
        $this->messageManager->addErrorMessage(__('We can\'t find a rule to delete.'));
        $this->_redirect('vendor_rules/*/');
    }
}
  • Adding (NOTE! Do not use the `New` word because it is keyword in the PHP):


app/code/Vendor/Rules/Controller/Adminhtml/Example/Rule/NewAction.php

<?php

namespace Vendor\Rules\Controller\Adminhtml\Example\Rule;

class NewAction extends \Vendor\Rules\Controller\Adminhtml\Example\Rule
{
    /**
     * New action
     *
     * @return void
     */
    public function execute()
    {
        $this->_forward('edit');
    }
}
  • Editing

app/code/Vendor/Rules/Controller/Adminhtml/Example/Rule/Edit.php

<?php

namespace Vendor\Rules\Controller\Adminhtml\Example\Rule;

class Edit extends \Vendor\Rules\Controller\Adminhtml\Example\Rule
{
    /**
     * Rule edit action
     *
     * @return void
     */
    public function execute()
    {
        $id = $this->getRequest()->getParam('id');
        /** @var \Vendor\Rules\Model\Rule $model */
        $model = $this->ruleFactory->create();

        if ($id) {
            $model->load($id);
            if (!$model->getRuleId()) {
                $this->messageManager->addErrorMessage(__('This rule no longer exists.'));
                $this->_redirect('vendor_rules/*');
                return;
            }
        }

        // set entered data if was error when we do save
        $data = $this->_session->getPageData(true);
        if (!empty($data)) {
            $model->addData($data);
        }

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

        $this->coreRegistry->register('current_rule', $model);

        $this->_initAction();
        $this->_view->getLayout()
            ->getBlock('example_rule_edit')
            ->setData('action', $this->getUrl('vendor_rules/*/save'));

        $this->_addBreadcrumb($id ? __('Edit Rule') : __('New Rule'), $id ? __('Edit Rule') : __('New Rule'));

        $this->_view->getPage()->getConfig()->getTitle()->prepend(
            $model->getRuleId() ? $model->getName() : __('New Rule')
        );
        $this->_view->renderLayout();
    }
}
  • Saving

app/code/Vendor/Rules/Controller/Adminhtml/Example/Rule/Save.php

<?php

namespace Vendor\Rules\Controller\Adminhtml\Example\Rule;

class Save extends \Vendor\Rules\Controller\Adminhtml\Example\Rule
{

    /**
     * @param \Magento\Backend\App\Action\Context $context
     * @param \Magento\Framework\Registry $coreRegistry
     * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory
     * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter
     * @param \Vendor\Rules\Model\RuleFactory $ruleFactory
     * @param \Psr\Log\LoggerInterface $logger
     */
    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \Magento\Framework\Registry $coreRegistry,
        \Magento\Framework\App\Response\Http\FileFactory $fileFactory,
        \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter,
        \Vendor\Rules\Model\RuleFactory $ruleFactory,
        \Psr\Log\LoggerInterface $logger
    ) {

        parent::__construct($context, $coreRegistry, $fileFactory, $dateFilter, $ruleFactory, $logger);
    }

    /**
     * Rule save action
     *
     * @return void
     */
    public function execute()
    {
        if (!$this->getRequest()->getPostValue()) {
            $this->_redirect('vendor_rules/*/');
        }

        try {
            /** @var $model \Vendor\Rules\Model\Rule */
            $model = $this->ruleFactory->create();
            $this->_eventManager->dispatch(
                'adminhtml_controller_vendor_rules_prepare_save',
                ['request' => $this->getRequest()]
            );
            $data = $this->getRequest()->getPostValue();
            $inputFilter = new \Zend_Filter_Input(
                ['from_date' => $this->dateFilter, 'to_date' => $this->dateFilter],
                [],
                $data
            );
            $data = $inputFilter->getUnescaped();
            $id = $this->getRequest()->getParam('rule_id');
            if ($id) {
                $model->load($id);
            }

            $validateResult = $model->validateData(new \Magento\Framework\DataObject($data));
            if ($validateResult !== true) {
                foreach ($validateResult as $errorMessage) {
                    $this->messageManager->addErrorMessage($errorMessage);
                }
                $this->_session->setPageData($data);
                $this->_redirect('vendor_rules/*/edit', ['id' => $model->getId()]);
                return;
            }

            $data = $this->prepareData($data);
            $model->loadPost($data);

            $this->_session->setPageData($model->getData());

            $model->save();
            $this->messageManager->addSuccessMessage(__('You saved the rule.'));
            $this->_session->setPageData(false);
            if ($this->getRequest()->getParam('back')) {
                $this->_redirect('vendor_rules/*/edit', ['id' => $model->getId()]);
                return;
            }
            $this->_redirect('vendor_rules/*/');
            return;
        } catch (\Magento\Framework\Exception\LocalizedException $e) {
            $this->messageManager->addErrorMessage($e->getMessage());
            $id = (int)$this->getRequest()->getParam('rule_id');
            if (!empty($id)) {
                $this->_redirect('vendor_rules/*/edit', ['id' => $id]);
            } else {
                $this->_redirect('vendor_rules/*/new');
            }
            return;
        } catch (\Exception $e) {
            $this->messageManager->addErrorMessage(
                __('Something went wrong while saving the rule data. Please review the error log.')
            );
            $this->logger->critical($e);
            $data = !empty($data) ? $data : [];
            $this->_session->setPageData($data);
            $this->_redirect('vendor_rules/*/edit', ['id' => $this->getRequest()->getParam('rule_id')]);
            return;
        }
    }

    /**
     * Prepares specific data
     *
     * @param array $data
     * @return array
     */
    protected function prepareData($data)
    {

        if (isset($data['rule']['conditions'])) {
            $data['conditions'] = $data['rule']['conditions'];
        }

        unset($data['rule']);

        return $data;
    }
}

In this case, you should pay attention to the field where the conditions come. The `prepareData` method allows us to correctly tranfer conditons to the model before saving. This is how to add new conditions:

app/code/Vendor/Rules/Controller/Adminhtml/Example/Rule/NewConditionHtml.php

<?php

namespace Vendor\Rules\Controller\Adminhtml\Example\Rule;

class NewConditionHtml extends \Vendor\Rules\Controller\Adminhtml\Example\Rule
{
    /**
     * New condition html action
     *
     * @return void
     */
    public function execute()
    {
        $id = $this->getRequest()->getParam('id');
        $typeArr = explode('|', str_replace('-', '/', $this->getRequest()->getParam('type')));
        $type = $typeArr[0];

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

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

This class is responsible for loading the conditions that have been chosen in the interface (all the conditions can’t be loaded at once).

  • And the last one is Grid:

app/code/Vendor/Rules/Controller/Adminhtml/Example/Rule/Index.php

<?php

namespace Vendor\Rules\Controller\Adminhtml\Example\Rule;

class Index extends \Vendor\Rules\Controller\Adminhtml\Example\Rule
{
    /**
     * Index action
     *
     * @return void
     */
    public function execute()
    {
        $this->_initAction()->_addBreadcrumb(__('Example Rules'), __('Example Rules'));
        $this->_view->getPage()->getConfig()->getTitle()->prepend(__('Example Rules'));
        $this->_view->renderLayout('root');
    }
}

Next, we need to create all the required blocks and layout. First, let’s add a container for the Grid:

app/code/Vendor/Rules/Block/Adminhtml/Example/Rule.php

<?php

namespace Vendor\Rules\Block\Adminhtml\Example;

class Rule extends \Magento\Backend\Block\Widget\Grid\Container
{
    /**
     * Constructor
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_controller = 'example_rule';
        $this->_headerText = __('Example Rules');
        $this->_addButtonLabel = __('Add New Rule');
        parent::_construct();
    }
}

Note that when doing this, you’ll be able to add only the Controller’s name (it will be used when forming Grid and Lable). In case you need to add something else to the Grid page, you can do that via your block-container or directly via the Layout.

This is the Grid’s Layout:

app/code/Vendor/Rules/view/adminhtml/layout/vendor_rules_example_rule_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
    <referenceContainer name="content">
        <block class="Vendor\Rules\Block\Adminhtml\Example\Rule" name="adminhtml.block.example.rule.grid.container">
            <block class="Magento\Backend\Block\Widget\Grid" name="adminhtml.block.example.rule.grid" as="grid">
                <arguments>
                    <argument name="id" xsi:type="string">example_rule_grid</argument>
                    <argument name="dataSource" xsi:type="object">Vendor\Rules\Model\ResourceModel\Rule\Collection</argument>
                    <argument name="default_sort" xsi:type="string">sort_order</argument>
                    <argument name="default_dir" xsi:type="string">ASC</argument>
                    <argument name="save_parameters_in_session" xsi:type="string">1</argument>
                </arguments>
                <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" as="grid.columnSet" name="adminhtml.example.rule.grid.columnSet">
                    <arguments>
                        <argument name="rowUrl" xsi:type="array">
                            <item name="path" xsi:type="string">vendor_rules/*/edit</item>
                            <item name="extraParamsTemplate" xsi:type="array">
                                <item name="id" xsi:type="string">getRuleId</item>
                            </item>
                        </argument>
                    </arguments>
                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="rule_id">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">ID</argument>
                            <argument name="index" xsi:type="string">rule_id</argument>
                            <argument name="column_css_class" xsi:type="string">col-id</argument>
                            <argument name="header_css_class" xsi:type="string">col-id</argument>
                        </arguments>
                    </block>
                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="name">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">Rule</argument>
                            <argument name="index" xsi:type="string">name</argument>
                        </arguments>
                    </block>
                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="is_active">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">Status</argument>
                            <argument name="index" xsi:type="string">is_active</argument>
                            <argument name="type" xsi:type="string">options</argument>
                            <argument name="options" xsi:type="array">
                                <item name="active" xsi:type="array">
                                    <item name="value" xsi:type="string">1</item>
                                    <item name="label" xsi:type="string" translate="true">Active</item>
                                </item>
                                <item name="inactive" xsi:type="array">
                                    <item name="value" xsi:type="string">0</item>
                                    <item name="label" xsi:type="string" translate="true">Inactive</item>
                                </item>
                            </argument>
                        </arguments>
                    </block>
                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="sort_order">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">Priority</argument>
                            <argument name="index" xsi:type="string">sort_order</argument>
                        </arguments>
                    </block>
                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="from_date">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">Start</argument>
                            <argument name="type" xsi:type="string">date</argument>
                            <argument name="index" xsi:type="string">from_date</argument>
                            <argument name="column_css_class" xsi:type="string">col-date</argument>
                            <argument name="header_css_class" xsi:type="string">col-date</argument>
                        </arguments>
                    </block>
                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="to_date">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">End</argument>
                            <argument name="type" xsi:type="string">date</argument>
                            <argument name="default" xsi:type="string">--</argument>
                            <argument name="index" xsi:type="string">to_date</argument>
                            <argument name="column_css_class" xsi:type="string">col-date</argument>
                            <argument name="header_css_class" xsi:type="string">col-date</argument>
                        </arguments>
                    </block>
                </block>
            </block>
        </block>
    </referenceContainer>
    </body>
</page>

Everything is placed inside of the page content.

`<block class=”Vendor\Rules\Block\Adminhtml\Example\Rule” …>…</block>` is our main container.

Note that the container’s block and grid names are formed from the controller’s name that we mentioned in the Container. If you do this incorrectly, the Grid may fail to display.

The Grid block itself: `<block class=”Magento\Backend\Block\Widget\Grid” name=”adminhtml.block.example.rule.grid” as=”grid”>`. Here, we need to fill in a number of arguments:

<arguments>
    <argument name="id" xsi:type="string">example_rule_grid</argument>
    <argument name="dataSource" xsi:type="object">Vendor\Rules\Model\ResourceModel\Rule\Collection</argument>
    <argument name="default_sort" xsi:type="string">sort_order</argument>
    <argument name="default_dir" xsi:type="string">ASC</argument>
    <argument name="save_parameters_in_session" xsi:type="string">1</argument>
</arguments>

We are using our `Vendor\Rules\Model\ResourceModel\Rule\Collection` collection as a source and define the sorting order.

The `<block class=”Magento\Backend\Block\Widget\Grid\ColumnSet” as=”grid.columnSet” name=”adminhtml.example.rule.grid.columnSet“>…</block>` block contains a number of columns. If you want to add your own columns, they should be added to the database and the Grid (if you want to see them):

Now, let’s get down to creating and editing new rules. Let’s create the main container block for editing:

app/code/Vendor/Rules/Block/Adminhtml/Example/Rule/Edit.php

<?php

namespace Vendor\Rules\Block\Adminhtml\Example\Rule;

class Edit extends \Magento\Backend\Block\Widget\Form\Container
{
    /**
     * Core registry
     *
     * @var \Magento\Framework\Registry
     */
    protected $coreRegistry = null;

    /**
     * @param \Magento\Backend\Block\Widget\Context $context
     * @param \Magento\Framework\Registry $registry
     * @param array $data
     */
    public function __construct(
        \Magento\Backend\Block\Widget\Context $context,
        \Magento\Framework\Registry $registry,
        array $data = []
    ) {
        $this->coreRegistry = $registry;
        parent::__construct($context, $data);
    }

    /**
     * Initialize form
     * Add standard buttons
     * Add "Save and Continue" button
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_objectId = 'id';
        $this->_controller = 'adminhtml_example_rule';
        $this->_blockGroup = 'Vendor_Rules';

        parent::_construct();

        $this->buttonList->add(
            'save_and_continue_edit',
            [
                'class' => 'save',
                'label' => __('Save and Continue Edit'),
                'data_attribute' => [
                    'mage-init' => ['button' => ['event' => 'saveAndContinueEdit', 'target' => '#edit_form']],
                ]
            ],
            10
        );
    }

    /**
     * Getter for form header text
     *
     * @return \Magento\Framework\Phrase
     */
    public function getHeaderText()
    {
        $rule = $this->coreRegistry->registry('current_rule');
        if ($rule->getRuleId()) {
            return __("Edit Rule '%1'", $this->escapeHtml($rule->getName()));
        } else {
            return __('New Rule');
        }
    }

}

When done, we should add the controllers title and the `save` and `edit current model` buttons in the Constructor. Also, here you should add the main text for of the block.

This is a form itself:

app/code/Vendor/Rules/Block/Adminhtml/Example/Rule/Edit/Form.php

<?php

namespace Vendor\Rules\Block\Adminhtml\Example\Rule\Edit;

class Form extends \Magento\Backend\Block\Widget\Form\Generic
{
    /**
     * Constructor
     *
     * @return void
     */
    protected function _construct()
    {
        parent::_construct();
        $this->setId('example_rule_form');
        $this->setTitle(__('Rule Information'));
    }

    /**
     * Prepare form before rendering HTML
     *
     * @return \Magento\Backend\Block\Widget\Form\Generic
     */
    protected function _prepareForm()
    {
        /** @var \Magento\Framework\Data\Form $form */
        $form = $this->_formFactory->create(
            [
                'data' => [
                    'id' => 'edit_form',
                    'action' => $this->getUrl('vendor_rules/example_rule/save'),
                    'method' => 'post',
                ],
            ]
        );
        $form->setUseContainer(true);
        $this->setForm($form);
        return parent::_prepareForm();
    }
}

and tabs:

app/code/Vendor/Rules/Block/Adminhtml/Example/Rule/Edit/Tabs.php

<?php

namespace Vendor\Rules\Block\Adminhtml\Example\Rule\Edit;

class Tabs extends \Magento\Backend\Block\Widget\Tabs
{
    /**
     * Constructor
     *
     * @return void
     */
    protected function _construct()
    {
        parent::_construct();
        $this->setId('rules_edit_tabs');
        $this->setDestElementId('edit_form');
        $this->setTitle(__('Rules'));
    }
}

We’ll have the two tabs: Ceneral model’s information and Conditions.

app/code/Vendor/Rules/Block/Adminhtml/Example/Rule/Edit/Tab/Main.php

<?php

namespace Vendor\Rules\Block\Adminhtml\Example\Rule\Edit\Tab;

use Magento\Backend\Block\Template\Context;
use Magento\Backend\Block\Widget\Form\Generic;
use Magento\Backend\Block\Widget\Tab\TabInterface;
use Magento\Framework\Data\FormFactory;
use Magento\Framework\Registry;

class Main extends Generic implements TabInterface
{

    /**
     * Constructor
     *
     * @param Context $context
     * @param Registry $registry
     * @param FormFactory $formFactory
     * @param array $data
     */
    public function __construct(
        Context $context,
        Registry $registry,
        FormFactory $formFactory,
        array $data = []
    ) {
        parent::__construct($context, $registry, $formFactory, $data);
    }

    /**
     * {@inheritdoc}
     */
    public function getTabLabel()
    {
        return __('Rule Information');
    }

    /**
     * {@inheritdoc}
     */
    public function getTabTitle()
    {
        return __('Rule Information');
    }

    /**
     * {@inheritdoc}
     */
    public function canShowTab()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function isHidden()
    {
        return false;
    }

    /**
     * Prepare form before rendering HTML
     *
     * @return Generic
     */
    protected function _prepareForm()
    {
        $model = $this->_coreRegistry->registry('current_rule');

        /** @var \Magento\Framework\Data\Form $form */
        $form = $this->_formFactory->create();
        $form->setHtmlIdPrefix('rule_');

        $fieldset = $form->addFieldset('base_fieldset', ['legend' => __('General Information')]);

        if ($model->getId()) {
            $fieldset->addField('rule_id', 'hidden', ['name' => 'rule_id']);
        }

        $fieldset->addField(
            'name',
            'text',
            ['name' => 'name', 'label' => __('Rule Name'), 'title' => __('Rule Name'), 'required' => true]
        );

        $fieldset->addField(
            'description',
            'textarea',
            [
                'name' => 'description',
                'label' => __('Description'),
                'title' => __('Description'),
                'style' => 'height: 100px;'
            ]
        );

        $fieldset->addField(
            'is_active',
            'select',
            [
                'label' => __('Status'),
                'title' => __('Status'),
                'name' => 'is_active',
                'required' => true,
                'options' => ['1' => __('Active'), '0' => __('Inactive')]
            ]
        );

        if (!$model->getId()) {
            $model->setData('is_active', '1');
        }

        $fieldset->addField('sort_order', 'text', ['name' => 'sort_order', 'label' => __('Priority')]);

        $dateFormat = $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT);
        $fieldset->addField(
            'from_date',
            'date',
            [
                'name' => 'from_date',
                'label' => __('From'),
                'title' => __('From'),
                'input_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT,
                'date_format' => $dateFormat
            ]
        );
        $fieldset->addField(
            'to_date',
            'date',
            [
                'name' => 'to_date',
                'label' => __('To'),
                'title' => __('To'),
                'input_format' => \Magento\Framework\Stdlib\DateTime::DATE_INTERNAL_FORMAT,
                'date_format' => $dateFormat
            ]
        );

        $form->setValues($model->getData());

        if ($model->isReadonly()) {
            foreach ($fieldset->getElements() as $element) {
                $element->setReadonly(true, true);
            }
        }

        $this->setForm($form);

        $this->_eventManager->dispatch('adminhtml_example_rule_edit_tab_main_prepare_form', ['form' => $form]);

        return parent::_prepareForm();
    }
}

Conditions:

app/code/Vendor/Rules/Block/Adminhtml/Example/Rule/Edit/Tab/Conditions.php

<?php

namespace Vendor\Rules\Block\Adminhtml\Example\Rule\Edit\Tab;

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

class Conditions extends Generic implements TabInterface
{
    /**
     * Core registry
     *
     * @var \Magento\Backend\Block\Widget\Form\Renderer\Fieldset
     */
    protected $rendererFieldset;

    /**
     * @var \Magento\Rule\Block\Conditions
     */
    protected $conditions;

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

    /**
     * {@inheritdoc}
     */
    public function getTabLabel()
    {
        return __('Conditions');
    }

    /**
     * {@inheritdoc}
     */
    public function getTabTitle()
    {
        return __('Conditions');
    }

    /**
     * {@inheritdoc}
     */
    public function canShowTab()
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function isHidden()
    {
        return false;
    }

    /**
     * Prepare form before rendering HTML
     *
     * @return Generic
     */
    protected function _prepareForm()
    {
        $model = $this->_coreRegistry->registry('current_rule');

        /** @var \Magento\Framework\Data\Form $form */
        $form = $this->_formFactory->create();
        $form->setHtmlIdPrefix('rule_');

        $renderer = $this->rendererFieldset->setTemplate(
            'Magento_CatalogRule::promo/fieldset.phtml'
        )->setNewChildUrl(
            $this->getUrl('vendor_rules/example_rule/newConditionHtml/form/rule_conditions_fieldset')
        );

        $fieldset = $form->addFieldset(
            'conditions_fieldset',
            [
                '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')]
        )->setRule(
            $model
        )->setRenderer(
            $this->conditions
        );

        $form->setValues($model->getData());
        $this->setForm($form);

        return parent::_prepareForm();
    }
}

Also, let me draw your attention to the template that are used to render a fieldset: `Magento_CatalogRule::promo/fieldset.phtml` and an address to recieve a new descendant: `vendor_rules/example_rule/newConditionHtml/form/rule_conditions_fieldset` that links to the `app/code/Vendor/Rules/Controller/Adminhtml/Example/Rule/NewConditionHtml.php` action we created earlier. In other words, all new descendants will derive from it.

If you can’t add conditions, start the debug from this fieldset and condition.

The correct answer for choosing another attribute in the conditions looks like this:

5

You can understand that the controller works incorrectly if the window is not responding when choosing a new attribute in the conditions:

5

In this case you should reload the page first and then open your browser’s console to find a mistake in respone for the ajax request.

This is the layout for the editing form:

app/code/Vendor/Rules/view/adminhtml/layout/vendor_rules_example_rule_edit.xml

<?xml version="1.0"?>
<page xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" layout="admin-2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
    <referenceContainer name="left">
        <block class="Vendor\Rules\Block\Adminhtml\Example\Rule\Edit\Tabs" name="example_rule_edit_tabs">
            <block class="Vendor\Rules\Block\Adminhtml\Example\Rule\Edit\Tab\Main" name="example_rule_edit_tab_main"/>
            <block class="Vendor\Rules\Block\Adminhtml\Example\Rule\Edit\Tab\Conditions" name="example_rule_edit_tab_conditions"/>
            <action method="addTab">
                <argument name="name" xsi:type="string">main_section</argument>
                <argument name="block" xsi:type="string">example_rule_edit_tab_main</argument>
            </action>
            <action method="addTab">
                <argument name="name" xsi:type="string">conditions_section</argument>
                <argument name="block" xsi:type="string">example_rule_edit_tab_conditions</argument>
            </action>
        </block>
    </referenceContainer>
    <referenceContainer name="content">
        <block class="Vendor\Rules\Block\Adminhtml\Example\Rule\Edit" name="example_rule_edit"/>
    </referenceContainer>
    </body>
</page>

The result should be the following:

  • Main fields

1

  • Conditions

2

3

TO BE CONTINUED…

See the second part of the article for more insights on the topic.

I am a huge coffee fan. If I’m not drinking it, I’m likely to be busy with getting MageWorx projects done. Fond of reading sci-fi books (especially those about dwarfs, ogres and the post-apocalyptical world). My biggest dream is to find a huge chest of gold and buy my own uninhabited island. Happy husband. Proud father. Ah... and also I'm a certified Magento developer. ;)

9 COMMENTS

  1. Hi Sergey,

    It’s a very nice tutorial for the developers.

    Thank you very much for that!

    It would be great if you let me know how we can apply our created condition in the frontend?

    Thank you very much for your answer!

    • Hello Ali, we are going to download the sources to our GitHub account soon. Will send you the link as soon as it’s ready 🙂

LEAVE A REPLY

Please enter your comment!
Please enter your name here