Skip to content
beginner Phase 116 · Beginner Projects

Project: Admin System Configuration

Build admin system configuration with custom fields, backend models, and validation

45m
2 problems
Topic Progress 0%

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

0 / 2 solved
Admin Configuration

Create a complete admin configuration section with validation, dependencies, and custom backend models.

Config Helper

Build a helper class to access configuration values with scope support.

Quiz

1. What defines admin configuration fields?

Question 1 options

2. What does config.xml provide?

Question 2 options

3. How to make a field encrypted?

Question 3 options

4. How to access config values in code?

Question 4 options

Flashcards

Question

What defines admin config fields?

Answer

system.xml defines sections, groups, and fields

Question

What provides default values?

Answer

config.xml sets default configuration values

Question

How to validate fields?

Answer

Add validate attribute in system.xml or custom JS validation

Question

How to access config in code?

Answer

Helper class with ScopeConfigInterface

Question

How to encrypt sensitive data?

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')