Dynamic fields in Magento 2 system configuration

System configuration is a very useful feature of the Magento platform. At the same time, it’s easy to create new configurations for your own custom solutions. We’ve previously discussed on how to add new system configurations with different fields. Today I would like to describe one more type of fields, which can be used depending on your needs. This is a dynamic field block. At times, managing complex data through system configurations is necessary, especially when the number of records to be added isn’t predetermined. In this case it’s impossible to create the exact fieldset in a system configuration tab. Thus, dynamic fields prove to be invaluable.

Magento 2 allows you to create custom fields in system configurations. There are two components needed:

  • Frontend model – the element renderer class responsible for the field view. This class must implement the Magento\Framework\Data\Form\Element\Renderer\RendererInterface interface. The custom renderer class needs to be specified in the frontend_model node of the field definition in system.xml file.
  • Backend model – the config data model class provides the backend logic for system configuration value. In most cases, it is used for the field data preparation before saving and after loading. The configuration field backend model must implement the Magento\Framework\App\Config\ValueInterface interface.

Let’s delve into creating a straightforward custom field. There is already an available frontend model for dynamic fields block. We just need to extend the Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray class and override the _prepareToRender method in order to add columns to dynamic fields block, change “add button” label etc.

<?php
/**
 * @author Atwix Team
 * @copyright Copyright (c) 2018 Atwix (https://www.atwix.com/)
 * @package Atwix_DynamicFields
 */

namespace Atwix\DynamicFields\Block\Adminhtml\Form\Field;

use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray;

/**
 * Class AdditionalEmail
 */
class AdditionalEmail extends AbstractFieldArray
{
    /**
     * {@inheritdoc}
     */
    protected function _prepareToRender()
    {
        $this->addColumn('firstname', ['label' => __('First Name'), 'class' => 'required-entry']);
        $this->addColumn('lastname', ['label' => __('Last Name')]);
        $this->addColumn('email',['label' => __('Email'), 'size' => '50px', 'class' => 'required-entry validate-email']);
        $this->_addAfter = false;
        $this->_addButtonLabel = __('Add Email');
    }
}

The data is saved in one record of the core_config_data table in the database for each system configuration field. That’s why we need to serialize the data before saving and unserialize this value back after loading. The backend model extends Magento\Framework\App\Config\Value base config data model class and overrides two methods: beforeSave and _afterLoad. The field value will be serialized to JSON since the Magento\Framework\Serialize\Serializer\Json is used as serializer by default starting from Magento 2.2.x version and up.

<?php
/**
 * @author Atwix Team
 * @copyright Copyright (c) 2018 Atwix (https://www.atwix.com/)
 * @package Atwix_DynamicFields
 */

namespace Atwix\DynamicFields\Config\Backend;

use Magento\Framework\App\Cache\TypeListInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Config\Value as ConfigValue;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\Model\Context;
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Registry;
use Magento\Framework\Serialize\SerializerInterface;

/**
 * Class AdditionalEmail
 */
class AdditionalEmail extends ConfigValue
{
    /**
     * Json Serializer
     *
     * @var SerializerInterface
     */
    protected $serializer;

    /**
     * ShippingMethods constructor
     *
     * @param SerializerInterface $serializer
     * @param Context $context
     * @param Registry $registry
     * @param ScopeConfigInterface $config
     * @param TypeListInterface $cacheTypeList
     * @param AbstractResource|null $resource
     * @param AbstractDb|null $resourceCollection
     * @param array $data
     */
    public function __construct(
        SerializerInterface $serializer,
        Context $context,
        Registry $registry,
        ScopeConfigInterface $config,
        TypeListInterface $cacheTypeList,
        AbstractResource $resource = null,
        AbstractDb $resourceCollection = null,
        array $data = []
    ) {
        $this->serializer = $serializer;
        parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
    }

    /**
     * Prepare data before save
     *
     * @return void
     */
    public function beforeSave()
    {
        /** @var array $value */
        $value = $this->getValue();
        unset($value['__empty']);
        $encodedValue = $this->serializer->serialize($value);

        $this->setValue($encodedValue);
    }

    /**
     * Process data after load
     *
     * @return void
     */
    protected function _afterLoad()
    {
        /** @var string $value */
        $value = $this->getValue();
        $decodedValue = $this->serializer->unserialize($value);

        $this->setValue($decodedValue);
    }
}

Now we need to define the field in etc/adminhtml/system.xml file of our module.

<?xml version="1.0"?>
<!--
/**
 * @author Atwix Team
 * @copyright Copyright (c) 2018 Atwix (https://www.atwix.com/)
 * @package Atwix_DynamicFields
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="sample" translate="label" sortOrder="50">
            <label>Sample</label>
        </tab>
        <section id="atwix_sample" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
            <class>separator-top</class>
            <label>Atwix Sample</label>
            <tab>sample</tab>
            <resource>Atwix_DynamicFields::dynamic_fields</resource>
            <group id="additional" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Additional</label>
                <field id="emails" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Additional Emails</label>
                    <frontend_model>Atwix\DynamicFields\Block\Adminhtml\Form\Field\AdditionalEmail</frontend_model>
                    <backend_model>Atwix\DynamicFields\Config\Backend\AdditionalEmail</backend_model>
                </field>
            </group>
        </section>
    </system>
</config>

Let’s check the result in Stores -> Configuration -> SAMPLE -> Atwix Sample -> Additional.

Magento 2 System Config Dynamic Fields

Each field in a row is also validated separately.

Magento 2 System Config Dynamic Fields Validation

Feel free to share your experience of system configs fields implementation in comments. Thanks for reading!