Admin Configuration Fields with system.xml

system.xml builds the Stores > Configuration form: sections, groups, fields, and the core_config_data row each one writes.

Syntax<field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">

config.xml gives a setting a default. system.xml gives a person somewhere to change it. It describes one page of the form under Stores > Configuration in the admin.

Three levels, one path

A <section> is a page, a <group> is a collapsible fieldset, a <field> is one input. The three id attributes joined by slashes are the config path, so id="loyalty", id="general" and id="enabled" together write loyalty/general/enabled. The section also names the tab it appears in and the ACL resource that guards it.

Fields

type picks the input: text, textarea, select, multiselect, obscure. A select needs a <source_model> to supply its options. A <backend_model> runs on save and on load. <depends> hides a field until another field holds a given value, which is a JavaScript effect in the form only and not a validation rule. canRestore="1" adds the Use system value checkbox that puts the field back to its config.xml default.

Where the value goes

Saving the form writes one row into core_config_data, with columns scope, scope_id, path and value. Nothing is written while a field still matches the value it inherits. After editing system.xml run bin/magento cache:flush.

Example

Example · xml
<!-- app/code/Acme/Loyalty/etc/adminhtml/system.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="loyalty" translate="label" type="text" sortOrder="400"
                 showInDefault="1" showInWebsite="1" showInStore="1">
            <label>Loyalty</label>
            <tab>general</tab>
            <resource>Acme_Loyalty::config_loyalty</resource>
            <group id="general" translate="label" type="text" sortOrder="10"
                   showInDefault="1" showInWebsite="1" showInStore="1">
                <label>General</label>
                <field id="enabled" translate="label" type="select" sortOrder="10"
                       showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
                    <label>Enable Loyalty Points</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="points_per_currency" translate="label comment" type="text" sortOrder="20"
                       showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
                    <label>Points Per Unit Of Currency</label>
                    <comment>Whole points awarded for each unit spent.</comment>
                    <validate>validate-digits</validate>
                    <depends>
                        <field id="enabled">1</field>
                    </depends>
                </field>
            </group>
        </section>
    </system>
</config>

When to use it

  • Expose a module setting to a merchant instead of making them ask a developer to edit a file.
  • Offer a fixed list of choices with a select and a source model, so an invalid value cannot be saved.
  • Hide advanced fields until the feature that uses them is switched on.
  • Give a merchant a way back to the shipped default with the Use system value checkbox.

More examples

The config.xml that pairs with it

The node names here must match the section, group and field ids in system.xml exactly, or the form writes one path while your code reads another and the setting appears to do nothing. These defaults are also what canRestore="1" restores: with no config.xml entry, Use system value puts the field back to an empty value. Run bin/magento cache:flush config after editing.

Example · xml
<!-- app/code/Acme/Loyalty/etc/config.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <loyalty>
            <general>
                <enabled>0</enabled>
                <points_per_currency>10</points_per_currency>
            </general>
        </loyalty>
    </default>
</config>

A source model for a select

Referenced from a field as <source_model>Acme\Loyalty\Config\Source\RoundingMode</source_model>. Magento\Framework\Data\OptionSourceInterface declares one method, toOptionArray, returning value and label pairs. Magento's own Magento\Config\Model\Config\Source\Yesno is the same shape and is worth reusing for plain on/off fields rather than writing your own. New classes need bin/magento setup:di:compile in production mode.

Example · php
<?php
// app/code/Acme/Loyalty/Config/Source/RoundingMode.php
declare(strict_types=1);

namespace Acme\Loyalty\Config\Source;

use Magento\Framework\Data\OptionSourceInterface;

class RoundingMode implements OptionSourceInterface
{
    public const MODE_DOWN = 'down';
    public const MODE_NEAREST = 'nearest';

    public function toOptionArray(): array
    {
        return [
            ['value' => self::MODE_DOWN, 'label' => __('Round down')],
            ['value' => self::MODE_NEAREST, 'label' => __('Round to nearest')],
        ];
    }
}

// Rendered as two <option> elements in the admin select.
// The saved core_config_data value is the string 'down' or 'nearest'.

depends in both forms, and config_path

The two <depends> entries express the same condition: inside the group general, a bare id and */general/enabled resolve to the same field. Use the longhand whenever the controlling field sits in a different group. <config_path> decouples the stored path from the form position, which is how Magento_Paypal shows one setting in several payment sections while writing a single row; without it the path is always section/group/field. Apply with bin/magento cache:flush.

Example · xml
<!-- app/code/Acme/Loyalty/etc/adminhtml/system.xml (fields only) -->
<field id="notify_email" translate="label" type="text" sortOrder="30"
       showInDefault="1" showInWebsite="1" canRestore="1">
    <label>Send Notices To</label>
    <validate>validate-email</validate>

    <!-- Shorthand: a bare id means a field in the SAME group. -->
    <depends>
        <field id="enabled">1</field>
    </depends>
</field>

<field id="notify_cc" translate="label" type="text" sortOrder="40"
       showInDefault="1" showInWebsite="1" canRestore="1">
    <label>Copy Notices To</label>

    <!-- Longhand: */group/field addresses any group in THIS section. -->
    <depends>
        <field id="*/general/enabled">1</field>
    </depends>

    <!-- The id says notify_cc, but the value is stored at this path. -->
    <config_path>loyalty/general/notify_cc</config_path>
</field>

Discussion

  • Be the first to comment on this lesson.