Dear Friend and Visitor,
Today I’m going to show you how to create (and easily extend) a custom Store Configuration that will allow you to save a table with selects and multiple rows into a single core_config_data row in the database.
You probably felt somewhere across your path a need for a dynamic and easily extensible config field.
Magento isn’t too helpful with the default types (i.e text, date, etc), but fortunately, they prepared a workaround via the Magento_Config module.
Desired Result we are looking for is following:
To render this table we will use Magento_Config’s AbstractFieldArray by extending:
\Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray:class
Step 1: Create new module
app/code/PeterRusin/DynamicField/registration.php
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'PeterRusin_DynamicField',
__DIR__
);
app/code/PeterRusin/DynamicField/etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="PeterRusin_DynamicField"/>
</config>
Step 2: Create DynamicField class
app/code/PeterRusin/DynamicField/Block/Adminhtml/System/Config/Form/Field/DynamicField.php
<?php
declare(strict_types=1);
namespace PeterRusin\DynamicField\Block\Adminhtml\System\Config\Form\Field;
use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray;
class DynamicField extends AbstractFieldArray
{
protected function _prepareToRender()
{
$this->addColumn('comment', [
'label' => __('Comment'),
'class' => 'required-entry',
'style' => 'width:75px'
]);
$this->_addAfter = false;
$this->_addButtonLabel = (string)__('Add Todo Comment');
}
}
Step 3: Add DynamicField to system.xml
app/code/PeterRusin/DynamicField/etc/adminhtml/system.xml
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<tab id="peterrusin" sortOrder="200">
<label>Peter Rusin</label>
</tab>
<section id="peterrusin_dynamicfield" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1"
showInStore="1">
<label>Dynamic Field</label>
<tab>peterrusin</tab>
<resource>PeterRusin_DynamicField::system_config</resource>
<group id="general" translate="label" type="text" showInDefault="1" showInWebsite="1" showInStore="1">
<label>General Settings</label>
<field id="dynamicfield_example" type="select" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Dynamic Field Example</label>
<frontend_model>PeterRusin\DynamicField\Block\Adminhtml\System\Config\Form\Field\DynamicField</frontend_model>
<backend_model>Magento\Config\Model\Config\Backend\Serialized\ArraySerialized</backend_model>
</field>
</group>
</section>
</system>
</config>
app/code/PeterRusin/DynamicField/acl.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://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::stores">
<resource id="Magento_Backend::stores_settings">
<resource id="Magento_Config::config">
<resource id="PeterRusin_DynamicField::system_config" title="PeterRusin_DynamicField Configuration" sortOrder="10" />
</resource>
</resource>
</resource>
</resource>
</resources>
</acl>
</config>
So far we’ve only added one column, and the table looks like this:
Step 4: Add Option Source for our select
app/code/PeterRusin/DynamicField/Model/Config/Source/Todo.php
<?php
declare(strict_types=1);
namespace PeterRusin\DynamicField\Model\Config\Source;
use GuzzleHttp\ClientFactory;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\ResponseFactory;
use Magento\Framework\Data\OptionSourceInterface;
use Magento\Framework\Serialize\SerializerInterface;
/** @link https://devdocs.magento.com/guides/v2.4/ext-best-practices/tutorials/create-integration-with-api.html */
class Todo implements OptionSourceInterface
{
const API_REQUEST_URI = 'https://jsonplaceholder.typicode.com';
/** @var ClientFactory */
private $clientFactory;
/** @var ResponseFactory */
private $responseFactory;
/** @var SerializerInterface */
private $serializer;
public function __construct(
ClientFactory $clientFactory,
ResponseFactory $responseFactory,
SerializerInterface $serializer
) {
$this->clientFactory = $clientFactory;
$this->responseFactory = $responseFactory;
$this->serializer = $serializer;
}
public function toOptionArray()
{
$client = $this->clientFactory->create(
[
'config' => [
'base_uri' => self::API_REQUEST_URI
]
]
);
try {
$response = $client->request('GET', 'todos');
} catch (GuzzleException $exception) {
$response = $this->responseFactory->create([
'status' => $exception->getCode(),
'reason' => $exception->getMessage(),
'body' => '[]'
]);
}
$responseBody = $response->getBody();
$responseContent = $responseBody->getContents();
return array_map(
function ($todo) {
return [
'label' => $todo['title'],
'value' => $todo['id']
];
},
$this->serializer->unserialize($responseContent)
);
}
}
Step 5: Add our select to the Dynamic Field
app/code/PeterRusin/DynamicField/Block/Adminhtml/System/Config/Form/Field/DynamicField.php
<?php
declare(strict_types=1);
namespace PeterRusin\DynamicField\Block\Adminhtml\System\Config\Form\Field;
use Magento\Backend\Block\Template\Context;
use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray;
use Magento\Framework\Data\OptionSourceInterface;
use Magento\Framework\DataObject;
use Magento\Framework\View\Helper\SecureHtmlRenderer;
use PeterRusin\DynamicField\Model\Config\Source\Todo;
class DynamicField extends AbstractFieldArray
{
/** @var Todo */
private $todoOptions;
public function __construct(
Context $context,
Todo $todoOptions,
array $data = [],
?SecureHtmlRenderer $secureRenderer = null
) {
parent::__construct($context, $data, $secureRenderer);
$this->todoOptions = $todoOptions;
}
private function markOptionAsSelected(
DataObject $row,
string $optionKey,
OptionSourceInterface $optionSource,
array &$options
) {
$optionValue = $row->getData($optionKey);
if ($optionValue) {
$optionHash = $this->createSelectRenderer($optionSource)->calcOptionHash($optionValue);
$options['option_' . $optionHash] = 'selected="selected"';
}
}
protected function _prepareArrayRow(DataObject $row)
{
$options = [];
$this->markOptionAsSelected($row, 'todo', $this->todoOptions, $options);
$row->setData('option_extra_attrs', $options);
}
protected function _prepareToRender()
{
$this->addColumn('todo', [
'label' => __('Todo'),
'class' => 'required-entry',
'style' => 'width:150px',
'renderer' => $this->createSelectRenderer($this->todoOptions)
]);
$this->addColumn('comment', [
'label' => __('Comment'),
'class' => 'required-entry',
'style' => 'width:75px'
]);
$this->_addAfter = false;
$this->_addButtonLabel = (string)__('Add Todo Comment');
}
private function createSelectRenderer(OptionSourceInterface $optionSource)
{
$layout = $this->getLayout();
/** @var Select $htmlSelect */
$htmlSelect = $layout->createBlock(
Select::class,
'',
['data' => ['is_render_to_js_template' => true]]
);
return $htmlSelect->setOptions(
$optionSource->toOptionArray()
);
}
}
app/code/PeterRusin/DynamicField/Block/Adminhtml/System/Config/Fom/Field/Select.php
<?php
declare(strict_types=1);
namespace PeterRusin\DynamicField\Block\Adminhtml\System\Config\Form\Field;
class Select extends \Magento\Framework\View\Element\Html\Select
{
public function setInputId(string $id)
{
$this->setId($id);
return $this;
}
public function setInputName(string $name)
{
$this->setName($name);
return $this;
}
}