Module Setup
Module Structure
Vendor/AdminConfig/
├── etc/
│ ├── module.xml
│ ├── registration.php
│ ├── system.xml
│ ├── config.xml
│ └── di.xml
├── Model/
│ └── Config/
│ ├── Backend/
│ │ ├── Logo.php
│ │ └── Color.php
│ └── Source/
│ ├── Yesno.php
│ └── PageSize.php
├── Block/
│ └── System\Config\Form.php
└── Helper/
└── Data.php
system.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_System:etc/system_file.xsd">
<system>
<sections>
<section id="custom_settings" translate="label" type="text" sortOrder="310"
showInDefault="1" showInWebsite="1" showInStore="1">
<group id="general" translate="label" type="text" sortOrder="10"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Custom Settings</label>
<field id="enabled" translate="label comment" type="select" sortOrder="10"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Enable Module</label>
<comment>Enable or disable the custom module</comment>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="store_name" translate="label comment" type="text" sortOrder="20"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Store Name</label>
<comment>Enter your store name</comment>
<validate>required-entry</validate>
</field>
<field id="logo" translate="label comment" type="image" sortOrder="30"
showInDefault="1" showInWebsite="0" showInStore="0">
<label>Logo Image</label>
<comment>Upload your store logo</comment>
<backend_model>Vendor\AdminConfig\Model\Config\Backend\Logo</backend_model>
<upload_dir>custom/logo</upload_dir>
</field>
<field id="primary_color" translate="label comment" type="text" sortOrder="40"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Primary Color</label>
<comment>Enter hex color code (e.g., #1979c3)</comment>
<backend_model>Vendor\AdminConfig\Model\Config\Backend\Color</backend_model>
<validate>required-entry color</validate>
</field>
</group>
<group id="email_settings" translate="label" type="text" sortOrder="20"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Email Settings</label>
<field id="sender_email" translate="label comment" type="text" sortOrder="10"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Sender Email</label>
<validate>required-entry email</validate>
</field>
<field id="sender_name" translate="label comment" type="text" sortOrder="20"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Sender Name</label>
<validate>required-entry</validate>
</field>
<field id="email_template" translate="label comment" type="select" sortOrder="30"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Email Template</label>
<source_model>Magento\Config\Model\Config\Source\Email\Template</source_model>
</field>
</group>
<group id="advanced" translate="label" type="text" sortOrder="30"
showInDefault="1" showInWebsite="0" showInStore="0">
<label>Advanced Settings</label>
<field id="page_size" translate="label comment" type="select" sortOrder="10"
showInDefault="1" showInWebsite="0" showInStore="0">
<label>Page Size</label>
<source_model>Vendor\AdminConfig\Model\Config\Source\PageSize</source_model>
</field>
<field id="enable_cache" translate="label comment" type="select" sortOrder="20"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Enable Cache</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="cache_lifetime" translate="label comment" type="text" sortOrder="30"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Cache Lifetime (seconds)</label>
<validate>validate-number validate-digits</validate>
<depend><field id="enable_cache">1</field></depend>
</field>
</group>
</section>
</sections>
</system>
</config>
config.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<custom_settings>
<general>
<enabled>1</enabled>
<store_name>My Store</store_name>
<primary_color>#1979c3</primary_color>
</general>
<email_settings>
<sender_email>store@example.com</sender_email>
<sender_name>Store Admin</sender_name>
</email_settings>
<advanced>
<page_size>20</page_size>
<enable_cache>1</enable_cache>
<cache_lifetime>3600</cache_lifetime>
</advanced>
</custom_settings>
</default>
</config>
Helper
<?php
namespace Vendor\AdminConfig\Helper;
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\StoreManagerInterface;
class Data extends AbstractHelper
{
const XML_PATH_ENABLED = 'custom_settings/general/enabled';
const XML_PATH_STORE_NAME = 'custom_settings/general/store_name';
const XML_PATH_PRIMARY_COLOR = 'custom_settings/general/primary_color';
const XML_PATH_SENDER_EMAIL = 'custom_settings/email_settings/sender_email';
const XML_PATH_SENDER_NAME = 'custom_settings/email_settings/sender_name';
const XML_PATH_PAGE_SIZE = 'custom_settings/advanced/page_size';
const XML_PATH_CACHE_ENABLED = 'custom_settings/advanced/enable_cache';
const XML_PATH_CACHE_LIFETIME = 'custom_settings/advanced/cache_lifetime';
public function __construct(
ScopeConfigInterface $scopeConfig,
StoreManagerInterface $storeManager,
array $data = []
) {
parent::__construct($scopeConfig, $data);
$this->storeManager = $storeManager;
}
public function isEnabled($storeId = null)
{
return $this->isSetFlag(self::XML_PATH_ENABLED, $storeId);
}
public function getStoreName($storeId = null)
{
return $this->getValue(self::XML_PATH_STORE_NAME, $storeId);
}
public function getPrimaryColor($storeId = null)
{
return $this->getValue(self::XML_PATH_PRIMARY_COLOR, $storeId);
}
public function getSenderEmail($storeId = null)
{
return $this->getValue(self::XML_PATH_SENDER_EMAIL, $storeId);
}
public function getSenderName($storeId = null)
{
return $this->getValue(self::XML_PATH_SENDER_NAME, $storeId);
}
public function getPageSize($storeId = null)
{
return (int) $this->getValue(self::XML_PATH_PAGE_SIZE, $storeId);
}
public function isCacheEnabled($storeId = null)
{
return $this->isSetFlag(self::XML_PATH_CACHE_ENABLED, $storeId);
}
public function getCacheLifetime($storeId = null)
{
return (int) $this->getValue(self::XML_PATH_CACHE_LIFETIME, $storeId);
}
}
Backend Models
Logo Upload Backend
<?php
namespace Vendor\AdminConfig\Model\Config\Backend;
use Magento\Config\Model\Config\Backend\File;
use Magento\Config\Model\Config\Backend\File\Validator\NotProtectedExtensions;
class Logo extends File
{
protected $_allowedExtensions = ['jpg', 'jpeg', 'gif', 'png', 'svg'];
protected $_fileName = 'logo.png';
public function beforeSave(): $this
{
$value = $this->getValue();
if (isset($_FILES['groups'])) {
$tmpName = $_FILES['groups']['tmp_name'][$this->getGroupId()]['fields'][$this->getField()]['name'];
if ($tmpName) {
$validator = new NotProtectedExtensions();
$validator->setAllowedExtensions($this->_allowedExtensions);
if (!$validator->isValid($tmpName)) {
throw new \Magento\Framework\Exception\LocalizedException(
__('Invalid file type. Allowed: %1', implode(', ', $this->_allowedExtensions))
);
}
}
}
return parent::beforeSave();
}
public function afterSave(): $this
{
$path = $this->getValue();
if ($path && is_file($path)) {
$uploadDir = $this->_getUploadDir();
$fileName = basename($path);
$destPath = $uploadDir . DIRECTORY_SEPARATOR . $fileName;
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
copy($path, $destPath);
unlink($path);
$this->setValue($fileName);
}
return parent::afterSave();
}
protected function _getUploadDir(): string
{
return BP . DIRECTORY_SEPARATOR . 'pub' . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'custom' . DIRECTORY_SEPARATOR . 'logo';
}
}
Color Validation Backend
<?php
namespace Vendor\AdminConfig\Model\Config\Backend;
use Magento\Framework\App\Config\Value;
class Color extends Value
{
public function beforeSave(): $this
{
$value = $this->getValue();
// Remove # if present
$value = ltrim($value, '#');
// Validate hex color
if (!preg_match('/^[a-fA-F0-9]{6}$/', $value)) {
throw new \Magento\Framework\Exception\LocalizedException(
__('Invalid hex color code. Use format: #1979c3 or 1979c3')
);
}
$this->setValue('#' . $value);
return parent::beforeSave();
}
}
Yes/No Source Model
<?php
namespace Vendor\AdminConfig\Model\Config\Source;
use Magento\Framework\Data\OptionarrayInterface;
class Yesno implements OptionarrayInterface
{
public function toOptionArray(): array
{
return [
['value' => 1, 'label' => __('Yes')],
['value' => 0, 'label' => __('No')]
];
}
}
Page Size Source Model
<?php
namespace Vendor\AdminConfig\Model\Config\Source;
use Magento\Framework\Data\OptionarrayInterface;
class PageSize implements OptionarrayInterface
{
public function toOptionArray(): array
{
return [
['value' => 10, 'label' => __('10')],
['value' => 20, 'label' => __('20')],
['value' => 50, 'label' => __('50')],
['value' => 100, 'label' => __('100')],
['value' => 200, 'label' => __('200')]
];
}
}
Observer for Config Change
<?php
namespace Vendor\AdminConfig\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Magento\Framework\App\Config\ReinitableConfigInterface;
use Psr\Log\LoggerInterface;
class ConfigChange implements ObserverInterface
{
public function __construct(
private ReinitableConfigInterface $config,
private LoggerInterface $logger
) {}
public function execute(Observer $observer)
{
$changedPaths = $observer->getEvent()->getChangedPaths();
foreach ($changedPaths as $path) {
if (strpos($path, 'custom_settings/') === 0) {
$this->logger->info('Custom config changed: ' . $path);
$this->config->reinit();
}
}
}
}
events.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="admin_system_config_changed_section_custom_settings">
<observer name="custom_config_change" instance="Vendor\AdminConfig\Observer\ConfigChange"/>
</event>
</config>
Field Dependencies
Field Dependencies
Basic Dependency
<!-- Show field only when another field has specific value -->
<field id="cache_lifetime">
<depend><field id="enable_cache">1</field></depend>
</field>
Inverse Dependency
<!-- Hide field when another field has specific value -->
<field id="advanced_option">
<depend><field id="simple_mode">0</field></depend>
</field>
Multiple Dependencies
<!-- AND logic -->
<field id="special_field">
<depend>
<field id="option1">1</field>
<field id="option2">value2</field>
</depend>
</field>
Custom Field Dependencies (JavaScript)
// view/adminhtml/web/js/config.js
define([
'jquery',
'Magento_Ui/js/form/element/select'
], function ($, Select) {
'use strict';
return Select.extend({
initialize: function () {
this._super();
// Watch parent field
this.source.on('custom_settings_general_enabled:updated', function (value) {
this.toggleVisibility(value === '1');
}.bind(this));
return this;
},
toggleVisibility: function (visible) {
this.visible(visible);
}
});
});
Field Validation
<!-- Required field -->
<field id="required_field">
<validate>required-entry</validate>
</field>
<!-- Email validation -->
<field id="email">
<validate>required-entry email</validate>
</field>
<!-- Number validation -->
<field id="number">
<validate>validate-number validate-digits</validate>
</field>
<!-- URL validation -->
<field id="url">
<validate>validate-url</validate>
</field>
<!-- Custom validation -->
<field id="custom">
<validate>required-entry validate-length maximum-length-255</validate>
</field>
Custom Validation (JavaScript)
// view/adminhtml/web/js/validation.js
define([
'jquery',
'mage/validation'
], function ($) {
'use strict';
return {
'validate-hex-color': function (value) {
return /^#?[a-fA-F0-9]{6}$/.test(value);
},
'validate-positive-number': function (value) {
return value > 0 && !isNaN(value);
}
};
});
// Register in system.xml
<field id="primary_color">
<validate>validate-hex-color</validate>
</field>
Config Encryption
// For sensitive data like API keys
<field id="api_key" type="obfuscated">
<label>API Key</label>
<comment>This value will be encrypted in the database</comment>
</field>
// In di.xml
<type name="Magento\Config\Model\Config\Backend\Encrypted">
<arguments>
<argument name="path" xsi:type="string">custom_settings/advanced/api_key</argument>
</arguments>
</type>
Export/Import Config
// Export configuration
$export = $objectManager->create(\Magento\Config\Model\Config\Exporter::class);
$exportData = $export->exportXml();
// Import configuration
$import = $objectManager->create(\Magento\Config\Model\Config\Importer::class);
$import->importXml($xmlData);
Testing Configuration
Test Configuration Access
<?php
namespace Vendor\AdminConfig\Test\Unit\Helper;
use PHPUnit\Framework\TestCase;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\StoreManagerInterface;
use Vendor\AdminConfig\Helper\Data;
class DataTest extends TestCase
{
private $helper;
private $scopeConfigMock;
protected function setUp(): void
{
$this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
$this->storeManagerMock = $this->createMock(StoreManagerInterface::class);
$this->helper = new Data(
$this->scopeConfigMock,
$this->storeManagerMock
);
}
public function testIsEnabled()
{
$this->scopeConfigMock->expects($this->once())
->method('isSetFlag')
->with('custom_settings/general/enabled')
->willReturn(true);
$this->assertTrue($this->helper->isEnabled());
}
public function testGetStoreName()
{
$this->scopeConfigMock->expects($this->once())
->method('getValue')
->with('custom_settings/general/store_name')
->willReturn('My Store');
$this->assertEquals('My Store', $this->helper->getStoreName());
}
public function testGetPageSize()
{
$this->scopeConfigMock->expects($this->once())
->method('getValue')
->with('custom_settings/advanced/page_size')
->willReturn('20');
$this->assertEquals(20, $this->helper->getPageSize());
}
}
Integration Test
<?php
namespace Vendor\AdminConfig\Test\Integration\Helper;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
use Vendor\AdminConfig\Helper\Data;
class DataIntegrationTest extends TestCase
{
public function testConfigValues()
{
$helper = Bootstrap::getObjectManager()->create(Data::class);
// Test default values
$this->assertTrue($helper->isEnabled());
$this->assertEquals('My Store', $helper->getStoreName());
$this->assertEquals('#1979c3', $helper->getPrimaryColor());
$this->assertEquals('store@example.com', $helper->getSenderEmail());
}
public function testConfigScope()
{
$helper = Bootstrap::getObjectManager()->create(Data::class);
// Test store-specific value
$storeId = 1;
$value = $helper->getStoreName($storeId);
$this->assertNotNull($value);
}
}
Manual Testing
1. Navigate to Stores > Configuration
2. Find Custom Settings section
3. Test field dependencies:
- Toggle Enable Module
- Verify Cache Lifetime visibility changes
4. Test validation:
- Submit without required fields
- Enter invalid hex color
- Enter invalid email
5. Test save:
- Save configuration
- Verify values persist
- Check admin logs
6. Test scope:
- Change store view
- Verify values are different
CLI Testing
# Check configuration value
php bin/magento config:show custom_settings/general/enabled
# Set configuration value
php bin/magento config:set custom_settings/general/enabled 1
# Delete configuration value
php bin/magento config:delete custom_settings/general/store_name
# Export configuration
php bin/magento app:config:dump
# Import configuration
php bin/magento app:config:import
Troubleshooting
- Fields not showing: Check showInDefault/Website/Store attributes
- Validation not working: Verify JS file path and registration
- Backend model errors: Check class path and interface implementation
- Config not saving: Check ACL permissions and admin role
- Scope not working: Verify store ID is correct
Practice Problems
Create a complete admin configuration section with validation, dependencies, and custom backend models.
Build a helper class to access configuration values with scope support.
Quiz
1. What defines admin configuration fields?
2. What does config.xml provide?
3. How to make a field encrypted?
4. How to access config values in code?
Flashcards
Question
What defines admin config fields?
Click to reveal answer
Answer
system.xml defines sections, groups, and fields
Question
What provides default values?
Click to reveal answer
Answer
config.xml sets default configuration values
Question
How to validate fields?
Click to reveal answer
Answer
Add validate attribute in system.xml or custom JS validation
Question
How to access config in code?
Click to reveal answer
Answer
Helper class with ScopeConfigInterface
Question
How to encrypt sensitive data?
Click to reveal answer
Answer
Use type="obfuscated" or type="encrypted"
Revision Notes
Key Takeaways
- 1. system.xml defines admin config: sections > groups > fields
- 2. config.xml provides default values for configuration
- 3. Backend models handle save/validation logic
- 4. Source models provide dropdown options
- 5. Dependencies control field visibility
- 6. Use Helper with ScopeConfig to access values in code
Interview Tips
- • How do you create admin configuration in Magento?
- • Explain the difference between system.xml and config.xml
- • How do you add validation to configuration fields?
- • How do you access configuration values in code?
- • How do you handle configuration for multi-store setups?
Cheat Sheet
Admin Configuration Cheat Sheet
Files:
- system.xml: field structure
- config.xml: default values
- di.xml: class preferences
Structure:
- section > group > field
Field Attributes:
- translate: label/comment
- type: text, select, image, etc.
- sortOrder: display order
- showInDefault/Website/Store
Validation:
- required-entry
- email, validate-url
- validate-number
Access:
- Helper with ScopeConfig
- $this->scopeConfig->getValue('path')
- $this->scopeConfig->isSetFlag('path')